🚨 Cross-Site Scripting (XSS) Field Guide for Red Teamers & Defenders

2/15/2026

🕵️ 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 🔍:


🧠 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

TypeWhere the payload livesTypical triggersWhat defenders should watch
Reflected XSSIn the request (URL, form) and reflected in responseVictim clicks a crafted link or submits a formLogs with suspicious parameters, 4xx/5xx spikes near templates, WAF alerts (but don’t rely on them)
Stored XSSPersisted server-side (DB, CMS, tickets, profiles)Anyone viewing the stored content triggers itAdmin views, moderation queues, rendering of rich content, file uploads that become HTML
DOM-based XSSIn the browser (DOM), often without server-side reflectionClient-side code reads source and writes to sinkFront-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:

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)

server-vuln-example.js
// ❌ 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

dom-vuln-example.js
// ❌ 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 sink
⚠️

Don’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)

dom-safe-textcontent.js
// ✅ 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.

ContextWhat the browser is doingTypical risky sinksPrimary defense
HTML text nodeTreats content as textString concatenation into templatesHTML-escape (encode) special chars
HTML attributeParses quotes/attributes; may create handlers/URLssetAttribute with untrusted stringsAttribute-encoding + strict allowlists
URL contextParses schemes, parameters; may trigger navigationlocation, href, srcAllowlist schemes/domains + URL encoding
JavaScript stringParses as JS code when embeddedInline scripts, script templatesJS string escaping; avoid inline scripts
CSS contextParses CSS tokens and functionsstyle attributes, CSS injection pointsAvoid 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.

CapabilityWhat it enablesWhy it matters
Session hijack (where cookies are accessible)Impersonation, account takeoverIf cookies are not HttpOnly or tokens are accessible in JS
Account actionsChange email/password, add SSH keys, initiate transfersThe browser already has the victim’s privileges
Data exfiltrationRead page data/DOM, scrape PIISame-origin access to sensitive UI data
Privilege escalation pathsAttack admins/moderators via stored XSSAdmin UIs often have higher privileges
Supply-chain style reachPivot to internal tools in the same originSSO 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:

✅ 2) Identify context at the sink

Ask:

✅ 3) Validate controls that actually break the chain

Controls that matter:


🧱 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)

csp-header-example.txt
# ✅ 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:

🚨

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

CheckWhat you’re looking forWhy it matters
Auto-escaping in templatesDefault escaping enabled; no raw HTML shortcutsPrevents HTML context injection
DOM sink inventoryinnerHTML, document.write, setAttribute, location assignmentsCommon execution points
Sanitizer usageAllowlist-based, updated, configured correctlyStops rich-text injection
CSP qualityNonce/hash-based, no unsafe-inline, strict sourcesReduces exploitability
Token/cookie safetyHttpOnly, Secure, SameSite; avoid tokens in localStorageReduces session theft impact
Logging/alertingCSP reports, client errors, suspicious input patternsYou 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: