Denial of service through internal XML entity expansion

XML entity expansion (Billion Laughs)

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.

References