Safeguard
AppSec

Choosing an npm XML Parser: Security Comparison and XXE Pitfalls

Not every npm XML parser carries the same risk. We compare xml2js, fast-xml-parser, sax, and libxmljs on their CVE history, XXE exposure, and safe configuration.

Priya Mehta
Security Analyst
7 min read

The safest npm XML parser for most Node.js services is a pure-JavaScript one — fast-xml-parser with entity processing disabled, or xml2js at 0.5.0 or later — because the classic XXE attack mostly lives in parsers that wrap the native libxml2 library. That single sentence hides a lot of nuance, though. Every popular XML library on npm has shipped at least one real vulnerability in the last few years, and the failure modes differ: prototype pollution in one, regex-injection denial of service in another, and memory-level type confusion in the native bindings. If you picked your parser off the first page of an "xml parser npm" search and never revisited the decision, this comparison is for you.

The main contenders and where they sit

Five libraries cover the vast majority of XML parsing in the Node ecosystem:

  • xml2js — the long-standing default for turning XML into plain JavaScript objects. Callback-based, widely embedded in older SDKs.
  • fast-xml-parser — pure JS, no dependencies, very fast, actively maintained. Used inside the AWS SDK v3, which tells you something about its footprint.
  • sax — a streaming, event-based parser. Low-level, small attack surface, but you build the object mapping yourself.
  • @xmldom/xmldom — a DOM implementation for Node, the maintained fork of the abandoned xmldom package.
  • libxmljs — native bindings to the C library libxml2. Fast and standards-complete, but it inherits libxml2's behaviors, including external entity expansion.

The architectural split matters more than the API differences. Pure-JavaScript parsers generally do not implement external entity resolution at all, which means the textbook XXE payload — a DOCTYPE declaring an external entity that reads /etc/passwd or calls out to an attacker URL — simply has nothing to expand it. Native-binding parsers can expand entities if you turn the wrong flag on.

What XXE actually requires, and which parsers are exposed

XML External Entity injection needs two things: a parser that processes DOCTYPE declarations, and a resolver willing to fetch or substitute the entity content. In libxmljs, passing noent: true to the parse options tells libxml2 to substitute entities, and combining user-supplied XML with that flag is the canonical Node.js XXE setup. The fix is boring and absolute: never enable noent on untrusted input, and avoid XML_PARSE_HUGE as well, since it removes the internal limits that act as a backstop.

Pure-JS parsers dodge classic file-read XXE but not the DOCTYPE machinery entirely. Internal entity expansion still enables billion-laughs-style memory exhaustion if the parser expands nested entities without limits, and fast-xml-parser's own advisory history (more below) shows that even the entity name handling can be a denial-of-service vector. Treat DOCTYPE support as a feature you must explicitly justify, not a default you tolerate.

CVE history: what each parser has actually shipped

These are verified, published advisories — worth knowing because they tell you how each project fails and how it responds.

xml2js — CVE-2023-0842: versions before 0.5.0 allow prototype pollution. An attacker-controlled document could edit __proto__ because parsed keys weren't validated, and prototype pollution in Node can escalate to denial of service or, in unlucky application code, remote code execution. Fixed in 0.5.0; the project later added regression tests in 0.6.x. If any dependency in your tree still resolves xml2js below 0.5.0, that is a patch worth forcing.

fast-xml-parser — two distinct advisories. CVE-2023-34104: entity names from a DOCTYPE were used to build a replacement regex without sanitization, so a crafted entity name produced catastrophic backtracking and stalled the process indefinitely. Fixed in 4.2.4, with processEntities: false as the documented mitigation for anyone stuck on older versions. CVE-2024-41818: a ReDoS in currency parsing affecting 4.3.5 through 4.4.0, fixed in 4.4.1.

