PhoneVault

webxsscsp-bypassjsonpsession-hijacking

An online storefront whose developer left the full vulnerability roadmap sitting in the web root as a to-do list — and then relied on a Content Security Policy to cover the parts they hadn’t finished.

Spoiler-light notes — no flags, credentials or copy-paste exploits. Solve it yourself first.

Recon

A single web port behind a reverse proxy, reachable only by its virtual hostname. Enumeration turns up two files worth more than everything else combined:

  • A publicly readable developer notes file. It’s a to-do list, and it names three deferred security items in plain language — a reflective endpoint that isn’t validated, a review feature that renders raw markup, and an admin cookie deliberately left without HttpOnly. That file maps the entire attack chain before a single request is crafted.
  • A front-end search implementation that reveals the app uses a callback-style JSON API — the kind that wraps its response in a client-supplied function name.

The notes file is also candid about why each item was deferred. The reflective endpoint was low priority “since the CSP should prevent abuse”. It’s worth pausing on that sentence, because it’s the entire box.

The CSP, and the misconception in it

The site ships a strict, well-written policy: scripts limited to the same origin, no inline scripts, object-src 'none', base-uri 'none', frame-ancestors 'none', and connections limited to the same origin.

That policy is genuinely good. What it does not do is stop the page from loading a script from its own origin — including from an endpoint whose response body is shaped by an attacker-supplied parameter.

script-src 'self' bans other origins and inline scripts. It has nothing to say about loading a script from your own origin — that’s the origin it explicitly permits. So if any same-origin endpoint reflects attacker-controlled text in a script-shaped response, that endpoint is your script loader, and the policy is fully satisfied while arbitrary JavaScript executes.

The developer spotted that the callback-style endpoint was dangerous, then treated the CSP as a compensating control. It never was one.

The three bugs

  1. The callback parameter is reflected verbatim. The JSON API wraps its response in whatever function name you supply, with no validation or allowlist. Supplying a value that ends in a line comment neatly discards the JSON that follows, leaving a clean block of executable JavaScript.
  2. Reviews render as raw markup. Any authenticated user can post a review, and the body is inserted into the page unescaped — the comment in the template even says it’s “to support rich text formatting”.
  3. The admin’s session cookie isn’t HttpOnly — and the flag lives in that cookie. The notes file admits it was left readable so the admin dashboard’s own JavaScript could read it.

Any one of those alone is a bug. Together they’re a chain, and each one removes the obstacle the previous one creates.

The chain

  1. Authenticate. The app ships default credentials, and the notes file helpfully confirms passwords are stored in plaintext. No cracking needed.
  2. Store the payload as a review. The review body is a script tag whose source is the same-origin JSON API, with the callback parameter carrying the JavaScript we want to run. Because the source is same-origin, the CSP allows it.
  3. Have the callback do the exfiltration. The injected JavaScript reads document.cookie (which contains the admin’s flag cookie) and posts it as a review on a different product — using credentials-included so the request is authenticated, and a form-encoded body so no preflight is involved. Writing the stolen value into another product’s review gives us a second-order read channel: the exfiltrated secret comes back out through the normal application UI.
  4. Trigger the admin bot. The storefront has a “report this page” feature that sends an admin bot to review a URL. Reporting our poisoned product page is the delivery mechanism.
  5. Collect and replay. The bot loads the page, the same-origin script loads and executes, and the admin’s cookie is written into the other product’s reviews where we can read it. Replay that cookie against the admin panel and the flag is displayed.

Notice what the exfiltration doesn’t need: no outbound connection to an attacker host, no DNS, no server of our own. Everything happens within the permitted origin. That’s precisely why the CSP didn’t help.

Dead ends

  • Sending the cookie to an external collector — blocked, as the policy intends: connect-src is same-origin only, which also rules out sendBeacon to an outside host.
  • Image-based exfiltration — the policy allows HTTPS images but a plaintext cookie in a URL needs plain HTTP, which would be a downgrade the browser won’t make.
  • Hunting a JSON API that returns the flag — the non-admin routes simply fail; the secret is only ever rendered in the admin HTML panel.
  • Injection at the login — malformed bodies crash or reset the worker rather than returning anything useful.

Each of these pushes you back toward the same conclusion: exfiltrate through the application, over the channel you’re already allowed to use.

Why it worked

All three failures share one root cause: the developer evaluated each risk in isolation and assumed a different control would catch it later. The reflective endpoint was “fine because CSP”. The raw review rendering was “fine because you must be logged in”. The cookie was non-HttpOnly “for convenience”. Individually, each mitigation sounds reasonable. Stacked, they cancel out — the raw rendering gives you script execution, the reflective endpoint gives you a way to run code the CSP permits, and the readable cookie gives you the prize.

Takeaways

  • script-src 'self' does not stop same-origin script injection. Any endpoint that reflects input into a script-shaped response is a script loader. JSONP is the classic case, and CSP cannot compensate for it — validate or remove the reflective parameter.
  • Never ship a developer notes or to-do file in the web root. It collapsed hours of enumeration into a two-minute read, and it told us which mitigations the developer believed were protecting them.
  • Encode on output, in every view — internal-looking pages are exactly the ones with privileged readers.
  • HttpOnly on every session cookie. Don’t leave secrets readable from JavaScript for convenience.
  • Second-order channels work. Writing a stolen value back into the application’s own data (a review, a comment, a profile field) is a reliable read-back when outbound traffic is blocked.
  • Hash passwords. Plaintext storage turned a default-credentials issue into total account compromise.