RedMirror Reflection is now the default way to use RedMirror — one offline binary that gives your own coding agent the power to find real bugs and prove them, right on your machine. Get started →
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

A RedMirror scan of OWASP Juice Shop's 61 server-side route handlers took about 22 minutes and cost $3.12. Against the benchmark's public list of planted bugs, it surfaced findings in 9 of the roughly 11 code-reachable categories and confirmed eight defects, including the login SQL injection, a zip-slip file write, and a CAPTCHA bypass.

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.

What did the scan find 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.

What are the 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

How did the scan score 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.

What are 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.”

What do the numbers add up to?

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.

Frequently asked questions

What is OWASP Juice Shop, and why scan it?

OWASP Juice Shop is the reference deliberately-insecure web application, with a published list of planted vulnerabilities across injection, broken access control, XSS, broken authentication and more. Because its bugs are documented, it is the one target where you can score a scanner against ground truth by opening the file and the official challenge list.

How much did the RedMirror scan cost, and how long did it take?

The scan covered Juice Shop's 61 server-side route handlers, 4,235 lines, in the routes/ directory. It took about 22 minutes and cost $3.12, using 168,779 tokens across 133 model calls.

How many bugs did the scan confirm?

Eight defects were confirmed, alongside 100 candidates that need further analysis and 3 that were investigated and ruled out as not security issues. A candidate is called Confirmed only when the deciding code path was read and the defect follows from the code as written.

What were the confirmed defects in OWASP Juice Shop?

They include the login SQL injection in routes/login.ts, a zip-slip arbitrary file write in routes/fileUpload.ts, and a CAPTCHA that always passes in routes/imageCaptcha.ts, plus a YAML path traversal, an authorization bypass via a forged continue code, unauthenticated delivery-status manipulation, and weak JWT verification.

Which vulnerability category did the scan miss?

XML External Entities. Juice Shop's XXE lives in an upload handler outside the routes/ directory that was scoped, so it was never in scope; scanning lib/ too brings it into range.

What language is the scanned code written in?

The scanned server-side route handlers are Node.js and TypeScript.

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.

Get RedMirror Read the docs