Walkthrough Node.js / TypeScript Known-vulnerable benchmark

Inside a scan: what $3.12 finds in OWASP Juice Shop

TargetOWASP Juice Shop
Scopeserver-side routes · 61 files, 4,235 lines
Wall time~22 minutes
Cost$3.12 (169K tokens)
Result8 confirmed · 9 of ~11 classes

Most of our other write-ups scan ordinary code where nobody knows in advance what is wrong. This one is different on purpose. OWASP Juice Shop is the reference deliberately-insecure web application, and its vulnerabilities are documented: a published list of planted bugs across injection, broken access control, XSS, broken authentication and more. That makes it the one target where you can do something you can't do on a private repo — score the scanner against ground truth. If we claim a SQL injection in the login route, you can open the file and the official challenge list and check.

We pointed redmirror scan at the server-side API — the routes/ directory, 61 request handlers where most of the planted server-side bugs live. Twenty-two minutes and $3.12 later, here is what came back.

Findings at a glance

CategoryCount
Confirmed security defects8
Needs further analysis (candidates)100
Investigated (not a security issue)3

A candidate is called Confirmed only when the deciding code path was read and the defect follows from the code as written. Everything else is shown, not hidden: candidates that need context we couldn't see from the file stay in a “needs analysis” list, and candidates a careful reading proves safe are investigated and ruled out with the reason. Severity is never inflated. On a target this dense, the eight confirmed are the high-confidence core; the wider hundred candidates are where the recall lives, each labelled with what it would take to confirm it.

Confirmed defects

The login SQL injection

High   routes/login.ts:34  ·  CWE-89

The signature bug of Juice Shop, and the scan lands on it. The email field is interpolated straight into a raw SQL string:

// routes/login.ts
models.sequelize.query(
  `SELECT * FROM Users WHERE email = '${req.body.email || ''}'
    AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`,
  { model: UserModel, plain: true })

Impact. The password half is hashed, but the email value is unprotected. An attacker submitting ' OR '1'='1'-- as the email breaks out of the string and authenticates as the first user in the table — the classic full authentication bypass.

Zip-slip: arbitrary file write on upload

High   routes/fileUpload.ts:31  ·  CWE-22

The interesting confirmations aren't the obvious ones — they're the guards that look right and aren't. Uploaded-archive entries are extracted with a confinement check that uses the wrong string test:

const absolutePath = path.resolve('uploads/complaints/' + fileName)
if (absolutePath.includes(path.resolve('.'))) {          // includes(), not startsWith()
  await pipeline(entry.stream(),
    fs.createWriteStream('uploads/complaints/' + fileName))
}

Root cause. The check asks whether the application root appears anywhere in the resolved path, not whether the file stays under the upload directory. A zip entry named ../../ftp/legal.md resolves inside the app root, passes the check, and is written outside uploads/complaints/. Fix: startsWith(path.resolve('uploads/complaints') + path.sep).

The CAPTCHA that always passes

High   routes/imageCaptcha.ts:52  ·  CWE-840

A one-character logic bug — || where the author meant &&:

if (!captchas[0] || req.body.answer === captchas[0].answer) {
  next()                          // proceeds when NO captcha exists at all
}

Impact. A request that never requested a CAPTCHA has no stored record, so !captchas[0] is true and the check is skipped entirely. The gate meant to stop automation waves it straight through.

And four more

Scored against the known bug list

This is the part you can only do on a benchmark. Of Juice Shop's ~11 vulnerability categories that a source review of the API can reach (the rest — leaked-file hunts, out-of-date dependencies, obscure UI tricks — are not code defects a route review would find), the scan surfaced findings in nine:

CategoryReached by the scan?Example
InjectionConfirmedlogin SQL injection; NoSQL in reviews/orders
Broken Access ControlConfirmedforged continue code; unauthenticated delivery change
Improper Input ValidationConfirmedzip-slip file write; YAML path traversal
Insecure DeserializationConfirmedyaml.load on user input
Broken Anti-AutomationConfirmedthe CAPTCHA bypass
Unvalidated RedirectsCandidateopen redirect in the redirect route
Broken AuthenticationCandidateweak JWT verification
Cryptographic IssuesCandidatethe same JWT weakness
Cross-Site ScriptingCandidateunencoded values reaching a response
XML External EntitiesMissedthe XXE lives outside the scanned routes

Nine of ten reached, five of them as confirmed defects. The one miss is honest: Juice Shop's XXE lives in an upload handler outside the routes/ directory we scoped, so it was never in scope — scan lib/ too and it comes into range. We'd rather tell you that than quietly drop the denominator.

The hundred candidates

Below the eight confirmations sits a longer list: an external value reaching a database query, a file path, a redirect or a response without a visible sanitizer on the way. Many are the other half of the injection and access-control challenges. They aren't promoted to “confirmed” because confirming each one needs a fact the single file doesn't show — whether that input is actually attacker-reachable, or neutralised in a helper defined elsewhere. So they're presented as leads with exactly that question attached, not as a wall of red. It's the difference between “here are eight things that are wrong” and “here are a hundred things to be scared of.”

The numbers

Three dollars, one directory, and the scanner walked onto most of a benchmark that exists specifically to be hard to fully catch. A small package runs a few cents; you always see the estimate before you spend anything. See the measured cost table for more real runs, or the vulnerabilities we've reported in real OSS.

Run it on your own repo

Point RedMirror at your code and get the same thing back: confirmed defects with a reproducible path, an honest “investigated and ruled out” list, and a bill metered to the token. Check the cost first with redmirror estimate: local, free, no tokens spent.

Start an audit Read the docs