XXE in Node: The libxmljs2 and Parser Defaults That Are Unsafe (2026)

If you landed here searching for what dtdload defaults to in libxmljs2, the answer is false. noent is false too, and so is every other parser option the library exposes, because libxmljs2 starts its libxml2 flag set at zero and only adds a flag when you pass that option as true.

That is good news and it is also not the end of the story. Node's XML stack has three traps that a "just set the flags" answer walks straight past: one popular option name means the opposite of what it reads like, the flag libxml2 now recommends for this is not reachable from libxmljs2 at all, and the nonet: true line in most hardening guides stopped doing anything.

TL;DR

dtdload and noent both default to false in libxmljs2, so a bare parseXml(input) is not the classic XXE hole. The real risk is a wrapper or a teammate passing noent: true to get entity text expanded, which turns external entity loading back on. nonet: true no longer helps on libxml2 2.15+, and libxmljs2 does not expose libxml2's newer XML_PARSE_NO_XXE flag.

Parser Defaults at a Glance

LibraryUnsafe by default?The setting that mattersSafe value
libxmljs2 (Node)Nonoentomit it, or noent: false
fast-xml-parser (Node)Nonone, no DTD support at allnothing to set
lxml (Python)Yes, with a DOCTYPE presentresolve_entitiesresolve_entities=False
xml.etree.ElementTree (Python)Nonone for XXE, still entity-expansion proneuse defusedxml
DocumentBuilderFactory (Java)Yesdisallow-doctype-decltrue

The Node row is the one people get backwards, so start there.

What dtdload and noent Actually Do

libxmljs2 maps its option names onto libxml2's XML_PARSE_* flags in one function, getParserOptions() in src/xml_document.cc. It initializes the flag set to 0 and then ORs in a flag only when the matching property is present and true. No property, no flag. That single line of logic is why the answer to "what is the libxmljs2 dtdload default" is false, and it holds for all 33 option names the file reads.

Several vulnerability databases claim noent defaults to true in libxmljs. It does not, and it never took a different code path. If you have seen that claim, it is worth rechecking anything else you took from the same page.

noent is the worst-named flag in XML parsing. It reads like "no entities". libxml2's own documentation opens the description with "Despite the confusing name, this option enables substitution of entities." Setting noent: true turns entity handling on, including loading external ones.

Which is exactly how the bug usually arrives. Someone parses a document, finds &customerName; sitting in the tree as an unexpanded entity reference node instead of the text they wanted, searches for a fix, and adds noent: true. The document parses correctly. The file-read primitive arrives with it.

The one-line change that introduces XXE
const libxmljs = require('libxmljs2');

// Safe: no flags set, no external entity loading
const doc = libxmljs.parseXml(xmlString);

// Unsafe: entity substitution on, external entities now load
const doc = libxmljs.parseXml(xmlString, { noent: true });

If you need entity text expanded from documents you don't control, you need a different parser, not a different flag.

Two Pieces of Hardening Advice That Are Now Stale

nonet: true is no longer a network kill switch. Nearly every XXE guide tells you to pass it. libxml2's current docs are blunt about what it does now: after the last built-in network client was removed in 2.15, the option "has no effect except for being passed on to custom resource loaders." It costs nothing to keep, but do not count it as the thing stopping an SSRF.

libxml2's recommended flag is unreachable from libxmljs2. libxml2 added XML_PARSE_NO_XXE specifically to disable external entity loading, and its docs now point untrusted-input handlers at it. libxmljs2's getParserOptions() reads 33 option names and no_xxe is not one of them, so there is no way to set it through the JavaScript API. On this library your ceiling is "don't turn noent on."

If XML is a core part of your product and the input is untrusted, that ceiling is the argument for moving to fast-xml-parser. It has no DTD support at all, which means there is no flag to get wrong and no wrapper that can quietly re-enable one.

What Is XXE?

XML External Entity (XXE) is a vulnerability in applications that parse XML input. The XML spec allows "entities" that reference external resources. If the parser follows those references, an attacker controls what gets read.

The four attack classes:

  • File read: Pull /etc/passwd, .env, or any readable path
  • SSRF: Make server-side HTTP requests to internal IPs, cloud metadata endpoints, or internal APIs
  • Denial of service: Recursive entity expansion ("billion laughs") that consumes all memory
  • Port scan: Probe internal network services by watching which requests succeed or time out

How the Attack Works

File-read XXE payload
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<userInfo>
  <name>&xxe;</name>
</userInfo>

A parser with entity substitution enabled replaces &xxe; with the contents of /etc/passwd before your application code sees the data. The name field in your parsed document now holds the server's user list, and whatever your app does with that field next (log it, echo it back, store it) decides how far the leak travels.

Upload endpoints are the usual delivery route, because an XML, SVG, or DOCX upload reaches a parser without anyone thinking of it as parsing untrusted XML.

The SSRF variant swaps file:// for http://:

SSRF XXE payload targeting AWS metadata
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<userInfo><name>&xxe;</name></userInfo>

Auditing the Rest of Your Stack

libxmljs2 as a transitive dependency

The defaults protect your own call sites. They don't protect the ones you didn't write. If libxmljs2 arrived through another package, that package chose the options, and a wrapper that wants clean text output has every incentive to pass noent: true.

