DEV Community

Rocky
Rocky

Posted on

The Stored XSS That Got Downgraded to Low

A bug bounty submission comes back three days later with one line from triage: severity changed from High to Low, "PoC only demonstrates alert(1), no evidence of real impact." The finding was real. A comment field on a support ticket page stored a payload, unescaped, and rendered it on every future page load for anyone who opened that ticket, including support staff with elevated permissions. It should have been a serious bug. It got closed as low severity because the proof of concept stopped at a popup box.

alert(1) proves the browser executed attacker-controlled JavaScript. It does not prove anything happened because of it, and triage teams see hundreds of alert(1) submissions a month, most from scanners with no follow-through. What actually convinces a reviewer, and what a real attacker would actually do, is showing what that execution buys you: someone else's session.

The naive version is document.cookie, base64 it, ship it to an attacker-controlled endpoint with an img tag or a fetch call. It still works often enough to be worth trying, but a competently built session cookie is HttpOnly, invisible to JavaScript entirely, and that naive payload comes back empty. The real move from there is session riding instead of session stealing: don't read the cookie, use it. A fetch call issued from inside the victim's browser, in the victim's origin, carries their session cookie along automatically whether JavaScript can read it or not. Point that fetch at an endpoint that changes something, an email on the account, a password, an API key, and the response is proof the injected script did something the attacker could never have done directly.

The piece most people skip after that is the CSRF token. A lot of apps that got HttpOnly right also gate state-changing requests behind a token, and a script running in the page can just read that token out of the DOM before making the request, because the same-origin policy that stops an outside attacker from reading the page does not apply to a script the page is already executing. Grab the token, attach it to the fetch, and the "protected" endpoint has no way to tell the real user clicking a button from the payload doing it for them.

That's the actual chain a stored XSS report needs to make: injection point, persistence, then one concrete action taken on the victim's behalf that the attacker could not have done unauthenticated. Skip the last step and the finding reads like every other alert(1) triage sees in a day.

Codelivly's Web Application Hacking book walks through building that full chain, injection through session takeover, as part of its OWASP Top 10 coverage, and the stored XSS lab on the site gives you a live target to build the exfil and session-riding payloads against instead of just reading about them.

Top comments (0)