🔵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-parserxml2js (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 xml2jsExpress.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