Find every parse call, including your dependencies
# Your code
grep -rn "parseXml\|parseXmlString" --include="*.js" --include="*.ts" .

# Your dependencies
grep -rn "noent" node_modules --include="*.js" | grep -v "noent: *false"

The second command is the one that finds surprises. Anything it prints is a package that turns entity substitution on for you.

Writing the flags out explicitly is still worth doing, not because the defaults are wrong but because it documents the intent for whoever edits the file next:

libxmljs2: explicit is better than default
const libxmljs = require('libxmljs2');

const doc = libxmljs.parseXml(xmlString, {
  noent: false,    // keep entity substitution off (this is already the default)
  dtdload: false,  // don't load external DTDs
  dtdvalid: false, // don't validate against DTD
});

fast-xml-parser avoids the question entirely:

fast-xml-parser: safe by default
const { XMLParser } = require('fast-xml-parser');

const parser = new XMLParser({
  // No DTD or external entity support. XXE-safe out of the box.
});
const result = parser.parse(xmlString);

lxml (Python)

Python's lxml is the other common source of XXE bugs. Its etree.fromstring and etree.parse functions resolve external entities by default when the document includes a DOCTYPE.

lxml: vulnerable pattern
from lxml import etree

# UNSAFE: resolves external entities
tree = etree.fromstring(xml_bytes)
lxml: safe pattern with defusedxml
import defusedxml.lxml as safe_lxml

# defusedxml patches lxml to block DTD loading, external entities, and entity expansion
tree = safe_lxml.fromstring(xml_bytes)

You can also configure lxml's parser directly:

lxml: manual safe parser
from lxml import etree

parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False,
)
tree = etree.fromstring(xml_bytes, parser)

Java (DocumentBuilderFactory)

Java's default XML parser is also unsafe. This is OWASP's recommended hardening:

Java: safe DocumentBuilderFactory
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

Is Your App at Risk?

Most apps built with Cursor, Bolt, Lovable, or Replit use JSON APIs and are not vulnerable. XXE only matters if your server parses XML.

You are at risk if your app:

  • Accepts XML file uploads (exports, imports, configuration)
  • Integrates with SOAP APIs (older enterprise services)
  • Parses SVG images server-side (thumbnailing, conversion)
  • Processes Office documents (DOCX, XLSX, PPTX are ZIP+XML)
  • Uses XML-based configuration files

If you use a PDF generation library that internally parses HTML as XML, or an image processing library that handles SVG, check the library's XXE posture even if you never write XML yourself.

How to Find XXE in Your App

Three places to check:

1. Grep for XML parser imports:

# Node.js
grep -r "require('libxmljs2')\|require('xml2js')\|require('sax')" .

# Python
grep -r "from lxml\|import xml.etree\|import libxml2" .

2. Look for multipart/form-data endpoints that accept XML or SVG content types.

3. Check any library that processes uploaded documents (DOCX parsers, spreadsheet parsers, diagram tools). Search the library's GitHub for "XXE" or "external entity" in open issues.

The Fastest Fix

If you only parse XML in one place and don't need DTD support (most apps don't), the fastest fix is to reject any document that contains a DOCTYPE declaration entirely:

Reject DOCTYPE in Node.js
function safeParseXml(xmlString) {
  if (xmlString.includes('<!DOCTYPE') || xmlString.includes('<!ENTITY')) {
    throw new Error('XML DOCTYPE/ENTITY declarations are not allowed');
  }
  return libxmljs.parseXml(xmlString, { noent: false, dtdload: false });
}

This is a belt-and-suspenders approach: reject suspicious input before the parser even runs.

What is the default value of dtdload in libxmljs2?

false. libxmljs2's getParserOptions() builds its libxml2 flag set starting from 0 and only ORs in a flag when the matching property is present and set to true. Pass no options and you get no flags, so dtdload is off. The same holds for all 33 option names the function reads.

Does noent default to true in libxmljs2?

No. Several vulnerability databases say it does, and the claim keeps getting copied forward. noent goes through the same one-line mechanism as every other option, so it is false unless you pass noent: true yourself. There is no alternate binding path where it flips.

What is an XXE attack?

XXE (XML External Entity) is an attack where a malicious XML payload tricks the parser into reading local files, making internal HTTP requests, or crashing the server via recursive entities. It targets the XML spec's "external entity" feature, which most apps have no reason to use.

Does setting nonet: true protect me from XXE?

Not the way it used to. libxml2 removed its last built-in network client in 2.15, and its documentation now says XML_PARSE_NONET "has no effect except for being passed on to custom resource loaders." Keep it if you like, but the thing actually preventing an outbound fetch is leaving noent and dtdload off, not this flag.

Can JSON APIs have XXE vulnerabilities?

No. XXE is specific to XML parsing. JSON parsers don't support external entities. If your API only accepts JSON, you're not vulnerable to XXE.

Check Your XML Handling

Our scanner tests for XXE vulnerabilities in your file upload endpoints and flags libraries with unsafe defaults.

Vulnerability Guides

XXE in Node: The libxmljs2 and Parser Defaults That Are Unsafe (2026)