Back to Blog
critical SEVERITY5 min read

How XML Entity Expansion happens in Node.js and how to fix it

A critical XML External Entity (XXE) vulnerability in `lib/xml2json.js` allowed attackers to trigger exponential memory consumption through nested entity expansion. The fix adds `strictEntities: true` to both SAX parser instances, disabling dangerous entity processing that could crash servers processing untrusted XML.

O
By Orbis AppSec
Published September 7, 2026Reviewed September 7, 2026

Answer Summary

XML External Entity (XXE) injection in Node.js occurs when SAX parsers process XML without disabling entity expansion. In `odata-csdl`'s `lib/xml2json.js`, the `sax.parser()` calls at lines 58 and 631 lacked `strictEntities` configuration, enabling Billion Laughs attacks. CWE-611 (Improper Restriction of XML External Entity Reference). Fix: Add `strictEntities: true` to parser options to block entity expansion entirely.

Vulnerability at a Glance

cweCWE-611
fixAdd `strictEntities: true` to both `sax.parser()` calls in `lib/xml2json.js`
riskDenial of Service via exponential memory consumption from malicious XML entities
languageJavaScript (Node.js)
root causeSAX parser instantiated without `strictEntities: true`, allowing unrestricted entity expansion
vulnerabilityXML External Entity (XXE) Injection

Introduction

In the odata-csdl repository—a library that converts CSDL XML to JSON for OData services—we discovered a critical XML External Entity (XXE) vulnerability lurking in lib/xml2json.js. The file's XML parsing logic relied on the popular sax npm package, but two specific instantiations of sax.parser() at lines 58 and 631 were missing a crucial security flag. With only { xmlns: true } passed as options, these parsers left the door wide open to entity expansion attacks that could crash any server processing untrusted XML input.

This vulnerability is particularly dangerous because odata-csdl sits in the data transformation layer—exactly where malicious payloads often enter enterprise systems. Developers using this library to process external XML feeds, user uploads, or third-party metadata would have had no defense against a classic "Billion Laughs" attack.

The Vulnerability Explained

The xml2json module creates two SAX parsers to handle XML processing: a preParser for initial validation and a main parser for the actual conversion. Both were initialized identically:

// VULNERABLE CODE (before fix)
const preParser = sax.parser(true, { xmlns: true });
// ...
const parser = sax.parser(true, { xmlns: true });

The sax.parser(strict, options) function takes a strictness boolean and an options object. The xmlns: true option enables namespace awareness, but critically, it does not restrict entity expansion. Without strictEntities: true, the parser processes internal entity declarations like these:

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<lolz>&lol4;</lolz>

This "Billion Laughs" payload defines nested entities that expand exponentially—&lol4; becomes 10^9 "lol" strings when fully resolved. In lib/xml2json.js, an attacker exploiting this could:

  1. Submit malicious XML to any endpoint using odata-csdl
  2. Trigger the xml2json() function at line 36
  3. Cause the preParser or parser to consume all available memory
  4. Crash the Node.js process with an out-of-memory error

The real-world impact extends beyond denial of service. In environments where the SAX parser might resolve external entities (depending on Node.js version and sax configuration), this could also enable Server-Side Request Forgery (SSRF) or local file disclosure.

The Fix

The remediation is elegantly simple: add strictEntities: true to both parser configurations. This single flag instructs the sax parser to reject all entity declarations, preventing both exponential expansion and external entity resolution.

Before (vulnerable):

// lib/xml2json.js:58
const preParser = sax.parser(true, { xmlns: true });

// lib/xml2json.js:631
const parser = sax.parser(true, { xmlns: true });

After (fixed):

// lib/xml2json.js:58
const preParser = sax.parser(true, { xmlns: true, strictEntities: true });

// lib/xml2json.js:631
const parser = sax.parser(true, { xmlns: true, strictEntities: true });

This change preserves all legitimate functionality—the xmlns: true option continues to handle XML namespaces correctly, and valid XML documents without entity declarations parse identically. The only affected inputs are those containing <!ENTITY> declarations, which are now correctly rejected as potentially malicious.

The version bump in package.json (0.11.1 → 0.11.2) signals this security patch to downstream consumers, enabling automated vulnerability scanners to flag outdated dependencies.

Prevention & Best Practices

To prevent XXE vulnerabilities in Node.js XML processing:

  1. Always configure strictEntities: true when using the sax parser for untrusted input
  2. Consider sax-stream or sax-wasm for memory-efficient streaming that naturally limits attack surface
  3. Implement input size limits before XML reaches the parser—reject documents exceeding reasonable thresholds
  4. Use JSON alternatives where possible—if you're converting XML to JSON anyway, evaluate whether XML is necessary in your data pipeline
  5. Enable Content Security Policies that restrict where XML can originate from in web-facing applications

For detection, configure Semgrep with rules targeting sax.parser calls missing strictEntities:

