agentggagentgg
Back to all findings
CRITICALconfirmedfile-upload-validationxxe-on-upload19cbaed44919

XML upload parsed with external entities enabled (XXE)

handleXmlUpload feeds uploaded XML directly into libxmljs2 with `noent: true`, enabling external entity expansion on attacker-supplied documents with no size or content validation.

Fileroutes/fileUpload.ts
Lines76103
Confidence
90%
File statusvalidated
Details
const xmlDoc = vm.runInContext('libxml.parseXml(data, { noblanks: true, noent: true, nocdata: true })', sandbox, { timeout: 2000 })
const xmlString = xmlDoc.toString(false)
challengeUtils.solveIf(challenges.xxeFileDisclosureChallenge, () => { return (utils.matchesEtcPasswdFile(xmlString) || utils.matchesSystemIniFile(xmlString)) })

The parser is invoked with noent: true, which causes libxml2 to substitute external entity references. The buffer is not size-limited (see checkUploadSize finding) and the filename / MIME is not validated (see checkFileType finding). The presence of xxeFileDisclosureChallenge and xxeDosChallenge confirms file disclosure and DoS via billion-laughs / quadratic-blowup are reachable. The vm timeout of 2000 ms only mitigates the simplest DoS pattern; file-disclosure XXE has no such bound.

Proof of concept

POST a payload such as:

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

as a file named pwn.xml to the upload endpoint. The server resolves the entity and reflects /etc/passwd content back in the error response (utils.trunc(xmlString, 400)).

Impact

Arbitrary local file disclosure (e.g. /etc/passwd, application secrets, source code) and DoS via entity expansion for any caller who can hit the upload endpoint. No size/type validation gates this path.

Validation
confirmed

The code in handleXmlUpload calls libxml.parseXml(data, { noblanks: true, noent: true, nocdata: true }) with noent: true, which expands external entities. The parsed xmlString is then reflected back in the error response via utils.trunc(xmlString, 400), enabling file disclosure via the classic <!ENTITY xxe SYSTEM "file:///etc/passwd"> payload. The 2000ms vm timeout only limits CPU-bound expansion attacks, not file disclosure. Per scope rules, Juice Shop's training nature is irrelevant — this is a confirmed XXE.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H
Base score: 9.1 · CRITICAL

The sink is an HTTP file-upload endpoint (handleXmlUpload) reachable over the network; no authentication or role check is visible in the shown code path, so PR=N and AV=N. Exploitation is a single POST of a crafted .xml file with no preconditions beyond the challenge flag being enabled (AC=L, UI=N). libxml.parseXml(data, { noent: true, ... }) resolves SYSTEM entities and the resolved content is echoed back via utils.trunc(xmlString, 400), giving the attacker arbitrary read of any file the Node process can access (C=H), plus quadratic-blowup/billion-laughs DoS that the 2000 ms vm timeout only partially mitigates (A=H); there is no write primitive here (I=N) and the disclosure stays within the application's own security authority (S=U).

References