RedRouterRedRouter

Blog

AI XSS analysis: source, sink and browser proof

Surgical instruments on green cloth, with sparse red pixels and the article title
Cover based on a photo by Saltanat ebli, CC0 1.0

An AI XSS hunter is most useful when it explains a specific source-to-sink path and tells you what browser observation is missing. Reflection alone is not script execution. Script execution in your own local page is not proof of impact on a real program.

Start with a small, intentionally vulnerable local fixture. Compare it with a safe rendering variant. Let the model describe the code, then check its claims in a browser you control. Don't spray payloads at an application and ask the model which responses look exciting.

All examples here are for owned local training only. For bug bounty work, follow explicit asset scope, permitted test methods, account ownership, data handling and disclosure rules. No victim interaction, credential collection or remote callback is needed for this lesson.

reflection, execution and impact are different claims

A marker appearing in HTML establishes that input reached an output. It might have been encoded as text. It might appear in an inert attribute. It might be removed before rendering. To claim XSS, you need a supported path to JavaScript execution in the relevant browser context.

PortSwigger's DOM-based XSS guide, checked November 5, 2026, explains sources, sinks and browser inspection. It distinguishes rendered DOM behavior from “View source,” which doesn't include later JavaScript changes. It also notes that inserting a script element through innerHTML doesn't simply make that script execute in modern browsers.

Impact needs another step: explain the origin, affected viewer and reachable feature within what you are permitted to demonstrate. Don't jump from a marker in your own preview to account takeover or a claim about every user.

capture the exact context

Save the relevant source lines, input value, rendered DOM and browser state. Record whether the value comes from a query parameter, fragment, message event or saved application field. Then trace transformations: decoding, concatenation, sanitization and the final insertion method.

A model needs those details. A screenshot with a highlighted word doesn't show whether an application used textContent, innerHTML or a framework renderer. If source isn't available, mark it unknown rather than fabricating a flow.

Our authorized recon guide covers keeping collection bounded. For this article, collection is just a local fixture and its browser behavior.

a deliberately unsafe local fixture

Save the following as xss-fixture.html and open it only as a local training page. Don't deploy it publicly. The textarea confines the supplied text to a local manual input; the unsafe button deliberately parses it as HTML.

<!doctype html>
<meta charset="utf-8">
<title>Local-only rendering fixture</title>
<textarea id="input" aria-label="Local test input"></textarea>
<button id="unsafe">Unsafe HTML render</button>
<button id="safe">Safe text render</button>
<button id="reset">Reset fixture</button>
<div id="output"></div>
<script>
const input = document.querySelector('#input');
const output = document.querySelector('#output');
document.querySelector('#unsafe').onclick = () => {
  output.innerHTML = input.value; // intentionally unsafe local sink
};
document.querySelector('#safe').onclick = () => {
  output.textContent = input.value; // renders the same value as text
};
document.querySelector('#reset').onclick = () => {
  output.replaceChildren();
  delete document.documentElement.dataset.fixtureExecuted;
};
</script>

Begin with an ordinary marker such as LOCAL-MARKER-01. Both variants should display it. That alone says nothing about execution.

For the owned local fixture only, this harmless HTML input changes a DOM marker when you click the inserted button:

<button onclick="document.documentElement.dataset.fixtureExecuted='yes'">
  Local marker only
</button>

Click Unsafe HTML render, then the inserted Local marker only button. Inspect document.documentElement.dataset.fixtureExecuted in developer tools. Reset, choose Safe text render with the same input, and check that it appears as literal text rather than creating a clickable element.

This proof requires a deliberate click on your own local element. It does not read cookies, contact another host or interact with another person's account. It demonstrates a rendering difference, not a deployed vulnerability or an automatic exploit path.

ask for a source-to-sink explanation

Review this local fixture only. Identify the input source, transformations and exact unsafe and safe sink lines. Separate code-based reasoning from browser-observed execution. State the required user interaction and what evidence is missing. Do not claim account takeover, generate remote callbacks or suggest tests on other assets.

The expected explanation is narrow: input.value flows into output.innerHTML in the unsafe handler. The safe handler assigns that value to textContent. The fixture has no sanitizer. The inserted event handler is executable only if the browser creates the HTML element and its event runs.

Reject an explanation that says a model “observed” the browser when it only received source text. A plausible execution path is still a hypothesis until the relevant behavior is recorded.

use a test plan with negative controls

Local testExpected observationWhat it establishes
Plain marker through both buttonsText visibleInput reaches output
HTML button through unsafe renderNew button element existsHTML was parsed
Click inserted local buttonRoot data marker becomes yesLocal event handler executed
Reset, same input through safe renderLiteral markup; no new buttonText insertion doesn't parse that markup
Fresh load, no interactionNo execution markerNo claim of automatic execution

This is a manual browser test plan, not a claim of a completed browser run. The fixture was saved with the editorial artifacts, but no browser execution was performed for this draft. Run and capture the plan before describing it as tested in a release note.

keep real application checks bounded

On an authorized program, preserve the exact origin, route, account role, browser version and interaction sequence. Check redirects and state changes within the approved scope. Stop rather than follow a redirect to an unapproved origin.

Sanitization may remove the executable part while preserving a harmless marker. A content-security policy may block the observed execution route. Self-only behavior may not cross a meaningful security boundary. These are reasons to describe limitations, not to produce a larger bypass catalog.

YesWeHack's July 17, 2026 Claude Code lab report illustrates the gap between identifying a DOM weakness and completing validation. It is a bounded lab account, not a guarantee about your model or target.

write the report from browser evidence

A compact report states the source, sink, origin, required interaction, expected rendering, actual behavior and supporting artifacts. Link each observation to an evidence ID. HackerOne's Quality Reports guidance, February 5, 2025, supports reproduction steps and expected versus actual behavior; avoid importing speculative impact from generic examples.

For permitted model review, use API setup and check the selected model's capabilities. Text-only models need source text, not an assumption that they inspected a screenshot. Zero data retention is opt-in, off by default and free on monthly plans; read data handling first.

What we still don't know: no browser result or real vulnerability is claimed here. The fixture teaches how to distinguish reflection, HTML parsing and local execution. The program decides eligibility, and a human verifies and submits any real report.