agentggagentgg
Back to all findings
CRITICALconfirmedfile-upload-validationmissing-extension-allowlist013198033297

checkFileType middleware does not actually enforce an extension allowlist

checkFileType inspects the filename extension only to update a CTF challenge state and unconditionally calls next(); any extension is accepted by the upload pipeline.

Fileroutes/fileUpload.ts
Lines6874
Confidence
90%
File statusvalidated
Details
function checkFileType ({ file }: Request, res: Response, next: NextFunction) {
  const fileType = file?.originalname.substr(file.originalname.lastIndexOf('.') + 1).toLowerCase()
  challengeUtils.solveIf(challenges.uploadTypeChallenge, () => {
    return !(fileType === 'pdf' || fileType === 'xml' || fileType === 'zip' || fileType === 'yml' || fileType === 'yaml')
  })
  next()
}

The allowlist string comparison is wrapped in solveIf — it only flips a challenge flag, the request continues regardless. There is also no MIME / magic-number verification (no file-type, no getimagesize equivalent), and no multer fileFilter is visible at the route level here. The downstream handlers only branch on .zip/.xml/.yml/.yaml suffix, so any other extension (e.g. shell.php, xss.html, evil.svg) flows out the bottom of the middleware chain to whatever route handler is registered.

Proof of concept

POST /file-upload with file=@shell.php. checkFileType records the challenge but does not block; the file proceeds through the pipeline with its attacker-controlled extension intact.

Impact

Any user (no auth check shown) can upload files of arbitrary type. Combined with the zip handler writing to uploads/complaints/ using attacker-supplied entry names, this enables web-shell / XSS / parser-confusion attacks depending on how uploads/complaints/ is served.

Validation
confirmed

The checkFileType function uses challengeUtils.solveIf only to flip a CTF challenge flag when the extension is NOT one of pdf/xml/zip/yml/yaml, then unconditionally calls next(). There is no return res.status(...) or short-circuit — control flow continues for any extension. No multer fileFilter or MIME/magic-byte verification is present, and downstream handlers (handleZipFileUpload, handleXmlUpload, handleYamlUpload) only branch on suffix, so non-listed extensions pass through. Per scope ("treat as production"), this is a confirmed missing-extension-allowlist bug.

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

The checkFileType middleware does the extension comparison only inside solveIf and then unconditionally calls next(), so any extension reaches downstream handlers; the route shown has no auth guard, making this remotely exploitable by an unauthenticated attacker over HTTP (AV:N, PR:N, UI:N, AC:L). Combined with handleZipFileUpload, an attacker-controlled zip entry path is written under uploads/complaints/ (the absolutePath.includes(path.resolve('.')) check still permits arbitrary names within the project root), yielding arbitrary file write — and per the finding, files with extensions like .php/.html/.svg flow through with their attacker-controlled extension, enabling web-shell/stored-XSS depending on how uploads/complaints/ is served, hence C:H and I:H. Availability is rated L because file overwrites in the upload directory can degrade service but no clear total-DoS sink is visible here; Scope stays U since the demonstrated impact lives inside the same Juice Shop application/security authority.

References