XML external entity expansion

File and network access risks from XML entity expansion

Description

The noent: true option in libxmljs or libxmljs2 expands entity references into their values. If untrusted XML declares an external entity and the parser can load it, parsing may access local files or cause server-side request forgery (SSRF). Actual file and network access, and expansion limits, depend on the bundled libxml2 version, build and loader configuration.

Potential impact

  • External entities may read local files, such as /etc/passwd, and disclose their contents in responses or logs.
  • The parser may access internal or metadata services, such as http://169.254.169.254, and expose sensitive information.
  • Excessive or recursive entity expansion may exhaust CPU or memory and disrupt the service.

Remediation

  • Keep noent disabled or set it to false for untrusted XML.
  • Reject DTDs when they are not needed. Also check additional options and custom loaders that read external DTDs or entities.
  • The XML parser handles predefined references such as & and <. Do not decode parsed text again with an HTML entity decoder.
  • Limit input size and accepted formats, handle parsing errors, and check expansion, memory and execution-time limits.
  • Minimize network and file permissions. noent: false alone does not block every external-resource access path.

Examples

Before

javascript
const express = require('express');
const libxml = require('libxmljs');
const app = express();
app.use(express.text({ type: '*/*' }));

// Parse externally supplied XML with noent: true
app.post('/api/parse-xml', (req, res) => {
  const xmlPayload = String(req.body || '');
  // noent: true may expand external entities, enabling file access, SSRF or DoS
  const doc = libxml.parseXmlString(xmlPayload, { noent: true });
  res.json({ root: doc.root().name() });
});

app.listen(3000);

After

javascript
const express = require('express');
const libxml = require('libxmljs');
const app = express();
app.use(express.text({ type: '*/*' }));

// Disable noent and reject DTDs
app.post('/api/parse-xml', (req, res) => {
  const xmlPayload = String(req.body || '');

  // Reject DTD declarations, including external entity declarations
  if (/<!DOCTYPE/i.test(xmlPayload)) {
    return res.status(400).send('DTD is not allowed');
  }

  // Keep noent disabled explicitly
  const doc = libxml.parseXmlString(xmlPayload, { noent: false });

  // Do not decode the parser output a second time
  const textContent = doc.root().text();
  res.json({ root: doc.root().name(), text: textContent.slice(0, 200) });
});

app.listen(3000);

The first example enables entity expansion. The second rejects DTD declarations in the string decoded by Express and disables noent. Do not assume this simple check protects every XML input path. Verify the parser's external-loading settings and add error responses for malformed XML. The example uses the parsed text directly without a second HTML entity decoding step.

References