rules:
  - id: sax-xxe
    patterns:
      - pattern: sax.parser($STRICT, { $...ARGS })
      - pattern-not: sax.parser($STRICT, { ..., strictEntities: true, ... })
    message: "SAX parser may be vulnerable to XXE without strictEntities: true"

Key Takeaways

  • The sax parser's default behavior permits entity expansion—secure configuration requires explicit opt-out with strictEntities: true
  • Both parsing phases in xml2json.js needed hardening—the preParser at line 58 and main parser at line 631 shared identical vulnerable patterns
  • Namespace awareness (xmlns: true) provides no XXE protection—these are orthogonal concerns that must both be configured
  • Data transformation libraries are high-value attack targetsodata-csdl processes structured data that often originates from external sources
  • Patch propagation matters—the 0.11.2 version bump enables dependency scanners to protect downstream consumers

How Orbis AppSec Detected This

Source: XML input passed to module.exports.xml2json() function in lib/xml2json.js:36

Sink: sax.parser(true, { xmlns: true }) calls at lines 58 and 631 without strictEntities restriction

Missing control: Absence of strictEntities: true option that would disable entity declaration processing

CWE: CWE-611: Improper Restriction of XML External Entity Reference

Fix: Added strictEntities: true to both SAX parser instantiations, completely blocking entity expansion while preserving namespace-aware parsing.

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

The odata-csdl XXE vulnerability demonstrates how a single missing configuration option in widely-used libraries can expose entire application stacks to resource exhaustion attacks. The sax package's default permissiveness toward entity expansion is a footgun that has bitten countless Node.js applications. By adding strictEntities: true to both parser instances, this fix eliminates the attack surface without sacrificing functionality. For developers maintaining XML-processing code, this incident underscores the importance of explicitly securing parser configurations—never assume defaults are safe when handling untrusted input.

References

Frequently Asked Questions

What is XML External Entity (XXE) Injection?

An attack where XML parsers process external or nested entity declarations that can exfiltrate data, execute remote requests, or consume excessive resources.

How do you prevent XML External Entity (XXE) Injection in Node.js?

Configure SAX parsers with `strictEntities: true` to disable entity expansion, or use streaming parsers with explicit entity limits.

What CWE is XML External Entity (XXE) Injection?

CWE-611: Improper Restriction of XML External Entity Reference

Is input validation enough to prevent XXE Injection?

No. Schema validation alone won't stop entity expansion; parser-level configuration with `strictEntities` is required.

Can static analysis detect XXE Injection?

Yes. Tools like Semgrep and CodeQL can flag SAX parser instantiations missing `strictEntities` or `entityResolvers` configurations.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #118

Related Articles

high

How JavaScript Injection via String Interpolation Happens in Go Wails Applications and How to Fix It

A high-severity JavaScript injection vulnerability in `internal/clusterconfigs/input.go` allowed arbitrary code execution through malicious kubeconfig filenames. The `saveClusterConfigFile` function at line 20 constructed JavaScript code by directly interpolating unsanitized filenames into `window.ExecJS()` calls, enabling attackers to break out of string literals and execute arbitrary JavaScript in the Webview context.

high

How Denial of Service via Prototype Pollution happens in Axios and how to fix it

Axios versions prior to 1.15.1 merged untrusted configuration objects without guarding against the `__proto__` key, letting attacker-controlled input pollute `Object.prototype` and crash or destabilize applications. Upgrading axios (and its transitive dependencies `form-data`, `follow-redirects`, `proxy-from-env`) closes this Denial of Service and prototype-pollution attack surface without changing any application code.

critical

How Server-Side Request Forgery happens in Node.js and how to fix it

The order-flow service in a Node.js e-commerce backend built an outbound fetch() URL by directly concatenating a configurable `sendingOrder.url` value with a query string, with no validation of protocol or destination. This allowed order data—including customer and payment-adjacent information—to be silently redirected to an attacker-controlled endpoint simply by changing a config value or environment variable.

high

How Infinite Loop Denial of Service Happens in nanoid and How to Fix It

CVE-2026-67213 is a high-severity infinite loop vulnerability in nanoid's `customAlphabet` function that could cause Denial of Service through CPU exhaustion. The fix upgrades nanoid from 3.3.12 to patched versions 3.3.18 and 5.1.6, eliminating the loop condition that trapped ID generation when processing certain input patterns.

critical

How Message Corruption via Protocol Length Header Abuse Happens in WebSocket Implementations and How to Fix It

CVE-2026-54466 is a critical vulnerability in websocket-driver 0.7.4 that allows attackers to corrupt WebSocket messages by abusing protocol length headers. The fix upgrades the package to version 0.7.5, which implements proper validation of untrusted length header inputs. This vulnerability could allow attackers to modify or inject data into real-time communication channels used by frontend applications.

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.