← Vulnerability class · WebApp Security

Cross-Site Scripting (XSS)

How untrusted input reaching a page without output encoding lets an attacker run script in another user's browser, and why that differs from SQL injection despite the surface similarity.

Typical ceiling: HighA03:2021 – InjectionCWE-79
Ask Synthreat AI about Cross-Site Scripting (XSS) ✦Practice this on PortSwigger's Web Security Academy →

Definition

Cross-site scripting, almost always shortened to XSS, happens when untrusted input ends up rendered into a page’s HTML, or into JavaScript that runs on that page, without being properly encoded for the context it lands in. When that happens, the victim’s browser can no longer tell the difference between “text the page is displaying” and “code the page wants to run,” and it executes the attacker’s script exactly as if it were the site’s own code, with the site’s own permissions.

The Trust Boundary That Breaks

The developer’s trust assumption is almost always: this data is just going to be displayed back to someone, so it’s inert text. A comment, a display name, a search term echoed back on a results page: none of it looks like code to the person writing the feature, so it doesn’t occur to them that it needs to be treated any differently from a static string in the template.

The actual trust boundary that has to hold is the line between data and code at the moment a page renders: every place untrusted input is written into HTML has to be encoded for that specific context (HTML body text is not the same encoding as an HTML attribute, which is not the same encoding as a JavaScript string). This is conceptually the same failure as SQL Injection: a boundary between data and code gets crossed because the application never actually drew it. The difference is where it happens: SQL injection crosses that line at the database’s SQL parser; XSS crosses it at the browser’s HTML and JavaScript parser instead. Same root cause, different parser on the other side of the mistake.

Where It Actually Shows Up

Why It Keeps Happening

Modern templating frameworks auto-escape output by default, which prevents most XSS automatically, but nearly all of them also expose a deliberate “raw” or “unescaped” output function for the rare legitimate case where a developer actually needs to render real HTML. Under deadline pressure, that escape hatch gets reached for to solve a formatting problem, without anyone stopping to threat-model what happens if the data being rendered “raw” ever contains something malicious. A related and equally common mistake is conflating input validation with output encoding: a developer checks that a field passed some validation rule and considers it “safe,” without recognizing that validation and output encoding are two different controls solving two different problems; a value can be perfectly valid input and still be dangerous if rendered without encoding.

How to Find It

  1. Submit a distinctive, harmless marker string and check whether it comes back unencoded in the HTML source, not just how it looks rendered on the page, since a browser might visually hide a broken tag while the raw markup is still exploitable.
  2. Test every distinct output context separately: the same field might be safely encoded when rendered in the HTML body but unsafely handled when the same value is also reflected into an HTML attribute, a JavaScript string, or a URL elsewhere on the same page. Encoding requirements differ per context, and a fix applied in one place doesn’t guarantee the same value is safe everywhere else it’s used.
  3. For DOM-based XSS specifically, review client-side JavaScript directly for places that read from location, document.referrer, or similar browser-provided sources and write into the DOM via an unsafe sink, since this category produces no server-side log entry to notice at all.
  4. Confirm real script execution (not just markup reflection) as proof before calling a finding confirmed: a payload appearing unencoded in the source is a strong signal, but demonstrating actual execution removes any doubt.

Impact by Scenario

Scenario Realistic impact
Reflected XSS on an unauthenticated public page, no session cookies affected Limited: requires tricking a specific victim into clicking a crafted link, with no ambient reach
Stored XSS on a page every logged-in user views High: fires automatically against every visitor, no crafted link or social engineering required
Stored XSS on an internal admin-only panel Very high: often results in full account takeover of an administrator, escalating a low-privilege submission into a high-privilege compromise
Session cookies protected by the HttpOnly flag Meaningfully reduces impact: client-side script can still act on the victim’s behalf while the page is open, but can no longer read the cookie value directly

Why a Business Should Care

