GUIDE / security
XSS Testing: Finding Cross-Site Scripting
Learn XSS testing to find reflected, stored, and DOM XSS with payloads, encoding checks, CSP tests, and a practical QA security checklist now.
XSS testing is one of the highest-value security skills a QA engineer can build. Cross-site scripting lets an attacker run script in a victim's browser under your application's origin. That can steal sessions, rewrite pages, force actions, phish users, or open deeper attacks. You do not need to be a full-time penetration tester to find many XSS bugs. You need a method, a map of input and output, careful payloads, and a habit of checking every rendering context.
This guide is a practical XSS testing playbook for QA. You will learn how to distinguish reflected, stored, and DOM-based XSS, how to probe forms and URLs safely, how to use XSS test payloads for input fields, how to verify output encoding, how to test Content Security Policy, and how to write defects developers can fix. You will also get a DOM-based XSS testing checklist, comparison tables, worked examples, and the common mistakes that hide real issues.
If you want tool support while you practice, pair this guide with the Burp Suite tutorial for beginners. For access control bugs that often sit next to XSS, read authentication and authorization testing.
What Is XSS and Why QA Should Care
Cross-site scripting (XSS) happens when untrusted data is included in a web page without safe handling, and the browser treats part of that data as code. The classic example is a comment field that stores <script>alert(1)</script> and later shows it to every reader as live script. Modern apps are more complex: single-page applications, JSON APIs, template engines, rich text editors, and client-side routers create many places where data can become code.
XSS matters to QA for three reasons:
- User impact is direct. Victims are real users, support agents, or admins viewing attacker-controlled content.
- It is often easy to find. Many XSS bugs appear during ordinary feature testing if you deliberately use special characters.
- It is easy to miss with happy-path testing. Teams that only type names and emails never trigger the dangerous paths.
XSS is not only about stealing cookies. Modern browsers may mark cookies HttpOnly, which reduces cookie theft. Attackers still use XSS for:
- Session or token theft from JavaScript-accessible storage.
- Forced actions as the logged-in user.
- Defacement and fake UI overlays.
- Keylogging or form harvesting on sensitive pages.
- Pivoting into admin tools when privileged users view malicious content.
The Three XSS Types You Must Separate
| Type | Where the payload lives | Who gets hit | Typical entry points | Why it is dangerous |
|---|---|---|---|---|
| Reflected XSS | In the request, echoed in the immediate response | The user who clicks a crafted link or submits a crafted form | Search boxes, error messages, query params, headers | Easy to weaponize via phishing links |
| Stored XSS | In the database or other persistent store | Other users who later view the content | Comments, profiles, tickets, chat, file names | Scales to many victims without extra clicks each time |
| DOM XSS | In client-side JavaScript sources and sinks | Users whose browser executes the unsafe client code | URL fragments, query params read by JS, postMessage, localStorage | Can bypass some server-side filters entirely |
Keep these labels in your bug reports. Developers fix each type differently. Reflected issues often need response encoding on a specific page. Stored issues need encoding at render time plus safer storage rules for rich text. DOM issues need safer JavaScript APIs and source-to-sink hardening.
Reflected XSS in Plain Language
The application takes input from the request and reflects it into the response without safe encoding. Example flow:
- Attacker crafts
https://app.example/search?q=<payload>. - Victim opens the link.
- The search page shows:
Results for <payload>. - The browser executes the payload.
Stored XSS in Plain Language
The application saves untrusted content, then later renders it to users. Example flow:
- Attacker posts a support ticket subject with a payload.
- Support agent opens the ticket list or ticket detail page.
- The subject renders as HTML or script.
- The agent browser executes attacker code while authenticated as support.
Stored XSS is often higher severity because privileged staff are common victims.
DOM XSS in Plain Language
The server may never echo the payload as HTML. Client-side code does the damage:
// Dangerous pattern
const name = new URLSearchParams(location.search).get("name");
document.getElementById("welcome").innerHTML = "Hello " + name;
If name is <img src=x onerror=alert(1)>, the page still becomes executable without a classic server reflection.
XSS Testing Mindset Before You Fire Payloads
Good XSS testing is not random script spam. Use a structured approach.
Step 1: Get Authorization and Scope
Only test systems you are allowed to test. Prefer dedicated staging. Confirm:
- Target hosts and environments.
- Accounts and roles you may use.
- Whether automated scanners are allowed.
- How to report findings.
- What not to touch (production users, payment rails, live PII).
Step 2: Map Inputs and Outputs
Build a simple inventory:
- Query parameters and path parameters.
- Form fields and file uploads.
- Headers that are reflected (User-Agent, Referer, custom headers).
- JSON fields in APIs that later appear in UI.
- WebSocket or chat messages.
- Error pages and validation messages.
- Emails, PDFs, exports, and admin reports generated from user data.
For each input, ask: where does this value reappear? Immediately? Later? In another role's UI? In logs shown in an admin console?
Step 3: Identify Context
Encoding that is safe in one context can be unsafe in another.
| Context | Dangerous if untrusted data is placed as | Safer direction |
|---|---|---|
| HTML body | Raw HTML/script | HTML entity encode |
| HTML attribute | Quote breakout + event handler | Attribute encode, strict quoting |
| JavaScript string | Quote breakout into code | JS encode, avoid building JS from strings |
| URL | javascript: or dangerous scheme | URL encode, allowlist schemes |
| CSS | Expression-like or URL injection | Avoid untrusted CSS, strict allowlists |
| Rich text | Allowed tags/attributes abused | Sanitize with a maintained library |
Your job as a tester is to discover the context, then choose payloads that fit that context.
Step 4: Prove Impact Safely
In staging, a simple alert('XSS') or a DOM marker is enough for many teams. Prefer:
- Unique canary strings first (
qabattle_xss_12345). - Then minimal proof payloads.
- Then impact notes: which cookie or token is accessible, which role is affected, whether the payload persists.
Never use real malware, cryptominers, or data exfiltration to third-party sites you do not control.
How to Test for Cross-Site Scripting Step by Step
1. Start With Reflection Detection
Before complex payloads, inject a unique marker:
qabattle_marker_91827
Submit it in each field and parameter. Search the response and the rendered DOM for that exact string. If it appears, note:
- Exact location in HTML.
- Whether quotes or brackets are preserved.
- Whether the value appears multiple times.
- Whether it appears only for the submitter or for other users later.
Reflection alone is not XSS. Reflection plus a context that can become code is the path to a finding.
2. Escalate With Context-Aware Payloads
Use XSS test payloads for input fields that match what you observed.
HTML body context probes:
<script>alert('xss')</script>
"><script>alert('xss')</script>
<img src=x onerror=alert('xss')>
<svg onload=alert('xss')>
Attribute context probes:
" onmouseover="alert('xss')
' autofocus onfocus=alert('xss') x='
URL or href context probes:
javascript:alert('xss')
"><a href="javascript:alert(1)">x</a>
Template or framework escape probes (examples only):
{{constructor.constructor('alert(1)')()}}
${alert('xss')}
Not every payload will apply to every stack. The point is controlled escalation, not a giant unreviewed list pasted into production.
3. Test Stored Paths With Two Accounts
Stored XSS needs dual-role testing:
- Account A submits the payload in a profile bio, comment, or ticket.
- Account B (or admin) views the content.
- Observe whether B's browser executes the payload.
- Check list views, detail views, notifications, search indexes, and export screens.
Many teams only re-open the page as the same user and miss cross-user impact.
4. Run a DOM-Based XSS Testing Checklist
Use this checklist whenever client-side rendering is involved.
| Check | What to do | Pass signal |
|---|---|---|
| Source map | List untrusted sources: location, document.referrer, postMessage, storage, editable DOM | Inventory exists |
| Sink map | Search for innerHTML, outerHTML, document.write, eval, setTimeout(string), dangerous jQuery APIs | Inventory exists |
| URL fragment tests | Put markers and payloads after # | No execution, safe text only |
| Query param JS reads | Payload in params consumed only by frontend code | Encoded or assigned to textContent |
| postMessage | Send untrusted messages to open handlers | Origin checks and schema validation |
| Client templates | Inject into client-rendered components | No HTML interpretation of untrusted strings |
| Framework bypasses | Test v-html, dangerouslySetInnerHTML, raw filters | Not used for untrusted data |
| Third-party widgets | Chat, markdown previews, analytics callbacks | Isolated and sanitized |
A practical DOM XSS workflow:
1. Open DevTools Sources and search for innerHTML, document.write, eval.
2. Note functions that read location.search or location.hash.
3. Place a marker in the URL and breakpoint on the sink.
4. Confirm whether your marker reaches the sink unencoded.
5. Replace the marker with a minimal event-handler payload.
6. Record the exact source, sink, and reproduction URL.
5. Test File Names, Metadata, and Secondary Channels
XSS is not only web forms:
- Upload
"><img src=x onerror=alert(1)>.pngand view the file list. - Put payloads in image EXIF fields if the app displays metadata.
- Use payloads in CSV imports that later render in HTML tables.
- Check PDF or HTML exports generated from user content.
- Inspect email templates that include user-provided names.
Secondary channels are common in admin tools and back-office portals.
6. Test Encoding and Filters, Not Just Blocks
If the app strips <script>, do not stop. Try:
- Case variation:
<ScRiPt>. - Nested tags if a filter is naive:
<scr<script>ipt>. - Event handlers without script tags.
- SVG, MathML, or uncommon tags.
- Encoded forms: HTML entities, URL encoding, double encoding.
- Unicode tricks only when relevant and authorized.
Your goal is not to bypass every WAF for sport. Your goal is to show whether application-level output handling is robust.
Output Encoding: How Testers Verify the Fix
When a developer says "we encoded the output," retest like a skeptic.
What Good Looks Like
If you enter <script>alert(1)</script> into a comment, the page source or DOM should show encoded content such as:
<script>alert(1)</script>
The user may still see angle brackets as text. The browser must not execute script.
Verification Steps
- Replay the original payload exactly.
- Replay nearby variants (attribute breakouts, event handlers).
- Confirm all render locations: list, detail, notification bell, email, export.
- Check both HTML source and runtime DOM after client rendering.
- Confirm the fix did not break legitimate content (for example, math formulas or intentional markdown).
Context Mistakes After Fixes
A common false fix is encoding for HTML body while the same value is also injected into a JavaScript string:
<script>
var search = "USER_INPUT";
</script>
If USER_INPUT is "; alert(1);//, HTML entity encoding may not be the right control for that context. Report the context precisely.
Content Security Policy Testing for XSS
Content Security Policy (CSP) is a browser-enforced allowlist for resources such as scripts. It can reduce XSS impact when implemented well. It can also give false confidence when implemented poorly.
What QA Should Check
| CSP question | Why it matters |
|---|---|
| Is a CSP header or meta policy present on sensitive pages? | Missing CSP removes a useful safety net |
Does the policy include unsafe-inline for scripts? | Inline attacker scripts may still run |
| Are nonces or hashes used correctly? | Broken nonces can break apps or weaken policy |
Is unsafe-eval present? | Expands the attack surface for DOM XSS |
| Are wildcards or overly broad CDNs allowed? | Script injection via trusted hosts becomes more likely |
| Is there a report-only policy first? | Helps teams adopt CSP without silent breakage |
| Do legitimate features still work? | Over-strict CSP can break analytics, editors, or payment widgets |
Practical CSP Test Cases
- Load key pages and inspect
Content-Security-PolicyandContent-Security-Policy-Report-Only. - Confirm whether inline script from a reflected payload is blocked.
- Confirm the application still loads its own scripts.
- Trigger a deliberate violation in staging and check reporting if enabled.
- Note pages that inherit no policy because of inconsistent middleware.
CSP is defense in depth. A strong CSP does not excuse missing output encoding. Weak CSP does not mean the app is doomed if encoding is solid. Test both.
XSS Testing With Common QA Tools
You can find many issues with browser DevTools alone. For request control and payload iteration, use an intercepting proxy.
High-value Burp (or similar) workflows for XSS:
- Intercept a form submit and inject payloads into fields the UI hides.
- Use Repeater to iterate encodings quickly.
- Compare raw response body with rendered browser behavior.
- Test headers and cookies that the UI cannot edit.
See the full Burp Suite tutorial for proxy setup, Repeater, and safe Intruder use.
For API-driven UIs, also test the API contract path:
- Send payloads in JSON fields.
- Confirm storage.
- Confirm UI rendering for every consumer of that field.
That pairs well with API authentication testing when tokens and role-specific views change what content is shown.
Worked Example: Search Box Reflected XSS
Requirement sketch:
The storefront search page shows the user's query in a heading: "Results for {query}".
Test Design
| ID | Case | Data | Expected secure behavior |
|---|---|---|---|
| XSS-S-01 | Marker reflection | qabattle_marker | Marker may appear as text |
| XSS-S-02 | Script tag | <script>alert(1)</script> | No execution; encoded or stripped safely |
| XSS-S-03 | Image handler | <img src=x onerror=alert(1)> | No execution |
| XSS-S-04 | Attribute breakout if query also appears in value attributes | " onfocus=alert(1) x=" | No execution |
| XSS-S-05 | Encoded payload | %3Cscript%3Ealert(1)%3C/script%3E | Decoded safely as text, not script |
| XSS-S-06 | Long payload | 2k-4k mixed meta characters | No crash, no execution, stable UI |
Reproduction Notes for a Finding
If XSS-S-03 fires:
URL: https://staging.shop.example/search?q=<img src=x onerror=alert(1)>
Role: anonymous and authenticated buyer
Browser: latest Chromium
Result: alert executes when search results render
Impact: reflected XSS in search results heading
Suggested fix direction: HTML-encode query when rendering; avoid innerHTML for this value
Write the defect with clear evidence. If you need a reusable bug format, use the guide on how to write a bug report.
Worked Example: Stored XSS in Support Tickets
- Log in as a standard user.
- Open "Create ticket".
- Set subject to
<img src=x onerror=alert('stored')>. - Submit.
- Log in as support agent.
- Open the queue and the ticket detail page.
- Observe execution on list, detail, or browser tab title if subject is reflected there too.
Also check:
- Email notifications to agents.
- Slack or webhook previews if integrated in staging.
- Search highlighting of the subject.
Stored XSS in internal tools is frequently critical because agents have elevated data access.
Automation and Regression for XSS
You cannot automate every XSS idea, but you can regress known sinks.
Ideas for automation:
- Contract tests that assert API responses encode or sanitize fields used in HTML.
- UI tests that submit known safe canaries and assert
textContentrather than executable nodes. - Security unit tests around sanitizer helpers.
- CSP header presence checks in synthetic monitors.
Keep a small curated payload pack for staging pipelines. Do not dump huge attack lists into shared environments without rate and data controls.
Severity and Risk Rating for QA
Not all XSS is equal. When you file a bug, include:
- Type: reflected, stored, DOM.
- Auth requirement: none, user, admin victim.
- Self-XSS only vs cross-user.
- Session impact: HttpOnly cookies, token in localStorage, CSRF pairing.
- Blast radius: one page vs site-wide template.
- User interaction: click required vs automatic on load.
Example ratings (adapt to your program):
| Scenario | Typical severity |
|---|---|
| Stored XSS in admin ticket subject, executes on open | Critical / High |
| Reflected XSS on public search, no auth | High |
| DOM XSS in forgotten fragment handler on marketing page | Medium / High |
| Self-XSS only in a profile preview that nobody else sees | Low / Informational, still fix |
Self-XSS can still matter if social engineering is realistic, but be honest in severity.
Common Mistakes in XSS Testing
1. Only Testing the Happy Field
Teams test the comment body and ignore title, display name, avatar URL, custom status text, and file name. Attackers do not ignore those fields.
2. Stopping at <script> Filters
If <script> is blocked, many apps still allow event handlers or dangerous URLs. Continue with context-appropriate probes.
3. Ignoring Client-Side Rendering
A response that looks encoded in "View Source" can still become dangerous after JavaScript inserts it with innerHTML. Always verify the live DOM.
4. Single-Role Testing for Stored Issues
Submitting and viewing as the same user underestimates impact. Use a second role.
5. No Context in the Bug Report
"XSS in profile" is weak. Name the parameter, the page, the DOM sink, the browser, and the payload that proves execution.
6. Testing Only Chromium
Most findings reproduce broadly, but markup parsing can differ. Spot-check Safari and Firefox for important apps.
7. Confusing Encoding Bugs With Feature Requests
Rich text editors may intentionally allow some HTML. In that case, test the sanitizer allowlist: scripts, handlers, javascript: URLs, and nested exploits. The product decision may allow bold text while still forbidding active content.
8. Running Aggressive Scanners on Shared Staging
Noisy scanners can wipe data, lock accounts, or pollute analytics. Coordinate and scope them.
9. Forgetting Downstream Renderers
Exports, PDFs, notification emails, and mobile WebViews may render the same data differently from the main web app.
10. Treating CSP as a Full Fix
CSP helps. Output encoding and safe JS patterns remain mandatory.
XSS Testing Checklist You Can Reuse
Scope and authorization confirmed
Input inventory complete (UI, API, headers, files, secondary channels)
Output inventory complete (who sees the data, where, when)
Marker reflection tested per input
Context identified (HTML, attribute, JS, URL, CSS, rich text)
Reflected XSS probes completed
Stored XSS dual-account tests completed
DOM source/sink review completed
Encoding verification after fixes
CSP presence and strength reviewed
Bug reports include payload, URL, role, impact, evidence
Regression cases added for confirmed sinks
How XSS Fits a Broader Security Testing Habit
XSS rarely lives alone. While you test injection points, you will often notice:
- Missing authorization on the endpoint that stores content.
- Session cookies without secure flags.
- Admin pages without CSRF protections.
- Verbose error messages that aid attacks.
Stay focused enough to finish XSS coverage, but log adjacent risks. Access control deserves its own method; start with authentication and authorization testing.
If you want deliberate practice with real scenarios, join a security-focused challenge in QABattle battles or create an account to track progress across testing tracks.
What Good Looks Like on a Team
Mature teams treat XSS testing as a normal quality activity:
- Every user-generated content feature has explicit security test cases.
- Sanitizer and encoding helpers have unit tests.
- CI checks for dangerous JS APIs in new code where feasible.
- Security findings use the same defect workflow as functional bugs, with clear severity.
- Developers and QA share a short internal wiki of approved proof payloads for staging.
You do not need a huge program to start. Begin with one high-risk surface: search, comments, tickets, or profile fields. Expand from there.
Final Takeaway
XSS testing rewards curiosity and structure. Map inputs and outputs, identify context, prove impact with safe payloads, verify encoding and CSP, and write precise reports. Separate reflected, stored, and DOM cases so fixes land in the right layer. Avoid payload spam without observation, and avoid trusting a single filter message as proof of safety.
If you practice this method on every feature that accepts free text, you will catch issues that pure functional testing never sees. That is the real goal: not collecting alerts, but protecting users and privileged operators from untrusted data that pretends to be code.
FAQ
Questions testers ask
How do you test for cross-site scripting (XSS)?
Map every place user input is accepted, stored, or rendered. Inject safe proof-of-concept payloads into forms, URLs, headers, and APIs, then check whether script or markup executes in the browser. Separate reflected, stored, and DOM XSS, and verify encoding and CSP after fixes land.
What is the difference between stored, reflected, and DOM XSS?
Reflected XSS echoes attacker input in an immediate response. Stored XSS saves the payload and later serves it to other users. DOM XSS never needs a server-side reflection: client-side JavaScript reads an untrusted source and writes it into a dangerous sink such as innerHTML.
How do you verify output encoding fixes XSS?
Re-run the original payload and nearby variants after the fix. Confirm the page shows encoded characters instead of executable markup, and that the browser does not fire script. Also check alternate contexts: HTML body, attributes, JavaScript strings, URLs, and CSS where encoding rules differ.
What are good XSS test payloads for input fields?
Start with simple probes such as a unique marker string, then escalate to harmless script tags, event handlers, SVG or image onerror attributes, and context-specific breakouts. Prefer proof payloads that alert or change DOM state only in staging. Never run destructive scripts against production.
How does Content Security Policy help with XSS?
CSP can block inline scripts, restrict script sources, and report violations. Testing CSP means checking policy presence, unsafe-inline usage, nonce or hash correctness, report endpoints, and whether legitimate app scripts still load. CSP is defense in depth, not a substitute for output encoding.
Is XSS testing safe for QA to do without a security team?
Yes, when limited to authorized staging or test environments with safe payloads and clear rules of engagement. Avoid production, avoid destructive payloads, and escalate complex findings to security engineers. Document steps so developers can reproduce and verify fixes.
RELATED GUIDES
Continue the route
Burp Suite Tutorial for Beginners
Burp Suite tutorial for beginners: proxy setup for QA, intercept HTTP requests, Repeater and Intruder basics, and OWASP-style web testing workflows.
Authentication and Authorization Testing
Learn authentication and authorization testing: RBAC, IDOR, session management, vertical privilege escalation, and broken access control test cases.
API Authentication Testing: OAuth, JWT, and API Keys
Learn API authentication testing for OAuth 2.0, JWT expiry and refresh flows, API keys vs bearer tokens, and broken authentication test cases.
Bug Report Template: How to Write a Great Defect Report
Learn how to write a bug report with a clear template, steps to reproduce, severity vs priority, expected vs actual results, and examples developers trust.