libxmljs — CVE-2024-34391 and CVE-2024-34392: type confusion bugs when parsing crafted XML that references entities, triggered via attrs() and namespaces() respectively. Consequences range from denial of service to potential remote code execution on 32-bit systems with XML_PARSE_HUGE enabled. The first was fixed in 1.0.12; the second had no published fix for some time after disclosure, which pushed several teams off native bindings entirely.

sax and @xmldom/xmldom have had a quieter record, though xmldom's original package was deprecated and the @xmldom/xmldom fork is the only line receiving fixes — a maintenance-status trap that vulnerability scanners catch but npm install xmldom does not.

Safe configuration, concretely

For fast-xml-parser, disable what you don't need and cap what you keep:

import { XMLParser } from "fast-xml-parser";

const parser = new XMLParser({
  processEntities: false,   // no DOCTYPE entity expansion
  ignoreAttributes: false,  // keep attributes if you need them
  allowBooleanAttributes: false,
});
const data = parser.parse(untrustedXml);

For xml2js, pin 0.5.0 or later (0.6.x if you can) and treat the output as untrusted data — validate the shape with a schema library before letting it near business logic:

const xml2js = require("xml2js"); // ensure >= 0.5.0 in the lockfile
const result = await xml2js.parseStringPromise(input, { explicitArray: false });

For libxmljs, the safe posture on untrusted input is restrictive parse options and a hard size limit before the parser ever sees the bytes:

const doc = libxmljs.parseXml(input, { noent: false, nonet: true, dtdload: false });

And regardless of parser: reject documents over a sane byte limit at the HTTP layer, and fuzz your XML endpoints in staging — a DAST scanner that mutates DOCTYPE payloads will find an entity-expansion misconfiguration faster than code review will.

Don't forget the transitive copies

The parser you chose is rarely the only XML parser in your build. SDKs for cloud providers, SOAP clients, RSS libraries, and SVG tooling each embed their own, and an advisory in xml2js or fast-xml-parser can enter your tree four levels deep. This is where a software composition analysis tool earns its keep: an SCA platform such as Safeguard resolves the full dependency graph and flags which lockfile entries actually match CVE-2023-0842 or CVE-2024-41818 ranges, so you're bumping the two packages that matter instead of auditing every mention of "xml" in package-lock.json.

XML parsing also frequently sits next to HTML sanitization in the same input pipeline — if your service accepts rich markup as well, the same "validate, cap, and allowlist" discipline applies, and we've covered that side in our guide to the xss sanitizer package.

A short decision checklist

  1. Untrusted input, structured output → fast-xml-parser 4.4.1+, processEntities: false.
  2. Legacy codebase already on xml2js → upgrade to 0.5.0+/0.6.x, schema-validate the output; migration optional.
  3. Streaming gigabyte feeds → sax, with your own depth and size accounting.
  4. Need XPath, XSD validation, or full standards compliance → libxmljs 1.0.12+, noent and dtdload off, ideally only on trusted or internally generated documents.
  5. Any choice → pin it in the lockfile, subscribe to advisories, and rescan on every dependency update.

FAQ

Which npm XML parser is immune to XXE?

None are "immune," but pure-JavaScript parsers (fast-xml-parser, xml2js, sax) don't implement external entity fetching, so file-read and SSRF-style XXE payloads don't work against them. Their real risks are entity-expansion DoS and parser-logic bugs, which is why version pinning still matters.

Is xml2js still safe to use?

Yes, at version 0.5.0 or later, which fixed the CVE-2023-0842 prototype pollution issue. It's less actively developed than fast-xml-parser, so check that transitive copies in older SDKs are also on patched versions.

Should I use libxmljs for performance?

Only if you control the input or genuinely need libxml2 features like XSD validation. The 2024 type-confusion CVEs showed that native-binding bugs can have memory-safety consequences that pure-JS parser bugs structurally cannot.

How do I find every XML parser in my dependency tree?

Query your lockfile or SBOM for the known parser names (xml2js, fast-xml-parser, sax, xmldom, libxmljs) — or let an SCA tool enumerate them automatically and match versions against published advisory ranges.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.