Skip to content
🔵Info0.0

Node.js XXE Prevention

Secure XML parsing configuration for Node.js applications using libxmljs2, fast-xml-parser, xml2js, and other XML libraries.

CWE-611: Improper Restriction of XML External Entity ReferenceOWASP Top 10:2021 - A05: Security Misconfiguration

Overview

Node.js has multiple XML parsing libraries with varying security characteristics. Many popular libraries are vulnerable to XXE by default, making proper configuration critical.

Common Node.js XML Libraries:

  • libxmljs2 - C bindings to libxml2 (VULNERABLE by default)
  • fast-xml-parser - Pure JavaScript (SAFE by default - doesn't support external entities)
  • xml2js - Built on sax-js (SAFE by default)
  • xmldom - Pure JavaScript DOM (SAFE by default)
  • sax-js - SAX parser (SAFE by default)

Security Status: ✅ SAFE: fast-xml-parser, xml2js, xmldom, sax-js ❌ VULNERABLE: libxmljs2 (if not configured)

Best Practice:

  • Use fast-xml-parser or xml2js for most applications
  • If using libxmljs2, explicitly disable entity loading
  • Never trust default configurations
  • Validate XML structure before parsing

Vulnerable libxmljs2 (Default)

JavaScriptvulnerable-libxmljs.js⚠️ Vulnerable
1const libxmljs = require('libxmljs2');
2
3// VULNERABLE: Default configuration allows XXE
4function parseXMLVulnerable(xmlData) {
5    // Default parser settings allow external entities
6    const xmlDoc = libxmljs.parseXml(xmlData);
7    
8    // External entities will be loaded and expanded
9    // File disclosure and SSRF possible
10    return xmlDoc;
11}
12
13// Example exploitation
14const xxePayload = `<?xml version="1.0"?>
15<!DOCTYPE root [
16  <!ENTITY xxe SYSTEM "file:///etc/passwd">
17]>
18<root>
19  <data>&xxe;</data>
20</root>`;
21
22const doc = parseXMLVulnerable(xxePayload);
23const dataNode = doc.get('//data');
24console.log(dataNode.text());  // Prints /etc/passwd contents!

Secure libxmljs2 Configuration

JavaScriptsecure-libxmljs.jsâś“ Secure
1const libxmljs = require('libxmljs2');
2
3// SECURE: Disable external entity loading
4function parseXMLSecure(xmlData) {
5    const xmlDoc = libxmljs.parseXml(xmlData, {
6        noent: false,    // Do NOT substitute entities (critical!)
7        nonet: true,     // Disable network access
8        dtdload: false,  // Don't load external DTD
9        dtdvalid: false  // Don't validate with DTD
10    });
11    
12    return xmlDoc;
13}
14
15// Alternative: Reject XML with DOCTYPE
16function parseXMLStrictSecure(xmlData) {
17    // Input validation: reject DOCTYPE declarations
18    if (xmlData.includes('<!DOCTYPE') || xmlData.includes('<!ENTITY')) {
19        throw new Error('DOCTYPE and ENTITY declarations not allowed');
20    }
21    
22    const xmlDoc = libxmljs.parseXml(xmlData, {
23        noent: false,
24        nonet: true,
25        dtdload: false,
26        dtdvalid: false
27    });
28    
29    return xmlDoc;
30}
31
32// Example usage
33const xxePayload = `<?xml version="1.0"?>
34<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
35<root><data>&xxe;</data></root>`;
36
37try {
38    const doc = parseXMLSecure(xxePayload);
39    // Entity NOT expanded - safe
40} catch (err) {
41    console.error('Blocked malicious XML:', err.message);
42}

fast-xml-parser (Recommended - Safe by Default)

JavaScriptfast-xml-parser-safe.jsâś“ Secure
1const { XMLParser, XMLValidator } = require('fast-xml-parser');
2
3// SAFE: fast-xml-parser doesn't support external entities
4// This is the recommended library for Node.js
5
6function parseXMLSafe(xmlData) {
7    // Validate XML first
8    const validationResult = XMLValidator.validate(xmlData);
9    if (validationResult !== true) {
10        throw new Error(`Invalid XML: ${validationResult.err.msg}`);
11    }
12    
13    // Configure parser
14    const options = {
15        ignoreAttributes: false,
16        parseAttributeValue: true,
17        parseTagValue: true,
18        trimValues: true,
19        // External entities not supported - inherently safe
20    };
21    
22    const parser = new XMLParser(options);
23    const jsonObj = parser.parse(xmlData);
24    
25    return jsonObj;
26}
27
28// Example usage
29const validXML = `
30<root>
31  <data id="1">Safe content</data>
32</root>`;
33
34const xxePayload = `<?xml version="1.0"?>
35<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
36<root><data>&xxe;</data></root>`;
37
38try {
39    const result1 = parseXMLSafe(validXML);
40    console.log(result1);  // Successfully parsed
41    
42    const result2 = parseXMLSafe(xxePayload);
43    // DOCTYPE and entities are simply ignored (safe behavior)
44    console.log(result2);
45} catch (err) {
46    console.error('Parse error:', err.message);
47}
48
49// Install: npm install fast-xml-parser

xml2js (Safe by Default)

JavaScriptxml2js-safe.jsâś“ Secure
1const xml2js = require('xml2js');
2
3// SAFE: xml2js uses sax-js which doesn't expand external entities
4
5function parseXMLWithXml2js(xmlData) {
6    return new Promise((resolve, reject) => {
7        const parser = new xml2js.Parser({
8            explicitArray: false,
9            ignoreAttrs: false,
10            mergeAttrs: false,
11            // No external entity support - inherently safe
12        });
13        
14        parser.parseString(xmlData, (err, result) => {
15            if (err) {
16                reject(err);
17            } else {
18                resolve(result);
19            }
20        });
21    });
22}
23
24// Example usage
25const validXML = `
26<root>
27  <data>Safe content</data>
28</root>`;
29
30const xxePayload = `<?xml version="1.0"?>
31<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
32<root><data>&xxe;</data></root>`;
33
34async function main() {
35    try {
36        const result1 = await parseXMLWithXml2js(validXML);
37        console.log(result1);  // Successfully parsed
38        
39        const result2 = await parseXMLWithXml2js(xxePayload);
40        // Entities not expanded - safe
41        console.log(result2);
42    } catch (err) {
43        console.error('Parse error:', err);
44    }
45}
46
47main();
48
49// Install: npm install xml2js

Express.js Integration

JavaScriptexpress-app.jsâś“ Secure
1const express = require('express');
2const { XMLParser, XMLValidator } = require('fast-xml-parser');
3const app = express();
4
5// Middleware for XML parsing with security
6app.use(express.text({ type: 'application/xml', limit: '1mb' }));
7
8app.post('/api/xml', (req, res) => {
9    try {
10        const xmlData = req.body;
11        
12        // Size validation
13        if (xmlData.length > 1048576) {  // 1MB
14            return res.status(413).json({ error: 'XML too large' });
15        }
16        
17        // Additional validation: reject DOCTYPE
18        if (xmlData.includes('<!DOCTYPE') || xmlData.includes('<!ENTITY')) {
19            return res.status(400).json({ 
20                error: 'DOCTYPE and ENTITY declarations not allowed' 
21            });
22        }
23        
24        // Validate XML structure
25        const validationResult = XMLValidator.validate(xmlData);
26        if (validationResult !== true) {
27            return res.status(400).json({ 
28                error: 'Invalid XML format' 
29            });
30        }
31        
32        // Parse safely
33        const parser = new XMLParser({ 
34            ignoreAttributes: false 
35        });
36        const result = parser.parse(xmlData);
37        
38        // Process result
39        res.json({ success: true, data: result });
40        
41    } catch (err) {
42        console.error('XML processing error:', err);
43        res.status(400).json({ error: 'Processing failed' });
44    }
45});
46
47app.listen(3000, () => {
48    console.log('Server running on port 3000');
49});

Fastify Integration

JavaScriptfastify-app.jsâś“ Secure
1const fastify = require('fastify')({ logger: true });
2const { XMLParser, XMLValidator } = require('fast-xml-parser');
3
4// Content type parser for XML
5fastify.addContentTypeParser(
6    'application/xml',
7    { parseAs: 'string' },
8    (req, body, done) => {
9        done(null, body);
10    }
11);
12
13fastify.post('/api/xml', async (request, reply) => {
14    try {
15        const xmlData = request.body;
16        
17        // Size validation
18        if (xmlData.length > 1048576) {  // 1MB
19            return reply.code(413).send({ 
20                error: 'XML too large' 
21            });
22        }
23        
24        // Security validation
25        if (xmlData.includes('<!DOCTYPE') || xmlData.includes('<!ENTITY')) {
26            return reply.code(400).send({ 
27                error: 'DOCTYPE/ENTITY not allowed' 
28            });
29        }
30        
31        // Validate XML
32        const validationResult = XMLValidator.validate(xmlData);
33        if (validationResult !== true) {
34            return reply.code(400).send({ 
35                error: 'Invalid XML' 
36            });
37        }
38        
39        // Parse safely
40        const parser = new XMLParser();
41        const result = parser.parse(xmlData);
42        
43        return { success: true, data: result };
44        
45    } catch (err) {
46        request.log.error(err);
47        return reply.code(400).send({ 
48            error: 'Processing failed' 
49        });
50    }
51});
52
53const start = async () => {
54    try {
55        await fastify.listen({ port: 3000 });
56    } catch (err) {
57        fastify.log.error(err);
58        process.exit(1);
59    }
60};
61
62start();

Security Testing with Jest

JavaScriptxxe-prevention.test.jsâś“ Secure
1const { XMLParser, XMLValidator } = require('fast-xml-parser');
2const libxmljs = require('libxmljs2');
3
4describe('XXE Prevention Tests', () => {
5    
6    test('fast-xml-parser blocks XXE (safe by default)', () => {
7        const xxePayload = `<?xml version="1.0"?>
8<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
9<root><data>&xxe;</data></root>`;
10        
11        const parser = new XMLParser();
12        const result = parser.parse(xxePayload);
13        
14        // Entity not expanded - safe
15        expect(result.root.data).not.toContain('root:x:0:0');
16    });
17    
18    test('libxmljs with secure config blocks XXE', () => {
19        const xxePayload = `<?xml version="1.0"?>
20<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
21<root><data>&xxe;</data></root>`;
22        
23        // Secure configuration
24        const doc = libxmljs.parseXml(xxePayload, {
25            noent: false,  // Don't expand entities
26            nonet: true,   // Block network
27            dtdload: false
28        });
29        
30        const dataNode = doc.get('//data');
31        // Entity not expanded
32        expect(dataNode.text()).not.toContain('root:x:0:0');
33    });
34    
35    test('Reject DOCTYPE in input validation', () => {
36        const xxePayload = `<?xml version="1.0"?>
37<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
38<root><data>&xxe;</data></root>`;
39        
40        expect(xxePayload).toContain('<!DOCTYPE');
41        // Application should reject this before parsing
42    });
43    
44    test('Valid XML is accepted', () => {
45        const validXML = '<root><data>test</data></root>';
46        
47        const parser = new XMLParser();
48        const result = parser.parse(validXML);
49        
50        expect(result.root.data).toBe('test');
51    });
52    
53    test('Billion laughs attack blocked', () => {
54        const billionLaughs = `<?xml version="1.0"?>
55<!DOCTYPE lolz [
56  <!ENTITY lol "lol">
57  <!ENTITY lol2 "&lol;&lol;&lol;&lol;">
58]>
59<lolz>&lol2;</lolz>`;
60        
61        const parser = new XMLParser();
62        const result = parser.parse(billionLaughs);
63        
64        // Entities not expanded - safe
65        expect(result.lolz).not.toBe('lol'.repeat(16));
66    });
67});

Node.js XXE Prevention Checklist

âś… Library Selection:

  • Prefer fast-xml-parser (safe by default, actively maintained)
  • xml2js is also safe (uses sax-js internally)
  • Avoid libxmljs/libxmljs2 unless absolutely necessary
  • If using libxmljs2, configure with noent:false, nonet:true

âś… libxmljs2 Configuration (if required):

  • Set noent: false (critical - disables entity expansion)
  • Set nonet: true (blocks network access)
  • Set dtdload: false (don't load external DTDs)
  • Set dtdvalid: false (don't validate with DTDs)

âś… Input Validation:

  • Reject XML containing <!DOCTYPE declarations
  • Reject XML containing <!ENTITY declarations
  • Implement size limits (prevent DoS)
  • Validate Content-Type header (application/xml)
  • Use XMLValidator before parsing

âś… Framework Integration:

  • Configure XML body parser middleware securely
  • Set appropriate size limits (1MB recommended)
  • Implement proper error handling
  • Don't leak XML parse errors to users
  • Log XXE attempts for monitoring

âś… Testing:

  • Unit tests with XXE payloads (should be blocked/safe)
  • Tests with billion laughs (should be blocked/safe)
  • Tests with external DTD (should be blocked/safe)
  • Integration tests with security scanner
  • Regular penetration testing

âś… Dependencies:

  • Keep XML libraries up to date (npm update)
  • Monitor security advisories (npm audit)
  • Use Snyk or similar for vulnerability scanning
  • Review transitive dependencies

âś… Defense in Depth:

  • Network egress filtering (block outbound from app servers)
  • File system permissions (limit file access)
  • Container security (isolate XML processing)
  • WAF rules for XXE patterns
  • Monitoring and alerting