XXE injection: the XML parser bug AI coders keep shipping
By Seaworthy · 27 June 2026
Photo by Philipp Katzenberger on Unsplash
Why XML is still in your stack
Nobody chooses XML in 2026. It chooses you. A supplier sends an order feed in it. A third-party API responds with it. A config file uses it because it was written in 2009 and nobody has touched it since. So you ask an AI assistant to write a quick parser, it spits out something that works in testing, and you ship it.
That parser is almost certainly vulnerable to XXE injection.
What XXE actually does
XML has a feature called external entities. The spec allows an XML document to declare a reference to an external resource, like a file on disk or a URL, and have the parser substitute its content inline. This was designed for legitimate document reuse. It has become one of the most reliably exploitable bugs in any stack that processes user-supplied XML.
An attacker sends you a document like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE order [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<order>
<item>&xxe;</item>
</order>
Your parser resolves &xxe;, reads /etc/passwd, and drops it into the output. If you reflect that output anywhere, the attacker has just read an arbitrary file from your server.
SSRF works the same way. Swap file:// for http://169.254.169.254/latest/meta-data/ and the parser makes an outbound HTTP request to the AWS metadata endpoint, handing the attacker your instance credentials. No network firewall saves you here: the request originates from inside your own process.
What AI assistants ship by default
Here is what you get when you ask an LLM to "parse this XML in Node.js" and it reaches for a popular library:
// Vulnerable: libxmljs with default options
const libxmljs = require('libxmljs');
function parseOrder(xmlString) {
const doc = libxmljs.parseXmlString(xmlString);
return {
item: doc.get('//item').text(),
};
}
libxmljs resolves external entities by default. So does the underlying expat parser used by many other packages. The code above will happily read from disk or make outbound HTTP calls on behalf of any caller who controls the XML input.
The same pattern appears with xml2js wrappers, with hand-rolled SAX parsers, and with SOAP clients. The common thread: the AI generated working code for the happy path. It did not think about what the parser does with a malicious document, because you did not ask it to.
Safer alternatives
The fix depends on which library you are using. For most Node.js XML work I now reach for fast-xml-parser, which disables external entity resolution by default and is actively maintained:
// Safe: fast-xml-parser with explicit options
const { XMLParser } = require('fast-xml-parser');
function parseOrder(xmlString) {
const parser = new XMLParser({
// External entity processing is off by default in fast-xml-parser.
// Be explicit anyway so future readers know it was a deliberate choice.
processEntities: false,
allowBooleanAttributes: false,
});
const result = parser.parse(xmlString);
return {
item: result?.order?.item ?? null,
};
}
If you are stuck on libxmljs or need a SAX-based approach, you need to pass options that disable DTD loading and external entity resolution. Read the library docs carefully: option names vary, and some libraries silently ignore options they do not recognise.
One general rule I apply: if the library's README shows a one-liner parse call with no options object, assume it is unsafe until proven otherwise.
Why this keeps slipping through
Code review is hard when reviewers are not looking for it. A parser call looks benign. The vulnerability is in what the parser does at runtime, not what the code says at a glance. Static linters do not flag this by default. Most unit tests do not include adversarial XML. And AI-generated code looks confident: it passes syntax checks, it runs, the tests go green.
The broader issue is that AI assistants are trained to generate code that works for normal inputs. Adversarial inputs are not normal. A model that has seen ten thousand XML parsing examples in its training data has probably seen nine thousand of them without any security options, because most developers writing tutorials are not thinking about XXE either.
This is not a criticism of LLMs specifically. Human developers make the same mistake. But when you are generating code faster than you can review it, the rate of this particular mistake goes up.
What to do right now
Audit every XML parser call in your codebase. Look at what library is being used, look at the options being passed, then read the library's security documentation. If there is no security documentation, that is itself a signal.
For new code, default to fast-xml-parser or an equivalent library with safe defaults, and be explicit about the options you pass. Comment on why you are setting them: it keeps the next developer from "simplifying" them out.
If you accept XML from external sources, add a schema validation step after parsing. This will not stop XXE at the parser level, but it limits what an attacker can do with the output.
Seaworthy's static analysis checks Node.js projects for XML parser usage with known-unsafe defaults and reports it as a data exposure finding.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.