XSS is routinely the actual mechanism behind session hijacking and account takeover in real incidents, not an abstract textbook risk: a script running in a victim’s browser can act with that victim’s full permissions for as long as the page stays open. When a tester demonstrates XSS by popping a harmless alert box, it’s worth explaining to a non-technical stakeholder exactly why that matters: the alert box itself is not the harm, it’s proof that arbitrary script execution is possible at all, and anything a script could do in that browser context (reading cookies, submitting forms as the victim, redirecting to a credential-harvesting page) is the actual risk being demonstrated.

A Worked Example

(Generalized from real engagement patterns. Company, product, and identifiers below are invented; no real system, client, or data is referenced.)

Meridian Support Desk lets customers open support tickets, and its internal staff view every ticket through an admin dashboard. During an authorized assessment, a tester submits a ticket whose message body contains a script payload instead of ordinary text. The public-facing ticket confirmation page renders the message back to the customer with proper encoding (no issue there), but the internal admin dashboard, built separately and later, renders the same stored message using a different template path that skips output encoding entirely.

When a support agent opens the ticket, the payload executes in the agent’s browser, in the context of the admin dashboard’s own session. The tester’s payload is deliberately limited to reading and reporting the session cookie’s presence and the page’s origin, proving that a real attacker’s script could exfiltrate that same session to an external server, without ever actually exfiltrating a real agent’s live session or taking any action inside the dashboard. Testing stops at that proof, the same ethical boundary demonstrated on the SQL Injection page: proving exploitability is the job, not causing the harm itself.

sequenceDiagram
    participant Assessor as Security Assessor (Customer Role)
    participant Portal as Meridian Customer Portal
    participant DB as Support Ticket Database
    participant AdminDashboard as Admin Portal (Staff Role)
    participant AdminBrowser as Staff Browser Runtime

    Assessor->>Portal: POST /tickets/create (Payload with script tag)
    Portal->>DB: INSERT INTO tickets (body) VALUES ('script tag')
    DB-->>Portal: Ticket Created (ID 9021)
    Portal-->>Assessor: Customer View: Escaped HTML script tag
    Note over AdminDashboard: Admin logs in & opens ticket #9021
    AdminDashboard->>DB: SELECT body FROM tickets WHERE id = 9021
    DB-->>AdminDashboard: Return raw unescaped payload string
    AdminDashboard-->>AdminBrowser: HTTP 200 with raw script tag in HTML stream
    Note over AdminBrowser: HTML parser executes script in admin session context
    AdminBrowser-->>Assessor: Script triggers benign origin verification ping
    Note over Assessor,AdminBrowser: ENGAGEMENT BOUNDARY PRESERVED<br/>Stored XSS execution proven using benign console marker.<br/>Zero admin session tokens exfiltrated - zero admin actions performed.
    Assessor->>Assessor: Document High-severity finding (Stored XSS in Admin Dashboard)

Severity Calibration

This instance rates High: it requires an authenticated customer account to submit the payload (a mild barrier compared to fully unauthenticated attacks) but results in script execution inside an internal administrative context, with a clear path to session compromise of staff accounts. It falls short of Critical specifically because reaching the vulnerable code path required an existing, if low-privilege, account: a fully unauthenticated stored XSS reaching the same admin surface would rate higher. As always, the vulnerability class alone doesn’t set the number; the demonstrated reach does.

Remediation

The real fix is context-aware output encoding everywhere untrusted data is written into a page: HTML-body encoding is not the same as attribute encoding, which is not the same as JavaScript-string encoding, and each output location needs the encoding appropriate to it. A Content Security Policy (CSP) is valuable defense-in-depth on top of that, restricting what a script, even one that does get injected, is actually allowed to do, but it is not a substitute for fixing the encoding itself.

The common bad fix is a blocklist that strips or rejects <script> tags specifically. This is trivially bypassed: event-handler attributes (onerror, onload), javascript: URLs, and inline SVG all provide alternate ways to execute script that a <script>-tag filter never considers. If the fix is context-specific pattern-blocking rather than systematic output encoding, it isn’t actually fixed.

AI Technical Drill

Test Your Understanding

On-demand, Gemini-generated technical drill testing root causes, exploit chains, and verified defenses.

🤖

Generate On-Demand Technical Quiz

Synthreat AI analyzes this topic's attack surface and compiles custom multiple-choice questions with verified explanations.

Question Count: