Description
An XML parser that expands entities may turn a small document with deeply nested DTD entities into an enormous string, exhausting memory or CPU. When using parsers such as node-expat, check entity behavior and the resource limits in the deployed version. Insufficiently limited expansion can expose the application to Billion Laughs-style denial of service.
Potential impact
- Unresponsive processes or service outages
- Out-of-memory termination
- Excessive CPU use during expansion and parsing
- Crashes, restart loops, or cascading availability problems
- Unnecessary autoscaling and resource costs
Remediation
- Disable DTD/entity expansion where possible and reject DOCTYPE declarations.
- Use a parser with suitable expansion controls, or a streaming parser such as sax that does not expand custom DTD entities.
- Limit request size, parsing time, node count, depth, and text length.
- Validate against an allowed schema and reject DTD declarations such as
<!DOCTYPE. - Apply request-size limits at the proxy or web server and control application concurrency and queues.
Examples
Before
javascript
const express = require("express");
const expat = require("node-expat");
const app = express();
app.use(express.text({ type: "application/xml" }));
app.post("/parse", (req, res) => {
const xml = req.body; // Untrusted input
const p = new expat.Parser();
p.on("startElement", () => {});
p.on("text", () => {});
// Entity expansion can enable XML bombs without adequate limits.
p.write(xml);
res.send("parsed");
});
module.exports = app;
After
javascript
const express = require("express");
const sax = require("sax");
const app = express();
// Limit the body size.
app.use(express.text({ type: "application/xml", limit: "200kb" }));
app.post("/safe-parse", (req, res) => {
const xml = req.body || "";
// Reject DTD declarations such as DOCTYPE/ENTITY.
const suspicious = /<!DOCTYPE|<!ENTITY/i.test(xml);
if (suspicious) {
return res.status(400).send("DTD/ENTITY not allowed");
}
// sax does not expand custom DTD entities.
const parser = sax.parser(true); // strict mode
parser.onopentag = () => {};
parser.ontext = () => {};
parser.onerror = (e) => {
throw e;
};
try {
parser.write(xml).close();
} catch {
return res.status(400).send("invalid xml");
}
return res.send("ok");
});
module.exports = app;
Explanation:
- Before: Untrusted XML reaches an entity-expanding parser. If its version and resource limits do not constrain expansion, a small input can exhaust memory or CPU.
- After: Reject DTD/ENTITY declarations, use sax without custom entity expansion, and limit the request body. On a parse error, stop parsing and send one error response.