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:
- Submit malicious XML to any endpoint using
odata-csdl - Trigger the
xml2json()function at line 36 - Cause the
preParserorparserto consume all available memory - 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:
- Always configure
strictEntities: truewhen using thesaxparser for untrusted input - Consider
sax-streamorsax-wasmfor memory-efficient streaming that naturally limits attack surface - Implement input size limits before XML reaches the parser—reject documents exceeding reasonable thresholds
- Use JSON alternatives where possible—if you're converting XML to JSON anyway, evaluate whether XML is necessary in your data pipeline
- 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
saxparser's default behavior permits entity expansion—secure configuration requires explicit opt-out withstrictEntities: true - Both parsing phases in
xml2json.jsneeded hardening—thepreParserat line 58 and mainparserat 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 targets—
odata-csdlprocesses 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.