XSS Testing
Cross-Site Scripting (XSS) lets an attacker run scripts in another user's browser with your application's trust. Learn the three types of XSS, how to test them safely in sandboxes, and the layered defenses — output encoding, sanitization and Content Security Policy — that stop them.
What is this?
Cross-Site Scripting (XSS) is a client-side injection flaw that lets an attacker inject executable scripts into pages other users view. Because the script runs in the victim's browser, it inherits the application's origin: it can read cookies, steal tokens, rewrite the page and act as the victim. XSS happens when untrusted data reaches the browser without being neutralized in the correct context.
Why do you need it?
- It appears in every OWASP Top 10 and is one of the most commonly reported web vulnerabilities.
- A single stored XSS on a page admins visit can lead to full account takeover across the whole team.
- Modern single-page apps remain vulnerable when frameworks render user data without escaping.
- Defense is cheap: output encoding and a strict Content Security Policy stop most payloads.
- It is easy to practice safely in authorized sandboxes, so every developer can learn it hands-on.
Mental model
A letter delivered to your mailbox stamped with your bank's logo
An XSS payload
The mail arrives through a channel you trust, so you open it and follow its instructions. XSS delivers attacker-controlled code through your trusted application, so the browser executes it with the full privileges of that page.
How it works
- The application accepts user input and later reflects it (reflected XSS) or stores it for others (stored XSS) without proper encoding.
- The attacker crafts a payload such as a <script> block or an HTML event handler like onerror.
- The victim's browser parses the injected markup as executable code in the application's origin.
- The script runs and can read cookies, call APIs, exfiltrate data or mount further attacks such as CSRF.
- Prevention: encode output for the right context, sanitize with a library when HTML is required, and set a strict CSP as defense-in-depth.
How a stored XSS payload reaches every visitor.
Key commands
curl -sI https://sandbox.target.local/ | grep -i content-security-policyCheck whether a Content-Security-Policy header exists before you test further.
Content-Security-Policy: default-src 'self'; script-src 'self'
curl -s "https://sandbox.target.local/search?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" | grep -i scriptEducational check: see whether the encoded payload is echoed back unescaped in the response body.
curl -s "https://sandbox.target.local/profile?name=%3Cimg%20src=x%20onerror=alert(1)%3E"Educational event-handler variant that does not rely on a <script> tag being allowed.
grep -rn "dangerouslySetInnerHTML" src/Audit the codebase for React sinks that bypass automatic escaping.
grep -rnE "innerHTML|document.write|eval\(" src/ public/Find legacy DOM sinks that can become DOM-XSS injection points.
Examples
Escaping output and a strict Content Security Policy
import React from "react" // VULNERABLE pattern — never render user HTML with dangerouslySetInnerHTML:// <div dangerouslySetInnerHTML={{ __html: userPost }} /> // SAFE: let the framework escape text automatically in every context.function Post({ author, body }) { return ( <article> <h2>{author}</h2> <p>{body}</p> </article> )} // SAFE: a strict CSP on every response (Express example).app.use((req, res, next) => { res.setHeader( "Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" ) next()})React auto-escapes {author} and {body}, so text and attribute injection are neutralized without any extra work — the vulnerability appears only when developers bypass escaping with dangerouslySetInnerHTML or similar sinks. The CSP then acts as a second layer: even if a payload survives encoding, script-src 'self' and object-src 'none' stop remote scripts and plugins from executing. The corresponding educational payload — a reflected <script>alert(document.cookie)</script> echo in a search box — is covered in the WebGoat lab, which is the only place you should run it.
In production
- A strict CSP is deployed with every frontend bundle and validated by a header test in CI.
- Framework auto-escaping is the default, and dangerouslySetInnerHTML is banned by a lint rule.
- HTML sanitization with DOMPurify is used only where rich text is genuinely required.
- Stored-XSS surfaces — comments, profiles, admin panels — are covered by automated DAST checks.
Best practices
- Encode output for the correct context: HTML text, HTML attributes, URLs, CSS and JavaScript each need their own encoder.
- Default to framework auto-escaping and never disable it without a documented, reviewed exception.
- Set Content-Security-Policy with script-src 'self' and no unsafe-inline or unsafe-eval.
- Sanitize with a maintained library like DOMPurify when rich HTML input is required.
- Test with realistic payloads in an authorized sandbox and confirm the fix in a real browser.
Common mistakes
- Testing with alert(1), which frameworks and WAFs special-case, instead of realistic payloads.
- Confusing escaping with sanitization — escaping alone does not make attacker HTML safe to inject as markup.
- Testing only reflected XSS and missing stored XSS in comments and profile fields.
- Assuming CSP alone is a complete fix instead of treating it as defense-in-depth behind encoding.
Common issues
The payload appears as text but does not executeThe injection point sits in a context the payload does not match, such as inside a JavaScript string.
Adjust the payload to the surrounding context (attribute, script or URL) instead of assuming HTML context.
CSP blocked the payloadA content security policy is present and doing its job.
Confirm the script-src and object-src directives, and test the encoding separately from the CSP.
Payload works in curl but not in a browserURL encoding differences or the browser blocking inline handlers.
Use proper URL encoding and test event handlers in a real browser, not just the raw response.
Debugging tips
- Open the browser devtools console to see exactly what the server returned around the injection point.
- Inspect the raw HTTP response to determine whether the sink is reflected, stored or DOM-based.
- Validate payload syntax in a sandbox such as PortSwigger Academy before testing a real application.
Interview checkpoint
Hands-on lab
- Find a reflected XSS sink and confirm it with a safe payload
- Find a stored XSS sink that persists for a second user
- Demonstrate that framework escaping and a CSP neutralize both
- Document the CWE mapping and the defensive fix
Reflected & Stored XSS in WebGoat
40 min · intermediate
Quick quiz
Which control is the most direct defense-in-depth layer for preventing XSS script execution?
React automatically escapes text interpolation, so {body} in a <p> element is safe by default.
A payload executes on /search but not on /profile, even though both echo the query string. What is the most likely explanation?
Cheat sheet
{value}Framework auto-escaping (default)dangerouslySetInnerHTMLBanned by lint — requires sanitizationDOMPurify.sanitize()Allow-listed HTML sanitizationscript-src 'self'CSP: no remote or inline scriptsobject-src 'none'CSP: block plugins/flashinnerHTML / document.writeDOM sinks to audit in legacy code<script>alert(1)</script>Basic reflected marker<img src=x onerror=alert(1)>Works without script tags'-alert(1)-'Breakout in a JS string context<svg onload=alert(1)>SVG-based execution vector