run-juiceshop

star 6

Run the full Clinkz pipeline against OWASP Juice Shop (Node/Angular SPA, JWT auth) and report vulnerability coverage across the 14 categories.

ptkvaibhav By ptkvaibhav schedule Updated 6/10/2026

name: run-juiceshop description: "Run the full Clinkz pipeline against OWASP Juice Shop (Node/Angular SPA, JWT auth) and report vulnerability coverage across the 14 categories."

Apply phase-work skill.

Juice Shop is the SPA/JWT counterpart to DVWA. Where DVWA is PHP + cookie sessions + HTML forms katana can crawl, Juice Shop is an Angular SPA whose real attack surface is /api/* and /rest/* JSON routes invoked by JavaScript — not HTML links — and it authenticates with a Authorization: Bearer <jwt> token, not a cookie session. Treat those two facts as the load-bearing differences when interpreting any miss.

  1. Verify Juice Shop is running at http://localhost:3000 (check via Invoke-WebRequest http://localhost:3000 or docker compose -f docker/docker-compose.yml ps). If not running: docker compose -f docker/docker-compose.yml up -d juiceshop
    • A healthy shell is a ~75 KB Angular index.html (HTTP 200) with almost no anchor links — the body is the SPA bootstrap, not the app. Do NOT read "200, large body" as "endpoints discoverable"; the routes are in the JS bundle.
    • Juice Shop self-registration is open: POST /api/Users {email, password, passwordRepeat} then POST /rest/user/login {email, password} returns the JWT at data.authentication.token. This is how tests/test_skills_juiceshop/conftest.py authenticates. The pipeline now does JSON/API auth too: WebAuthenticator.authenticate() falls back to POSTing JSON creds to /rest/user/login (and other common API routes), extracts the token, and propagates it as Authorization: Bearer <jwt> to scan/exploit via the session_headers handoff. The canonical default admin (admin@juice-sh.op / admin123) is seeded, so the orchestrator's default-cred flow can establish a JWT session without self-registering.
  2. The orchestrator handles clinkz-tools container readiness via its preflight. Just invoke: python -m clinkz scan --target http://localhost:3000
  3. Parse the JSON report from outputs/<engagement_id>/report_<engagement_id>.json and the trace from outputs/<engagement_id>/trace.jsonl (python -m clinkz trace inspect <engagement_id>).
  4. Report coverage as X/13-applicable using the Juice Shop category map below. Command Injection is the one genuine N/A (Node SPA, no child_process.exec sink reachable from HTTP input — the /ftp challenge is traversal/LFI, not shell). The other 13 categories have real surface.
  5. Include: total findings, by severity, engagement ID, runtime.
  6. If coverage < applicable count, give a single-line root cause per missed category and bucket it as one of: (a) JWT-auth failure (JSON/API auth is now wired — bucket here only if the JWT session genuinely failed to establish), (b) SPA/API endpoint discovery (the /api+/rest routes never reached planning), (c) JSON-body param handling (params live in a JSON request body, not the query string our extraction reads), or (d) methodology-shape mismatch (the methodology reached the right surface but its assumptions don't fit it). This bucketing is the deliverable — it prioritises the next fix. The upstream pipeline is now proven end-to-end (engagement a07df54b: recon→scan→discovery→JWT delivered ~138 endpoints and 8 methodologies ran), so buckets (a) and (b) are resolved — only flag them if the trace shows a genuine regression (no JSON login, or service_scans: [] / total_endpoints: 0 with no katana invocation). Bucket (c) JSON-body params is now RESOLVED (fix #4): ParamLocation is threaded EndpointExploitTaskPageAnalysis, a shared request builder (_send_probe/_http_post_json) injects into JSON bodies, the form-shaped methodologies iterate a synthesized JSON pseudo-form (_injectable_forms), and discovery recovers the body shape via OpenAPI requestBody parsing OR — since Juice Shop serves no parseable spec (/api-docs is Swagger-UI HTML over a partial /b2b/v2 doc) — a curated _KNOWN_JSON_POST_BODIES fallback. Verified live: brute-force (POST /rest/user/login) and CSRF (POST /api/Feedbacks) now find their JSON-body points; stored-XSS reaches /api/Feedbacks but is captcha-gated (captchaId required ⇒ justified non-finding unless the captcha is solved). So the expected remaining blocker is (d) methodology-shape (e.g. the open-redirect allowlist, the captcha gate on stored-XSS, and the SPA-shape rows below) — flag (c) only if a JSON-body category misses for a param-handling reason, not a target protection.

Juice Shop → 14-category map (canonical surfaces)

# Our category Juice Shop surface Canonical endpoint Notes
1 SQL Injection Product search GET /rest/products/search?q=' Sequelize/SQLite error-based; q concatenated into raw SQLite. Query-string param — most pipeline-friendly.
2 XSS Reflected Search bar GET /#/search?q=<payload> Reflected into DOM via Angular binding (not server HTML body).
3 XSS Stored Customer feedback POST /api/Feedbacks {comment,rating} Rendered into feedback list DOM. JSON body, no HTML form on page — now reached via the synthesized JSON pseudo-form (fix #4). Captcha-gated (captchaId required), so a verified finding needs the captcha solved — expect a justified non-finding otherwise, bucket (d), not (c).
4 Command Injection N/A — no OS-shell sink from HTTP input. The only filesystem surface (/ftp) is LFI-shaped. Expect zero cmdi findings: phase-5 now rejects an echo-canary reflected in an error response (a07df54b emitted 3 phantom "high" RCEs off 500 pages that merely reflected ;echo PWNED; hardened), so any cmdi finding here is a regression, not a catch.
5 File Inclusion / Path Traversal Legacy ftp GET /ftp/<file> (%2e%2e%2f bypass) Allowlist + .. block defeated by URL-encoding; path segment, not query param.
6 File Upload Complaint attachment POST /file-upload (or /api/Complaints) Over-permissive upload validation.
7 CSRF State-changing JSON APIs POST /api/Feedbacks, profile change JWT-bearer is the only CSRF defence → MISSING/COOKIE_ONLY token surface.
8 Brute Force Login POST /rest/user/login No lockout / captcha / rate-limit by design. JSON body creds.
9 Weak Session IDs JWT token token from /rest/user/login JWT, not a cookie session ID. Our entropy/flag methodology is cookie-shaped — shape mismatch expected, not a true negative.
10 DOM XSS SPA fragment routes GET /#/track-order?id=<payload> Client-side Angular binding; strength=likely (no JS interpreter).
11 JavaScript Attacks Score-board gate GET /#/score-board Client-side gate. Methodology is form-bound; SPA route serves no server-rendered form → expect skip/empty, not a contract break.
12 Authorization Bypass (IDOR) Basket / users GET /rest/basket/:id, GET /api/Users/:id Horizontal IDOR — auth'd but :id not owner-checked. Strong surface. Reference in path segment.
13 Open HTTP Redirect Redirector GET /redirect?to=<url> Substring-allowlist gateto 302s only if it contains an allowlisted URL (e.g. https://github.com/bkimminich/juice-shop); every generic attacker URL (//evil, @evil, appended) 406s. So _test_open_redirect phase-1 candidacy correctly returns False (no 302 to detect) — bucket (d) methodology-shape, NOT a candidacy-signal bug. Confirming it needs an allowlist-bypass (harvest the app's own allowlisted outbound links → phase-4 synthesis). Deferred (a07df54b diagnosis).
14 CSP Bypass / Security Headers Root response GET / headers Missing/weak CSP, Referrer-Policy, Permissions-Policy on most builds (build-dependent).

Applicable = 13 (all except #4 Command Injection).

Tier-1 + Tier-2 primitives beyond the 14-category map (expansion — see docs/ROADMAP.md)

Primitive Juice Shop surface Canonical request Notes
NoSQL Injection (_test_nosqli) Product reviews (MarsDB) PATCH /rest/products/reviews {"id":{"$ne":-1},"message":...} "NoSQL Manipulation"$ne operator object widens the match set; confirmed by a modified-count jump over the benign baseline. JSON-body operator object (the structured carrier). The $where DoS surface (/rest/track-order/:id, /rest/products/sleep(N)/reviews) is sanitised on recent builds (the lone quote is stripped, ',sleep(2000),' collapses to sleep2000 with no delay) → a verification-honest non-finding there. Note: Juice Shop's login is SQLi, not NoSQL, so the gate is reviews-manipulation, not a login {"$ne":null} bypass.
SSTI (_test_ssti) Profile username (Pug) POST /profile username=#{a*b} → read-back GET /profile "SSTi" — second-order Pug injection; #{a*b} compiled into views/userProfile.pug renders the product on read-back (the POST 302-redirects). Engine fingerprinted as Pug (#{}). Expected justified non-finding on default Docker: the eval is gated behind isChallengeEnabled(usernameXssChallenge) and challenges.safetyMode=auto + disabledEnv:[Docker] disables it, so #{a*b} renders literally → nothing emits. Confirmed end-to-end against a safetyMode=disabled instance (high-severity expression_eval; the process.mainModule RCE gadget degrades to eval on Node 22). The profile reflects the username twice (eval'd <p> + raw input value=), so detection is baseline-anchored / scaffold-stripped, not literal-presence-gated.
XXE (_test_xxe) B2B complaint file upload (libxml) POST /file-upload multipart .xml with <!ENTITY xxe SYSTEM "file:///etc/passwd"> "XXE Data Access" / "XXE DoS" — the XML parse + entity expansion is gated by deprecatedInterfaceChallenge (no disabledEnv → runs in Docker); the two XXE challenges are disabledEnv:[Docker,Heroku,Gitpod], which suppresses only the scoreboard banner. On expansion the server reflects the file content in-band in an HTTP 410 (…deprecated for security reasons: <…>root:x:0:0:…), so file disclosure confirms on the file-content signature — and the 4xx is the legitimate channel, NOT a reject signal (the prior trio's "reject 4xx" guard does not apply). Endpoint-scoped (the whole XML doc is the payload; carrier = multipart upload, no injectable param). May emit a real finding even on default Docker if the container's libxml resolves the external entity (documented-unreliable → the reason the challenges are Docker-disabled); else a verification-honest non-finding — the smoke gate is skip-tolerant. DoS is bounded (parse-delay / 503), never a real billion-laughs (segfaults the parser).
JWT attacks (_test_jwt) — Tier-2 Bearer-gated API routes (/rest/basket/:id, /rest/user/whoami) GET /rest/basket/1 with a forged Authorization: Bearer "Unsigned JWT" (alg:none) / "Forged Signed JWT" — the first Tier-2 primitive (cryptographic token manipulation, not injection). Endpoint-scoped (the token is the payload; carrier authorization_bearer, no injectable param). Phase 1 carries the captured JWT as a bearer and baseline-anchors the route (valid token accepted, broken-sig rejected); the new _jwt_send_with_token carrier drops the cookie jar so only the token is under test (Juice Shop's token cookie would otherwise mask acceptance). Fully in-band — confirmed when a forged token (alg:none strip via PyJWT, weak/known secret re-sign, RS256→HS256 confusion via a JWKS public key) is accepted like the valid baseline. Skip-tolerant: alg:none acceptance is build-dependent → a critical finding when accepted, a verification-honest non-finding when hardened. Tokens are redacted in evidence (header + claim names + <sig elided>, no secret). Distinct from #9 Weak Session IDs (which scores the JWT's entropy/flags; JWT attacks forge/tamper it).

What the trace must answer (diagnosis, not just a number)

The point of running against Juice Shop is to test the pipeline, not the isolated smoke tests (which hand each _test_* method a pre-built PageAnalysis at the canonical endpoint). The smoke tests passing proves the methodologies work given the right page; this run proves whether recon→scan→auth→plan actually delivers that page. From clinkz trace inspect <id> confirm:

  • Auth: Did a JWT session establish? WebAuthenticator now tries cookie/form auth first, then falls back to JSON/API auth (POST /rest/user/login, token → Authorization: Bearer via session_headers). Confirm the trace shows a successful JSON login and the bearer reaching scan/exploit; if the session did not establish, this is the break — but it is no longer the expected one (SPA discovery now is).
  • Discovery: Did scan surface /api/* and /rest/* routes, or only the SPA index.html shell? Angular routes aren't anchor links, but scan no longer relies on HTML crawling alone — _scan_http_service runs the _route_discovery.py discoverers (static JS-bundle parsing of main.js/chunks + OpenAPI probing) that extract the real routes with param structure. This is resolved as of a07df54b (recon delivered the :3000 HTTP service, the discoverers ran, ~138 endpoints reached planning). Only treat discovery as the break if the trace regresses to service_scans: [] / total_endpoints: 0 with no katana invocation — otherwise a missed category is a downstream methodology/param-shape issue, not a discovery gap.
  • Planning: Did _llm_plan_exploits receive real parameterized endpoints (/rest/products/search?q=, /rest/basket/:id, /redirect?to=) or just the SPA index?
  • Open Redirect specifically: expect it not found, and that is now understood (a07df54b): Juice Shop's /redirect?to= is gated by a substring allowlist that 406s every generic probe, so phase-1 candidacy correctly returns False — there is no 302 to detect. Confirming it requires defeating the allowlist (harvest the app's own allowlisted outbound links, bypass via substring), which is deferred to a dedicated fix. Do not re-diagnose this as a candidacy-signal bug.

Report the diagnosis as a table: category | found | root cause if missed | bucket (a/b/c/d). Then give the % split of what blocks coverage across the four buckets — that is the fix priority for the next prompt.

Do NOT fix issues found during the run. Measurement and diagnosis only unless the user asks for fixes.

Install via CLI
npx skills add https://github.com/ptkvaibhav/clinkz --skill run-juiceshop
Repository Details
star Stars 6
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator