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.
| Category | Count |
|---|---|
| Confirmed security defects | 8 |
| 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.
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.
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).
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.
../ filtering.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:
| Category | Reached by the scan? | Example |
|---|---|---|
| Injection | Confirmed | login SQL injection; NoSQL in reviews/orders |
| Broken Access Control | Confirmed | forged continue code; unauthenticated delivery change |
| Improper Input Validation | Confirmed | zip-slip file write; YAML path traversal |
| Insecure Deserialization | Confirmed | yaml.load on user input |
| Broken Anti-Automation | Confirmed | the CAPTCHA bypass |
| Unvalidated Redirects | Candidate | open redirect in the redirect route |
| Broken Authentication | Candidate | weak JWT verification |
| Cryptographic Issues | Candidate | the same JWT weakness |
| Cross-Site Scripting | Candidate | unencoded values reaching a response |
| XML External Entities | Missed | the 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.
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.”
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 vulnerabilities we've reported in real OSS.
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.
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.
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.
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.
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.
The scanned server-side route handlers are Node.js and TypeScript.
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.