Quick answer
Secondary context XSS arises when the input passes through two parsing pipelines: the first escapes what it understands as dangerous, the second re-interprets what the first ignored. Markdown renderers, BBCode parsers, custom template engines, latex-to-HTML, SVG inline serialization — all are secondary contexts where the "safe" characters of the primary become payloads. The trick: find the delta between what the sanitizer looks at and what the render ultimately emits.
The threat model
Typical vulnerable pipeline:
User input
↓
[Primary parser] ← escapa <, >, " según context HTML
↓
[Secondary parser] ← markdown / BBCode / template engine → emite NUEVO HTML
↓
DOM render ← ejecuta el HTML resultante
The sanitizer is usually applied at the primary or at the final output. If it's only applied at the primary, the secondary has freedom to emit whatever it wants. If it's applied at the final output, the sanitizer is protected — but only if it knows all the possible outputs of the secondary.
Markdown — the #1 secondary context
Markdown→HTML is the most exploited case. CommonMark/markdown-it/marked/showdown all have different behaviors in edge cases.
Basic vector — link without sanitize
[click me](javascript:alert(1))
Some parsers emit:
<a href="javascript:alert(1)">click me</a>
Typical solution: the parser blocks javascript:. But:
[click me](javascript:alert(1))
[click me](javascript:alert(1))
[click me](java	script:alert(1))
[click me](java%0ascript:alert(1))
The CommonMark spec says "preserve the URL as-is, decode HTML entities at render time". Some parsers decode, others don't. The inconsistency is the bypass.
Vector — raw inline HTML
By default most parsers allow raw HTML mixed with markdown:
Hola
<img src=x onerror=alert(1)>
mundo
Common mitigation: pass the output through DOMPurify. But DOMPurify works on the final HTML, not on markdown. If the parser emits valid HTML that DOMPurify accepts + a side effect, the chain passes.
Vector — references and autolinks
[click][autolink]
[autolink]: javascript:alert(1)
Reference-style links. The parser may decode the destination later, after the sanitize.
Vector — code blocks that escape to the render
Markdown allows a lang hint in code blocks. A typical payload is an "html" block with a payload inside:
```html
<img src=x onerror=alert(1)>
```
If the renderer passes the code block through a syntax highlighter (Prism, highlight.js, Shiki) that emits "structured" HTML, the output's sanitizer may assume it's inside <pre><code> and allow tags... and some highlighters serialize incorrectly.
Keep reading the full chain
The remaining part includes the step-by-step PoC, exploitation code and the full chain that led to impact. Available to subscribers.
Practice this in a lab
Secondary Context
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
Stored XSS in template names — from the most boring field to domain takeover
A template title field, unsanitized, in a session with permissions over domains. A real €1,200 bounty. How to find XSS where nobody looks.
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.
postMessage — common vulnerabilities: origin bypass, XSS sink, cross-window IDOR
How to identify and exploit vulnerabilities in window.postMessage(): listeners without origin validation, insecure JSON payloads that reach DOM XSS, cross-origin IDOR.