🚨 Cross-Site Scripting (XSS) Field Guide for Red Teamers & Defenders
🕵️ Cross-Site Scripting (XSS)
XSS is not “just JavaScript in the page.” It’s a browser trust violation: attacker-controlled input becomes executable (or security-relevant) output in a victim’s browser—inside your application’s origin.
Modern apps still fall to XSS because the hardest part isn’t “filtering <script>”—it’s understanding context 🔍:
- Where does untrusted data originate? (sources)
- Where does it end up? (sinks)
- In what parsing context does the browser interpret it? (HTML, attribute, URL, JS string, CSS, DOM APIs)
🧠 Mental Model: “Input → Transformation → Context → Sink”
Think of XSS as a pipeline where one wrong join turns data into behavior.
1) Source: Untrusted data enters
Request / storage / third-party
Query params, path segments, headers, post bodies, cookies, localStorage, backend data stores, and upstream APIs can all contain attacker-controlled input.
2) Transformation: App processes data
Routing / templates / serialization
Framework rendering, string concatenation, HTML building, JSON serialization, markdown rendering, and DOM updates may change meaning or encoding.
3) Context: Browser parses output
HTML / attribute / URL / JS / CSS
The browser decides how to interpret bytes based on where they appear. This is where XSS lives.
4) Sink: Data reaches a dangerous API
DOM & rendering
Sinks like innerHTML, document.write, setAttribute, and unsafe template injection are where payloads become execution.
5) Impact: Attacker code runs
Victim browser
Session compromise, account actions, data exfiltration, and UI redress happen under your origin’s trust.
Key takeaway: XSS is context-sensitive. Encoding that is safe in one context (HTML text) can be unsafe in another (JavaScript string or URL).
🧩 The Big Three: Reflected vs Stored vs DOM-Based
| Type | Where the payload lives | Typical triggers | What defenders should watch |
|---|---|---|---|
| Reflected XSS | In the request (URL, form) and reflected in response | Victim clicks a crafted link or submits a form | Logs with suspicious parameters, 4xx/5xx spikes near templates, WAF alerts (but don’t rely on them) |
| Stored XSS | Persisted server-side (DB, CMS, tickets, profiles) | Anyone viewing the stored content triggers it | Admin views, moderation queues, rendering of rich content, file uploads that become HTML |
| DOM-based XSS | In the browser (DOM), often without server-side reflection | Client-side code reads source and writes to sink | Front-end telemetry: DOM sink usage, CSP reports, client-side error traces |
DOM-based XSS often evades server-side scanning because the server might return “safe-looking” HTML while the client-side JavaScript turns data into executable DOM.
🔥 What “Execution” Really Means (It’s Bigger Than <script>)
Browsers execute or security-impact data in many ways:
- Script execution (obvious)
- Event handlers (
on...attributes) ⚠️ - JavaScript URLs (
javascript:in certain contexts) ⚠️ - HTML injection leading to data theft (even without JS in some cases)
- DOM clobbering (changing what variables/elements mean in JS) 🧱
- Framework template expression injection (depends on framework) 🧩
So the defensive goal is not “remove bad strings.” It’s: prevent untrusted input from being interpreted as code or active content ✅
🧪 Vulnerable Patterns (Safe, Illustrative Examples)
1) Server-side template building with string concatenation (HTML context)
// ❌ Vulnerable pattern: untrusted input lands in HTML without contextual encoding
app.get('/search', (req, res) => {
const q = req.query.q || '';
res.send(
'<h1>Search</h1>' +
'<div>You searched for: ' + q + '</div>' // q is untrusted
);
});Risk: If untrusted data is injected into HTML markup, the browser may interpret it as tags/attributes—not text.
2) Dangerous DOM sink: innerHTML
// ❌ Vulnerable pattern: source -> sink in the browser
const params = new URLSearchParams(location.search);
const name = params.get('name'); // untrusted source
document.getElementById('greeting').innerHTML = "Hi, " + name; // dangerous sinkDon’t memorize “bad functions.” Learn why they’re bad: anything that causes HTML parsing or script interpretation becomes a sink.
✅ Safer Patterns: Contextual Output Encoding + Safer DOM APIs
A) Prefer text insertion APIs (HTML text context)
// ✅ Safer: treat data as text, not markup
const params = new URLSearchParams(location.search);
const name = params.get('name') ?? '';
document.getElementById('greeting').textContent = "Hi, " + name;B) If you must render HTML, use a proven sanitizer + strict allowlist
If your product requires rich text (comments, CMS), use a well-maintained HTML sanitizer with an allowlist and pair it with a strict Content Security Policy (CSP). Avoid “roll-your-own” regex filters.
Note: Sanitizer choice and safe configuration are highly context-dependent. Validate with security tests and keep it updated.
🧭 Context Cheat Sheet: Where Output Encoding Changes
This is where red teamers win and defenders lose—the same bytes mean different things.
| Context | What the browser is doing | Typical risky sinks | Primary defense |
|---|---|---|---|
| HTML text node | Treats content as text | String concatenation into templates | HTML-escape (encode) special chars |
| HTML attribute | Parses quotes/attributes; may create handlers/URLs | setAttribute with untrusted strings | Attribute-encoding + strict allowlists |
| URL context | Parses schemes, parameters; may trigger navigation | location, href, src | Allowlist schemes/domains + URL encoding |
| JavaScript string | Parses as JS code when embedded | Inline scripts, script templates | JS string escaping; avoid inline scripts |
| CSS context | Parses CSS tokens and functions | style attributes, CSS injection points | Avoid dynamic CSS; strict allowlists |
Defensive mantra: Encode on output for the exact context you’re outputting into. Validate/normalize input, but don’t rely on that alone.
🧨 Impact Map: What an XSS Can Do (Under Your Origin)
Even “small” XSS can be catastrophic because it runs in the victim’s authenticated browser context.
| Capability | What it enables | Why it matters |
|---|---|---|
| Session hijack (where cookies are accessible) | Impersonation, account takeover | If cookies are not HttpOnly or tokens are accessible in JS |
| Account actions | Change email/password, add SSH keys, initiate transfers | The browser already has the victim’s privileges |
| Data exfiltration | Read page data/DOM, scrape PII | Same-origin access to sensitive UI data |
| Privilege escalation paths | Attack admins/moderators via stored XSS | Admin UIs often have higher privileges |
| Supply-chain style reach | Pivot to internal tools in the same origin | SSO dashboards and internal portals are prime targets |
Modern mitigations (HttpOnly cookies, SameSite, CSP) reduce impact, but do not remove it. Your UI still becomes attacker-controlled.
🔍 Detection & Analysis: How to Hunt XSS Without “Exploit Chains”
A practical workflow for defenders and appsec teams:
✅ 1) Map sources & sinks (client + server)
Look for:
- URL/query parsing → DOM writes
- Template rendering with untrusted strings
- Markdown/HTML rendering paths
- “Preview” features (email previews, file previews, CMS previews)
- Admin views of user-generated content
✅ 2) Identify context at the sink
Ask:
- Is it HTML? Attribute? URL? JS? CSS?
- Is there automatic escaping by the framework?
- Are there bypasses due to double-encoding or decoding?
✅ 3) Validate controls that actually break the chain
Controls that matter:
- Contextual output encoding ✅
- DOM-safe APIs ✅
- Sanitization with allowlists ✅
- CSP with nonces/hashes ✅
- Trusted Types (where supported) ✅
- Secure cookie flags + token handling ✅
🧱 Defensive Hardening Stack (Layered, Not Single-Point)
Layer 1: Safe rendering defaults
Baseline
Use frameworks that auto-escape in templates. Avoid bypasses like “raw HTML” rendering unless strictly necessary.
Layer 2: Contextual output encoding
Core control
Encode for HTML/attribute/URL/JS contexts. Don’t reuse the wrong encoder in the wrong place.
Layer 3: DOM hardening
Front-end
Prefer textContent, setAttribute with allowlists, and avoid innerHTML/document.write. Consider Trusted Types for enforcement.
Layer 4: CSP (Content Security Policy)
Damage reduction
Use nonce- or hash-based CSP, avoid unsafe-inline, restrict script sources, and enable reporting to catch violations.
Layer 5: Session & token protections
Containment
HttpOnly + Secure + SameSite cookies, short-lived tokens, and reduce sensitive data exposure in DOM.
🛡️ Example: CSP That Aims to Reduce XSS Impact (Illustrative)
# ✅ Illustrative CSP (tune per app; test thoroughly)
Content-Security-Policy:
default-src 'none';
script-src 'self' 'nonce-<RANDOM_PER_REQUEST_NONCE>';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
report-to csp-endpoint;CSP is strongest when you avoid inline scripts (or use nonces/hashes). Treat CSP as a seatbelt, not the brakes.
🧰 “Red Team Lens” (Concepts, Not Copy‑Paste Payloads)
If you’re assessing risk, focus on these questions instead of payload trivia:
- 🧷 Can I control data that reaches a sink?
(source → sink flow) - 🧩 What is the sink’s context?
(HTML/attribute/URL/JS/CSS) - 🔁 Is the app decoding/encoding multiple times?
(double-encoding bugs) - 🧱 Are there “escape hatches”?
e.g., template helpers that render raw HTML, markdown renderers, WYSIWYG editors - 🧾 Do security headers meaningfully constrain scripts?
CSP with nonces/hashes, nounsafe-inline, restricted script hosts - 🧪 Do you have telemetry when it happens?
CSP reports, client-side logging, anomaly detection
If you find a flow that reaches a dangerous sink in an executable context, treat it as high severity—even if the “demo” seems small.
📋 Quick Triage Checklist
| Check | What you’re looking for | Why it matters |
|---|---|---|
| Auto-escaping in templates | Default escaping enabled; no raw HTML shortcuts | Prevents HTML context injection |
| DOM sink inventory | innerHTML, document.write, setAttribute, location assignments | Common execution points |
| Sanitizer usage | Allowlist-based, updated, configured correctly | Stops rich-text injection |
| CSP quality | Nonce/hash-based, no unsafe-inline, strict sources | Reduces exploitability |
| Token/cookie safety | HttpOnly, Secure, SameSite; avoid tokens in localStorage | Reduces session theft impact |
| Logging/alerting | CSP reports, client errors, suspicious input patterns | You can’t fix what you can’t see |
✅ Closing Notes (For Builders Who Want Fewer Surprises)
If you remember only one thing: XSS is a context bug. Treat untrusted data as data all the way through—encode on output, minimize dangerous sinks, and layer CSP + monitoring.
If you’re building or auditing with Agesec, prioritize:
- Inventorying sinks across the frontend codebase 📌
- Validating template escaping behaviors ✅
- Tightening CSP with reporting and incremental rollout 🧱
- Testing rich-content pipelines (markdown/WYSIWYG) with allowlists ✍️