Not Safe for Production — fix 3 things before you ship.Not Safe for Production — fix 3 things before you make it live.Not Safe for Production — fix 3 things before you ship.
63/ 100executive score · higher is betterthe one overall score · higher is betterexecutive score · higher is better
Critical issues block production use. Do not ship as-is.Serious problems block using this for real users. Don't make it live as-is.Fix these serious problems before shipping.
62Production ReadinessReady for real usersProduction Readiness60Security58Privacy74ArchitectureHow the code is put togetherArchitecture92IP ProtectionProtecting your ideasIP Protection
Read it:
3 things to fix before you ship3 things to fix before you launch3 things to fix before you ship
A finite, ranked list — highest-severity, lowest-effort blockers first. Clear these and the verdict lifts.A short, ranked list — the most serious, easiest-to-fix problems first. Clear these and the verdict improves.Ranked by severity and effort. Clear these and the verdict lifts.
distance to ship
Your code scores 63. 16 blockers stand between it and shipping — the Not Safe for Production verdict above holds until they are resolved.
Resolving these 16 blockers is necessary but not sufficient: even setting them aside, at 63 this code sits in the High Risk band on its own and needs broader work before it is ready to ship. Fix these 16 blockers first — they force the Not Safe for Production verdict outright — then keep going.
What’s blocking — resolve to clear the Not Safe for Production verdict
1
Generic hardcoded secret committed to the repository
data/static/users.yml:63Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
2
Generic hardcoded secret committed to the repository
data/static/users.yml:88Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
3
Generic hardcoded secret committed to the repository
data/static/users.yml:107Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
4
Generic hardcoded secret committed to the repository
data/static/users.yml:115Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
5
Generic hardcoded secret committed to the repository
data/static/users.yml:123Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
6
Generic hardcoded secret committed to the repository
data/static/users.yml:141Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
7
Generic hardcoded secret committed to the repository
data/static/users.yml:150Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
8
Generic hardcoded secret committed to the repository
data/static/users.yml:159Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
9
Generic hardcoded secret committed to the repository
data/static/users.yml:239Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
10
Generic hardcoded secret committed to the repository
data/static/users.yml:245Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
11
Generic hardcoded secret committed to the repository
data/static/users.yml:272Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
12
Generic hardcoded secret committed to the repository
data/static/users.yml:277Small (hours)
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
13
Generic hardcoded secret committed to the repository
Fix — Remove the credential from source, purge it from git history, and rotate it — a committed secret must be treated as already compromised.
14
Critical vulnerability in jsonwebtoken
jsonwebtoken@0.4.0Small (hours)
GHSA-c7hr-j4mj-j2w6 (CRITICAL) — Verification Bypass in jsonwebtoken
Fix — Upgrade jsonwebtoken past the affected range to a patched release. Reachability from your own code was not verified — the floor stands on the vulnerable version being installed.
15
Critical vulnerability in marsdb
marsdb@0.6.11Small (hours)
GHSA-5mrr-rgp6-x4gr (CRITICAL) — Command Injection in marsdb
Fix — Upgrade marsdb past the affected range to a patched release. Reachability from your own code was not verified — the floor stands on the vulnerable version being installed.
Fix — Upgrade sequelize past the affected range to a patched release. Reachability from your own code was not verified — the floor stands on the vulnerable version being installed.
GitHub Actions script injection via release event fields in news-update workflows
High·Security·.github/workflows/update-news-www.yml:19·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
.github/workflows/update-news-www.yml:19and .github/workflows/update-news-www-legacy.yml:19 embed ${{ github.event.release.published_at }} and ${{ github.event.release.tag_name }} directly into a shell `sed -i` command (IaC scan: gha-script-injection).
Business impactWhy it mattersImpact
Untrusted-looking event input interpolated into a shell command can allow command injection into the CI runner, potentially compromising build secrets or the project's own web content pipeline — a supply-chain integrity risk distinct from the app's intentional vulnerabilities.
Everything else we foundEverything else we noticedEverything else
22 more findings across 7 dimensions — none blocking, all worth a look.22 more problems across 7 categories — none blocking, all worth a look.22 more findings — none blocking.
25Findings in allTotal problems foundAll findings
1High
4Medium
16Low
4Info
Security
How we scored it
Five component scores roll up into the one number — and the verdict is held to the evidence, not to how the write-up reads.Five scores combine into the one overall number — and the verdict is checked against the actual evidence, not just how the write-up sounds.Five scores roll into one number. The verdict is held to the evidence, not the write-up.
Production ReadinessReady for real usersProduction Readiness62
In the model’s wordsWhat the AI wrote, in its own wordsThe model's words
The executive summary, as Claude wrote it. Evidence, scores and the verdict are computed by nittim.The summary, as Claude wrote it. The evidence, scores and verdict are computed by nittim, not by Claude.Claude wrote this summary. nittim computes the evidence, scores, and verdict.
executive summary · in the model’s wordssummary · in the AI’s own wordssummary · in the model's words
Claude says
Evidence, scores, and the verdict above are computed by nittim. The written read below — this summary and every finding’s narrative — is Claude’s.The evidence, scores, and the verdict above are computed by nittim. The written explanation below — this summary and every finding’s write-up — is Claude’s, the AI assistant that reviewed your code.nittim computes the evidence, scores, and verdict. Claude writes the summary below.
This is the OWASP Juice Shop (v20.2.0), the flagship OWASP intentionally-vulnerable web application used for security training, CTFs, and awareness. Its self-evident and declared purpose is to CONTAIN vulnerabilities: the SQL injection in the login route, the hardcoded RSA private key and MD5 hashing in lib/insecurity.ts, the deliberately pinned vulnerable dependencies (express-jwt@0.1.3, jsonwebtoken@0.4.0, sequelize, marsdb, sanitize-html@1.4.2), the seed credentials in data/static/users.yml, and the DOM sanitizer bypasses are all designed challenges — many even documented in CSAF security advisories under .well-known/csaf/ and pinned via .dependabot/config.yml. Per audit rules these intentional vulnerabilities are recorded in the exclusion list and do NOT count against the scores or verdict. Setting them aside, the repository itself is an exceptionally mature, well-engineered open-source product: comprehensive multi-tier test suites (unit/api/e2e), multi-OS/Node CI matrices, SBOM generation, observability via prom-client/winston, pinned CI action SHAs for most workflows, and clean modular TypeScript/Angular architecture. The genuine (non-intentional) findings are limited to CI/DevOps supply-chain hygiene: GitHub Actions script-injection patterns in release-notes workflows, a handful of unpinned third-party actions, and a root-running smoke-test container. The over-arching operational caveat is inherent to the product: it must only ever be deployed in isolated training environments, never exposed as a real shop.
Root causeWhy it happensCause
Direct interpolation of GitHub event context into a run: shell step instead of passing values via environment variables.
Recommended fixHow to fix itFix
Assign the event fields to env: variables and reference them as "$TAG_NAME"/"$PUBLISHED_AT" inside the script, or validate/escape them; never inline ${{ github.event.* }} into shell.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
GitHub Actions script injection via release event interpolation
Medium·Infrastructure & DevOps·.github/workflows/update-news-www.yml:19·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
.github/workflows/update-news-www.yml:19and .github/workflows/update-news-www-legacy.yml:19 build a sed command by directly interpolating ${{ github.event.release.published_at }} and ${{ github.event.release.tag_name }} into a shell script (flagged by IaC scan as gha-script-injection).
Business impactWhy it mattersImpact
A crafted release tag/body could inject shell commands into the runner that holds secrets.BOT_TOKEN with write access to external OWASP/juice-shop.github.io repositories, enabling supply-chain compromise of the project's public websites.
Root causeWhy it happensCause
Untrusted release-event fields are inlined into a run step instead of being passed through intermediate environment variables.
Recommended fixHow to fix itFix
Assign the release fields to env: variables and reference them as "$TAG_NAME" inside the script, or validate/escape them; never interpolate ${{ github.event.* }} directly into run: shell.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
GitHub Actions script injection via release event fields in news-publishing workflows
Medium·Infrastructure & DevOps·.github/workflows/update-news-www.yml:19·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
.github/workflows/update-news-www.yml:19and .github/workflows/update-news-www-legacy.yml:19 interpolate ${{ github.event.release.published_at }} and ${{ github.event.release.tag_name }} directly into a shell `sed -i` command (flagged as gha-script-injection by the IaC scanner).
Business impactWhy it mattersImpact
A crafted release tag name could inject shell commands into the workflow runner, potentially compromising CI secrets or the published artifact. This is a supply-chain integrity risk distinct from the application's intentional training vulnerabilities. Static config analysis only; live exploitability not confirmed and the trigger (release events) is normally maintainer-controlled.
Root causeWhy it happensCause
Untrusted-but-maintainer-influenced event context values are used unquoted inside an inline shell command instead of being passed via environment variables.
Recommended fixHow to fix itFix
Assign the release fields to intermediate `env:` variables and reference them as "$TAG_NAME" inside the run step, or use actions/github-script with proper escaping. Avoid embedding ${{ github.event.* }} directly in shell.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
OSV scan flags e.g. @angular/common@22.0.1 (GHSA-jhpw-976m-542j cache poisoning), body-parser@1.20.4 (GHSA-v422-hmwv-36x6 DoS), morgan@1.10.0 (log forging), multer@1.4.5-lts.1 (multiple DoS), socket.io@3.1.2 (unhandled error). Reachability was NOT verified — these are version matches, and exploitability against this repo's own code paths is not established.
Business impactWhy it mattersImpact
Distinct from the deliberately-outdated challenge libraries, these are current-generation framework/middleware deps carrying known advisories; on any real deployment they could enable DoS or cache/state issues if reachable.
Root causeWhy it happensCause
Normal transitive/direct dependency lag against newly published advisories.
Recommended fixHow to fix itFix
Run npm audit / OSV triage, upgrade the non-challenge framework and middleware dependencies to patched releases, and keep the intentionally-vulnerable challenge libraries pinned and documented separately.
RemediationHow to fix itRemediationMedium (1–3d)Medium (1–3 days)Medium (1–3d)
OSV scan lists among others body-parser@1.20.4 (GHSA-v422-hmwv-36x6 DoS), morgan@1.10.0 (log forging), socket.io@3.1.2 (unhandled error event), multer@1.4.5-lts.1 (multiple DoS), file-type@16.5.4 (infinite loop), glob@10.4.5 (CLI command injection), lodash-es@4.17.21 (prototype pollution / _.template). Reachability was NOT verified by the scanner (version match only).
Business impactWhy it mattersImpact
These are not part of the deliberately-vulnerable component challenges and represent ordinary supply-chain drift; several are DoS-class and reachability is unconfirmed, so real-world exploitability is uncertain.
Root causeWhy it happensCause
Transitive/aged dependency versions in the two package roots.
Recommended fixHow to fix it
Privacy & Compliance
Priority: Core2 findings
Application handles PII categories with intentionally non-compliant GDPR flows (by design) · No demonstrable data-subject-rights, consent-granularity, or retention controls for registered personal data
Low·Privacy & Compliance·test/api/address.test.ts·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
Models and routes handle emails, addresses (test/api/address.test.ts), payment cards (routes/payment.ts), security answers, and a GDPR data-export/erasure path (routes/resetPassword.ts, erasure-request tests). The 'GDPR Data Erasure' challenge (data/static/challenges.yml, ghostLoginChallenge) intentionally implements soft-delete that leaves users recoverable.
Business impactWhy it mattersImpact
None in intended use — the shop runs with fabricated seed data in isolated training environments; the GDPR non-compliance is a teaching artifact, not a real data-subject exposure.
Root causeWhy it happensCause
Privacy weaknesses are deliberate learning objectives, not production data handling.
Recommended fixHow to fix itFix
No change required for the training tool; operators must never load real personal data into a Juice Shop instance.
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
No demonstrable data-subject-rights, consent-granularity, or retention controls for registered personal data
Low·Privacy & Compliance·data/static/users.yml·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
data/static/users.ymlstores real-looking PII (emails, postal addresses, card numbers, security-question answers). The application collects self-registered user emails/passwords via frontend/src/app/Services/user.service.ts and related flows, and cookie handling is present (LocalBackupService, cookieconsent). No lawful-basis/consent-granularity, retention-period, or working access/rectification/erasure process is evident in the audited snapshot (the erasure path is an intentional challenge).
Business impactWhy it mattersImpact
For publicly-hosted training instances that accept real registrations, absence of retention/deletion and consent controls is a minor GDPR exposure, materially limited because instances are sandboxes with disposable data and the app is not a real commercial data controller.
Code Quality & Architecture
Priority: Important1 finding
Pervasive error-swallowing to console.log across frontend components
Pervasive error-swallowing to console.log across frontend components
Nearly every service subscription handles the error branch with `error: (err) => { console.log(err) }` (e.g. navbar.component.ts, order-completion.component.ts, recycle.component.ts, purchase-basket.component.ts, faucet.component.ts) with no user-facing recovery or centralized reporting.
Business impactWhy it mattersImpact
Failures degrade silently for the end user and leave no structured client telemetry, hurting incident readiness and UX; acceptable for a demo but a real product defect.
Root causeWhy it happensCause
No shared error-handling/notification abstraction; happy-path-oriented handlers typical of rapid feature development.
Recommended fixHow to fix itFix
Introduce a central error handler / user-facing snackbar strategy and route errors to an observability sink rather than console.
RemediationHow to fix itRemediationMedium (1–3d)Medium (1–3 days)Medium (1–3d)
Useful?
Infrastructure & DevOps
Priority: Important12 findings
GitHub Actions script injection in release-news workflows · Unpinned third-party GitHub Actions · Bundle-analysis workflow auto-commits to master with unpinned Playwright/npm installs · Smoke-test container runs as root on unpinned base image · GitHub Actions script-injection pattern in release-news workflows · Unpinned third-party GitHub Actions and base images in real CI/build config · Unpinned GitHub Actions and container base images in CI/build · Smoke-test container runs as root · Smoke-test container runs as root and uses an unpinned base image · Unpinned third-party GitHub Actions in CI · Unpinned GitHub Actions and root-running smoke-test container · Smoke-test container runs as root and uses unpinned base image
GitHub Actions script injection in release-news workflows
Medium·Infrastructure & DevOps·.github/workflows/update-news-www-legacy.yml:19·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
IaC scanner: [high] gha-script-injection at .github/workflows/update-news-www-legacy.yml:19 and .github/workflows/update-news-www.yml:19 interpolating ${{ github.event.release.tag_name }} / ${{ github.event.release.published_at }} directly into a `sed -i` shell command. (Static config match; live pipeline not executed.)
Business impactWhy it mattersImpact
Untrusted release-event fields interpolated into a shell command could allow command execution in the CI runner, a supply-chain integrity risk for the project's own publishing automation. Exploitability is limited because release tags are set by maintainers with push rights, but tag names are not a trusted input surface.
Root causeWhy it happensCause
Directly embedding GitHub event context expressions into a run-step shell command instead of passing them via environment variables.
Recommended fixHow to fix itFix
Assign the event fields to intermediate `env:` variables and reference them as quoted shell variables ("$TAG") inside the run step, never inline `${{ ... }}` in shell commands.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
Unpinned third-party GitHub Actions
Low·Infrastructure & DevOps·.github/workflows/ci.yml:188·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
.github/workflows/ci.yml:188uses coverallsapp/github-action@v2; .github/workflows/image_actions.yml:33 uses calibreapp/image-actions@main; image_actions.yml:42 uses peter-evans/create-pull-request@v8 (flagged gha-unpinned-action). Most other workflows correctly pin to full commit SHAs.
Business impactWhy it mattersImpact
A compromised or force-pushed tag/branch of these actions could execute arbitrary code in CI with repository token access.
Root causeWhy it happensCause
Inconsistent action-pinning policy; some actions reference mutable tags/branches rather than immutable commit SHAs.
Bundle-analysis workflow auto-commits to master with unpinned Playwright/npm installs
Low·Infrastructure & DevOps·.github/workflows/frontend-bundle-analysis.yml·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
.github/workflows/frontend-bundle-analysis.ymlinstalls playwright and chromium at runtime and git-commits/pushes a screenshot to master (continue-on-error: true), running node -e with a file:// path built from ${{ github.workspace }}.
Business impactWhy it mattersImpact
Automated pushes to the default branch plus unpinned runtime dependency installs widen the CI supply-chain surface and can silently mutate the protected branch.
Root causeWhy it happensCause
Convenience automation writing back to master without pinned tooling or branch protection considerations.
Smoke-test container runs as root on unpinned base image
Low·Infrastructure & DevOps·Dockerfile:1·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
test/smoke/Dockerfile:1 uses 'FROM alpine' with no USER instruction (flagged docker-root-user and docker-unpinned-base-image). This is test-only tooling, not the shipped image (the production Dockerfile correctly uses distroless and USER 65532).
Business impactWhy it mattersImpact
Minimal — confined to CI smoke testing; no production exposure, but root-in-container and floating base tag are avoidable hygiene gaps.
Root causeWhy it happensCause
Test harness image not held to the same hardening standard as the release image.
Recommended fix
GitHub Actions script-injection pattern in release-news workflows
Low·Infrastructure & DevOps·.github/workflows/update-news-www.yml:19·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
.github/workflows/update-news-www.yml:19and .github/workflows/update-news-www-legacy.yml:19 interpolate ${{ github.event.release.tag_name }} and github.event.release.published_at directly into a shell `sed` command (IaC scan: gha-script-injection).
Business impactWhy it mattersImpact
An attacker able to control a release tag name could inject shell commands into the news-update automation. Real exploitability is limited because release events are controlled by maintainers with push/release rights, but the pattern is a genuine CI supply-chain hardening gap rather than intentional challenge material.
Unpinned third-party GitHub Actions and base images in real CI/build config
Low·Infrastructure & DevOps·Dockerfile:22·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
IaC scan flags .github/workflows/ci.yml:188 (coverallsapp/github-action@v2), image_actions.yml:33 (calibreapp/image-actions@main), :42 (peter-evans/create-pull-request@v8), and unpinned Docker base images (Dockerfile:22 distroless/nodejs24-debian13, test/smoke/Dockerfile:1 alpine).
Business impactWhy it mattersImpact
Floating tags/branches (notably @main) allow a compromised upstream action to run in the project's CI with repository permissions. This is standard supply-chain hygiene for the project's own pipeline, not challenge content.
Root causeWhy it happensCause
Actions referenced by moving tag/branch rather than immutable commit SHA.
Unpinned GitHub Actions and container base images in CI/build
Low·Infrastructure & DevOps·Dockerfile:22·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
IaC scan: gha-unpinned-action at .github/workflows/ci.yml:188 (coverallsapp/github-action@v2), image_actions.yml:33 (calibreapp/image-actions@main) and :42 (peter-evans/create-pull-request@v8); docker-unpinned-base-image at Dockerfile:22 (distroless/nodejs24-debian13) and test/smoke/Dockerfile:1 (alpine).
Business impactWhy it mattersImpact
Mutable tags/branches (notably @main) allow a compromised or retagged upstream to alter build behavior, a moderate supply-chain risk to the CI pipeline.
Root causeWhy it happensCause
Actions and images referenced by tag/branch rather than pinned commit SHA/digest.
Smoke-test container runs as root
Low·Infrastructure & DevOps·Dockerfile:1·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
IaC scan: docker-root-user at test/smoke/Dockerfile:1 — no USER instruction present, container runs as root.
Business impactWhy it mattersImpact
A root-running container in the smoke-test path slightly widens blast radius if the test environment is compromised; low impact as it is a test artifact, not the shipped image.
Root causeWhy it happensCause
Missing non-root USER directive in the test Dockerfile.
Recommended fixHow to fix itFix
Add a non-root USER instruction to the smoke-test Dockerfile.
Smoke-test container runs as root and uses an unpinned base image
Low·Infrastructure & DevOps·Dockerfile:1·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
test/smoke/Dockerfile:1 has no USER instruction (docker-root-user) and uses `FROM alpine` unpinned (docker-unpinned-base-image), per the IaC scan.
Business impactWhy it mattersImpact
A smoke-test container running as root with a floating base tag is low-risk (test-only, not the shipped app image, which uses distroless nodejs), but reduces reproducibility and violates least-privilege container hygiene.
Root causeWhy it happensCause
Test tooling image was not hardened or pinned since it is ephemeral.
Recommended fixHow to fix it
Unpinned third-party GitHub Actions in CI
Low·Infrastructure & DevOps·.github/workflows/ci.yml:188·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
.github/workflows/ci.yml:188uses coverallsapp/github-action@v2; .github/workflows/image_actions.yml:33 uses calibreapp/image-actions@main and :42 uses peter-evans/create-pull-request@v8 (gha-unpinned-action).
Business impactWhy it mattersImpact
Actions referenced by moving tags/branches (@main, @v2) can change under maintainers, creating a supply-chain drift/tampering surface for CI.
Root causeWhy it happensCause
Actions pinned to tags/branches rather than commit SHAs.
Unpinned GitHub Actions and root-running smoke-test container
Low·Infrastructure & DevOps·Dockerfile:1·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
IaC scan: gha-unpinned-action at .github/workflows/ci.yml:188 (coverallsapp/github-action@v2), image_actions.yml:33 (calibreapp/image-actions@main) and :42 (peter-evans/create-pull-request@v8); docker-root-user at test/smoke/Dockerfile:1 (no USER instruction); docker-unpinned-base-image at Dockerfile:22 and test/smoke/Dockerfile:1.
Business impactWhy it mattersImpact
Floating action tags (especially @main) allow an upstream compromise to flow into CI; a root smoke-test container slightly widens blast radius if that image is misused. Impact is limited (test/CI scope).
Root causeWhy it happensCause
Third-party actions referenced by mutable tag rather than commit SHA; test Dockerfile omits a non-root USER.
Smoke-test container runs as root and uses unpinned base image
Low·Infrastructure & DevOps·Dockerfile:1·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
IaC scanner: [high] docker-root-user at test/smoke/Dockerfile:1 (no USER instruction) and [low] docker-unpinned-base-image `FROM alpine` at the same file.
Business impactWhy it mattersImpact
Root containers and floating base images are poor hygiene; here the blast radius is minimal because this Dockerfile is only used for smoke-testing, not the shipped image (root Dockerfile uses distroless).
Root causeWhy it happensCause
Test-only Dockerfile omits a non-root USER and pins no base image digest.
Recommended fixHow to fix it
Accessibility & UX
Priority: Supporting1 finding
Incomplete localization leaves untranslated English strings in several locales
Incomplete localization leaves untranslated English strings in several locales
Info·Accessibility & UX·fa_IR.json·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
fa_IR.jsoncontains empty-string values (e.g. key "2":"", "Submit 10 or more customer feedbacks within 20 seconds.":"") and many locale files (ca_ES.json, et_EE.json, el_GR.json, ga_IE.json, he_IL.json) leave large blocks as verbatim English.
Business impactWhy it mattersImpact
Users of partially-translated locales see mixed or blank UI strings, a minor UX/i18n quality issue.
Root causeWhy it happensCause
Community translations are contributed incrementally and remain incomplete for some languages.
Recommended fixHow to fix itFix
Fill empty values from the English base at build time and track per-locale completeness; never ship empty translation values.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
Observability
Priority: Supporting1 finding
No structured logging/metrics/telemetry wiring on the client
No structured logging/metrics/telemetry wiring on the client
Client error paths rely exclusively on console.log; no evidence in the audited frontend of error-reporting, metrics, or tracing integration.
Business impactWhy it mattersImpact
For a real deployment there would be no way to detect client-side failures or measure SLOs; for a training tool this is acceptable.
Root causeWhy it happensCause
Demo/training scope; observability not a project goal.
Recommended fixHow to fix itFix
If ever repurposed for real use, wire a browser error-reporting SDK (env-gated) and basic RUM. Cannot verify from the repo whether any such reporting is enabled at runtime.
RemediationHow to fix itRemediationMedium (1–3d)Medium (1–3 days)Medium (1–3d)
Useful?
Maintainability Forecast
Priority: Specialized3 findings
Large duplicated English-fallback strings across every locale file · Audit snapshot for this pass contained only i18n translation data · Vendored legacy three.js (revision 67) and dat.gui bundled in frontend assets
Large duplicated English-fallback strings across every locale file
Each data/static/i18n/*.json (e.g. ca_ES.json, cs_CZ.json, da_DK.json, en.json, and ~1000+ eligible files) repeats the full ~1000-key English string set as fallback, with many values identical to the English source (e.g. entire challenge/hint blocks untranslated in ca_ES.json and et_EE.json).
Business impactWhy it mattersImpact
High storage/maintenance overhead and drift risk: source-string changes must be manually propagated across every locale, and partially-translated files silently ship English text to users of that locale.
Root causeWhy it happensCause
i18n design stores full key/value pairs per locale with English duplicated as the untranslated fallback rather than a delta-over-base scheme.
Recommended fixHow to fix itFix
Rely on a runtime fallback to en.json for missing keys and store only actually-translated deltas per locale; add CI coverage reporting for translation completeness.
RemediationHow to fix itRemediationMedium (1–3d)Medium (1–3 days)Medium (1–3d)
Useful?
Audit snapshot for this pass contained only i18n translation data
Info·Maintainability Forecast·Trivial (<1h)Very quick (under an hour)Trivial (<1h)
EvidenceWhat we foundEvidence
All 12 files shown in this pass are data/static/i18n/*.json translation dictionaries; no server routes, models, authentication, or data-layer code was present in this pass (pass 3 of 6).
Business impactWhy it mattersImpact
Application-level security, data-layer, and architecture conclusions cannot be drawn from this pass alone; findings here rest on scanner output and translation content only.
Root causeWhy it happensCause
Bounded per-pass file selection; core code is read in other passes.
Recommended fixHow to fix itFix
Correlate this pass with the other passes' code review before finalizing app-level verdicts; treat absence of app-code findings here as scope, not as a clean bill.
Vendored legacy three.js (revision 67) and dat.gui bundled in frontend assets
Info·Maintainability Forecast·frontend/src/assets/private/three.js·Small (hours)Quick (a few hours)Small (hours)
EvidenceWhat we foundEvidence
frontend/src/assets/private/three.jsdeclares `THREE = { REVISION: '67' }` (circa 2013); OrbitControls.js, EffectComposer.js, dat.gui.min.js are similarly vendored copies used by frontend/src/assets/private/threejs-demo.html.
Business impactWhy it mattersImpact
These are intentionally retained challenge assets (the 'Planet Orangeuze' easter egg), so no direct production risk, but the vendored copies are unmanaged by any dependency tooling and would be dead weight/technical debt in a non-training codebase.
Root causeWhy it happens
ArchitectureHow the code is put togetherArchitecture
74
IP ProtectionProtecting your ideasIP Protection92
How the executive score is composedHow the overall score is put togetherHow the exec score is composed
Security60
weightimportanceweight
Privacy58
weightimportanceweight
Production ReadinessReady for real usersProduction Readiness62
weightimportanceweight
ArchitectureHow the code is put togetherArchitecture74
weightimportanceweight
Weighted blend of 4 dimensions → ~63/100. Dimensions are weighted by importance (Security and Privacy weigh heaviest). Published figures are approximate; nittim's exact weighting and precise scoring are proprietary, while the verdict is computed on exact internal values.4 categories combine into ~63/100. Some categories count for more than others (Security and Privacy count the most). The published numbers are approximate; nittim's exact math is kept private, while the verdict itself is worked out from the exact internal numbers.Weighted blend of 4 dimensions → ~63/100 (Security and Privacy weigh heaviest). Published figures are approximate; nittim's exact weighting is proprietary. The verdict runs on exact internal values. IP Protection is an independent IP-defensibility lens and is excluded from this blend.IP Protection (checking whether your ideas are exposed) is judged separately and isn’t
part of this combined number.IP Protection is judged separately, excluded from this blend. Scores are a judgement, not a measurement: a re-audit of the same commit can land a few
points either side of this figure. The evidence underneath it is deterministic — the same
commit always yields the same committed-secret and vulnerable-dependency findings.Scores are a judgment call, not an exact measurement: checking the same saved snapshot of
your code twice can land a few points either side of this number. The evidence underneath
it is not — the same snapshot always turns up the same passwords-in-code and known-security-bug
findings.Scores are a judgment, not a measurement — a re-audit can land a few points either side. The evidence underneath is deterministic: the same commit always yields the same secret and vulnerable-dependency findings. Each score is the model's overall assessment of the dimensions it covers, across everything it reviewed — not a tally of the findings in this report. Findings are the subset worth acting on, so a score can sit above or below what the listed findings alone would suggest.Each score is the AI's overall judgment of everything it reviewed in that category — not a
count of the problems listed in this report. The listed problems are the ones worth acting
on, so a score can sit above or below what they alone would suggest.Each score is the model's overall assessment across everything it reviewed — not a tally of this report's findings. A score can sit above or below what the findings alone suggest.
nittim guardrail — nittim's safety check — nittim guardrail — The model proposed “ready_with_conditions”; nittim's deterministic layer set the final verdict to “not_safe” because of a committed production secret, a critical vulnerability in an installed runtime dependency (reachability from your own code was not verified). The written rationale below reflects the model's proposal — the verdict above is the one that governs.
AI-generated likelihoodHow much of this looks AI-writtenAI-generated likelihood20/100Informational only — an estimate of how the code was authored. It does not affect the
executive score or the verdict.This is just for information — a guess at how much of the code an AI wrote. It does not
change the overall score or the verdict.Informational only — it doesn't affect the score or the verdict.
Where this score sits on nittim's production-readiness scaleWhere this score falls on nittim's “is it ready to ship” scaleThis score's spot on nittim's scale.
63
0–49 Not Safe for Production
50–69 High Risk
70–84 Ready with Conditions
85–100 Production Ready
nittim grades against fixed production-readiness thresholds, not a peer corpus — this is a band on our own verdict scale, NOT a percentile or “better than X% of repos.”nittim grades against fixed, unmoving standards for “is this ready to ship” — not against other people’s projects. This is a spot on our own scale, NOT a percentile and NOT a claim that you beat some percent of other repos.nittim grades against fixed thresholds, not other repos. This is our own scale — never a percentile.
nittim audits a prioritized snapshot of the repository. It’s a strong, evidence-based trust signal — not a substitute for a full security review or penetration test.nittim checks a prioritized copy of your project’s code. It’s a strong, evidence-based signal you can trust — not a replacement for a full security review or a professional break-in test (a “penetration test”).nittim audits a snapshot — not a full security review or penetration test.
Verdict rationale — Why this verdict — Verdict rationale — Judged as what it is — the OWASP flagship intentionally-vulnerable training application — this repository is a mature, well-maintained, released product (v20.2.0) with strong engineering practices: extensive automated test coverage, multi-platform CI, SBOM generation, observability wiring, and hardened production/distroless container images. All of the alarming scanner results (SQLi, hardcoded RSA key, MD5, pinned CVE-laden dependencies, seed credentials) are the deliberate, documented reason the project exists and are therefore excluded from scoring. The only genuine, actionable defects are CI/DevOps supply-chain hygiene items (Actions script-injection in the release-notes workflows, a few unpinned actions, an auto-push bundle workflow, and a root smoke-test container), all low-to-medium and easily fixed. The controlling condition is inherent and non-negotiable: this application must only ever be deployed in isolated training/CTF environments and must never be exposed as a real internet-facing shop or loaded with real personal data. With that operational boundary honored and the minor CI issues addressed, it is fit for its intended purpose.
Fix
Run dependency updates / npm audit fix for the non-challenge packages and confirm via import/call-graph analysis whether the vulnerable paths are reachable before prioritising.
RemediationHow to fix itRemediationMedium (1–3d)Medium (1–3 days)Medium (1–3d)
Useful?
Root causeWhy it happensCause
Training-app design prioritizes challenge scenarios over compliance controls; seed PII is demo data.
Recommended fixHow to fix itFix
For any hosted instance, document a privacy notice, retention/wipe schedule, and treat registration data as disposable; do not process real user PII. No change needed for the training purpose itself.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
Recommended fixHow to fix itFix
Pin all third-party actions to full-length commit SHAs, as already done for actions/checkout and setup-node elsewhere in the same repo.
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
Recommended fixHow to fix itFix
Pin playwright version, avoid direct pushes to master (open a PR instead), and remove continue-on-error masking of failures.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
How to fix it
Fix
Pin the alpine tag and add a non-root USER, or note explicitly that this container is ephemeral CI-only.
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
Root causeWhy it happensCause
Untrusted-looking event context fields interpolated into a shell command instead of passed via environment variables.
Recommended fixHow to fix itFix
Pass release fields through `env:` variables and reference them as "$TAG_NAME" inside the run block, rather than templating ${{ ... }} directly into the shell command.
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
Recommended fixHow to fix itFix
Pin third-party actions to full commit SHAs and pin base images to digests; enable Dependabot for GitHub Actions.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
Recommended fixHow to fix itFix
Pin third-party actions to full commit SHAs and base images to digests; enable Dependabot for actions updates.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
Fix
Pin the base image by digest/tag and add a non-root USER instruction to the smoke-test Dockerfile.
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
Recommended fixHow to fix itFix
Pin third-party actions to full commit SHAs and use Dependabot to bump them.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
Recommended fixHow to fix itFix
Pin actions to full commit SHAs, pin base images by digest, and add a non-root USER to the smoke-test Dockerfile. Static config only.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)
Useful?
Fix
Add a non-root USER and pin the alpine base to a digest, or document that this container is ephemeral test-only.
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
RemediationHow to fix itRemediationTrivial (<1h)Very quick (under an hour)Trivial (<1h)
Useful?
Cause
Static third-party libraries committed directly into assets to support an easter-egg feature.
Recommended fixHow to fix itFix
None required for the training use case; if reused elsewhere, migrate to a managed dependency and drop the vendored copies.
RemediationHow to fix itRemediationSmall (hours)Quick (a few hours)Small (hours)