Web Security

What Is Cross-Site Scripting (XSS) and How to Prevent It

A defensive guide to cross-site scripting: the three types explained, plus the layered prevention that stops it — output encoding, CSP and framework escaping.

StackOptic Research Team19 May 202611 min read
What cross-site scripting (XSS) is and how to prevent it

Cross-site scripting is the vulnerability that lets an attacker put their own JavaScript into your pages and run it in your users' browsers — and because that script runs as if it were yours, it can do almost anything your own code can. In plain terms, XSS happens when untrusted data is allowed to become executable script in the browser, where it can steal session tokens, harvest what users type, deface the page, or redirect victims elsewhere. It is one of the most common and most serious web vulnerabilities, but it is also one of the most preventable. This guide explains, defensively, what XSS is, the three types at a high level, and the layered set of defences that reliably stop it. It contains no attack payloads — the aim is to help you protect your site.

Cross-site scripting is the very threat that a Content Security Policy exists to mitigate, so this pairs directly with what is a Content Security Policy and how to set one, and it is a central risk in how to protect your website from common attacks.

What XSS is and why it is dangerous

The Open Worldwide Application Security Project (OWASP) classes cross-site scripting within the injection family — and the OWASP Top 10 treats injection as one of the most critical categories of web risk. The defining feature of XSS is that an attacker's data is mistakenly handled as code by the browser.

Why is that so dangerous? Because a script that runs on your origin inherits the full privileges of your own JavaScript. It can:

  • Read cookies and tokens not protected from script access, enabling session hijacking.
  • Capture keystrokes and form input, stealing passwords and personal data as users type.
  • Modify the page — insert fake login forms, deface content, or show misleading information.
  • Make requests as the user, performing actions on their behalf.
  • Redirect victims to phishing or malware pages.

In short, XSS hands an attacker a foothold inside the trust boundary of your site, in your users' browsers. That is why it is worth taking seriously even on sites that feel low-risk: a comment box, a search field, or a profile name can be enough of an opening if untrusted data is mishandled.

The three types of XSS

XSS is usually divided into three types, distinguished by where the untrusted data enters and where the flaw lives. Understanding them at a high level helps you reason about defences — without needing any exploit detail.

Stored (persistent) XSS

In stored XSS, malicious input is saved on the server — in a database, a comment, a forum post, a profile field, a product review — and then served to other users later. When victims load the page containing that stored content, the injected script runs in their browsers. This is often the most damaging type because it is persistent and affects many users: anyone who views the poisoned content is exposed, with no special action on their part. A single successful injection into a popular page can reach a large audience.

Reflected XSS

In reflected XSS, untrusted input from a request — a query parameter, form field, or part of the URL — is immediately echoed back in the response without proper handling, and runs in that one victim's browser. It is typically delivered via a crafted link that the attacker convinces the victim to follow; the malicious input is part of the request, reflected straight into the page. Unlike stored XSS, it is not saved on the server and usually affects one victim per crafted request, but it is still serious, especially when combined with social engineering.

DOM-based XSS

DOM-based XSS is different: the flaw lives entirely in the browser, in your client-side JavaScript. It occurs when scripts take data from a source the attacker can influence (such as the URL fragment) and write it into the page through an unsafe sink — a DOM operation that turns data into markup or code — without proper handling. The server may never see the malicious data at all, which is what makes DOM-based XSS distinctive and sometimes harder to spot. As sites have become more JavaScript-heavy, this class has grown in importance.

TypeWhere untrusted data entersWho is affectedNote
StoredSaved on the server, served to usersMany users who view the contentOften the most damaging; persistent
ReflectedEchoed straight back in a responseThe victim of a crafted request/linkFrequently paired with social engineering
DOM-basedHandled unsafely by client-side JSThe victim whose browser runs the scriptFlaw is in the front end, not the server

The defences: an overview

No single control stops every XSS, so the professional approach is defence in depth — several independent layers, so that if one is missed, another still protects you. The table below maps each defence to what it primarily addresses; the sections that follow explain each.

DefenceWhat it primarily stops
Context-aware output encodingUntrusted data being interpreted as markup/code when rendered
Input validationMalformed or unexpected input entering the system at all
Content Security PolicyInjected scripts from running even if they get in
Framework auto-escapingThe most common XSS, by escaping output by default
Avoiding dangerous sinks (e.g. innerHTML)DOM-based XSS from unsafe client-side writes
Trusted TypesUnsafe values reaching dangerous DOM sinks (modern browsers)
HttpOnly cookiesScript from reading session cookies (limits the damage)

Defence 1: context-aware output encoding

The single most important defence is output encoding (also called output escaping): when you render untrusted data into a page, you convert characters that have special meaning into a form the browser displays as inert text rather than interpreting as code. Encode the characters that would otherwise start a tag or break out of an attribute, and the browser shows the data instead of executing it.

The critical nuance is that encoding must be context-aware. The same piece of data needs different treatment depending on where it lands:

  • HTML body context — encode so it cannot open a new element.
  • HTML attribute context — encode so it cannot break out of the attribute.
  • JavaScript context — data placed into a script needs JavaScript-appropriate encoding (and ideally should be avoided; prefer passing data as inert markup).
  • URL context — data placed into a URL needs URL encoding and scheme validation.

Getting the context right is what makes encoding reliable. OWASP's cheat sheets document the correct encoding for each context in depth and are the authoritative defensive reference. The encouraging news, covered below, is that frameworks do most of this for you when you let them.

Defence 2: input validation

Validate input as it enters your system: accept only what matches an expected format, length and type, and reject or constrain the rest. A field that should hold a postcode, an email or a number can be checked against exactly that shape, which removes a great deal of malicious input at the door.

Two cautions, though. First, validation complements but does not replace output encoding — some legitimate data (a name like O'Brien, or a comment containing characters with special meaning) is valid yet still needs encoding when rendered. Second, prefer allow-lists (define what is permitted) over block-lists (try to enumerate what is forbidden), because block-lists are routinely bypassed by inputs the author did not anticipate. Validation narrows the problem; encoding and CSP handle what remains.

Defence 3: Content Security Policy

A Content Security Policy (CSP) is the strongest backstop against XSS: even if an attacker manages to inject a script despite your other defences, a good CSP can stop the browser from running it. CSP tells the browser which sources may supply scripts; a script from an unapproved origin, or an inline script without a valid nonce or hash, simply will not execute.

CSP does not fix the underlying injection — it limits the blast radius when something slips through, which is precisely the value of a backstop. Two points are essential: avoid 'unsafe-inline' in script-src, because it permits any inline script (including injected ones) and largely defeats the policy; and prefer nonces or hashes, often with 'strict-dynamic', to allow only your intended scripts. The full rollout method — including testing in report-only mode first so you do not break your site — is covered in what is a Content Security Policy and how to set one.

Defence 4: framework auto-escaping

Here is the most practical defence of all: modern frameworks escape output by default. The mainstream front-end and server-side templating ecosystems automatically encode interpolated values when you render them into the page, treating data as text rather than markup. If you build with one of these and render data through its normal mechanisms, most XSS is prevented for you, for free.

The danger is in the escape hatches. Frameworks provide ways to render raw HTML deliberately — features explicitly named to signal danger, or low-level DOM operations — and using them on untrusted data reintroduces XSS. The rule is simple: use the framework's safe, default rendering; reach for raw-HTML features only when genuinely necessary, and only after sanitising the input with a well-maintained, purpose-built sanitiser. Treat every use of a raw-HTML feature as a spot that needs a second look in code review.

Defence 5: avoid dangerous sinks (and use Trusted Types)

DOM-based XSS comes from client-side code writing untrusted data into dangerous sinks — DOM operations that turn strings into markup or executable code. The defensive habits are:

  • Avoid assigning untrusted data to raw-HTML sinks such as innerHTML; prefer safe operations that set text content rather than parse markup.
  • Never pass untrusted data to code-evaluating constructs (the family of features that execute strings as code).
  • Treat client-side data sources the attacker can influence as untrusted, and handle them with the same care as server input.

A powerful modern reinforcement is Trusted Types, a browser feature (governed via CSP) that locks down the dangerous DOM sinks so they only accept values that have passed through a vetted policy, rather than arbitrary strings. Where supported, Trusted Types can systematically eliminate whole swathes of DOM-based XSS by making the unsafe pattern impossible rather than merely discouraged. MDN documents Trusted Types and the relevant DOM APIs as the reference for implementing this safely.

Defence 6: HttpOnly cookies (limit the damage)

Finally, mark session cookies HttpOnly so that JavaScript cannot read them. This does not prevent XSS, but it limits the damage: one of the classic goals of an XSS attack is to steal the session cookie, and HttpOnly takes that specific prize off the table. Combined with Secure and SameSite, it is part of basic cookie hygiene — see how to secure cookies with HttpOnly, Secure and SameSite for the full treatment. Think of it as containment: assume a defence might fail, and ensure the most valuable target is still protected.

How site owners can check their exposure

XSS is fundamentally a code-level issue, so it is not something an external header scan can fully confirm — but you can assess your posture:

  • Confirm you are using your framework's default escaping everywhere, and audit every use of a raw-HTML feature or innerHTML-style sink.
  • Check whether a meaningful Content Security Policy is present (DevTools → Network → the document's response headers, or curl -I), and that it does not rely on 'unsafe-inline'.
  • Verify session cookies are HttpOnly (visible in the Set-Cookie header / DevTools cookies view).
  • For thoroughness, consider professional code review or dedicated application security testing, which can find injection points an external check cannot.

Broader audits surface the supporting signals: StackOptic reports whether a CSP and secure cookie flags are present as part of a wider security-header grade, alongside performance and SEO, so you can confirm those backstops are in place — for reading the raw headers, see how to read a website's HTTP headers.

Common mistakes

  • Relying on input validation alone — valid data can still be dangerous when rendered; always encode output too.
  • Using block-lists of "bad" characters or words — they are routinely bypassed; prefer allow-lists.
  • Allowing 'unsafe-inline' in CSP — it looks like protection but lets injected inline scripts run.
  • Bypassing framework escaping with raw-HTML features on untrusted data without sanitising.
  • Assigning untrusted data to innerHTML and similar sinks — a leading source of DOM-based XSS.
  • Forgetting HttpOnly on session cookies — leaving the prize an XSS attack most wants within easy reach.
  • Encoding for the wrong context — HTML-encoding data that lands in a URL or script context does not protect it.

A quick XSS prevention checklist

  • Encode output appropriately for every context (HTML, attribute, JavaScript, URL).
  • Validate input with allow-lists, as a complement to encoding.
  • Deploy a Content Security Policy with nonces/hashes; avoid 'unsafe-inline'.
  • Use your framework's default escaping; avoid raw-HTML escape hatches unless sanitised.
  • Avoid dangerous DOM sinks like innerHTML; adopt Trusted Types where supported.
  • Mark session cookies HttpOnly (plus Secure and SameSite).
  • Review code for injection points; consider application security testing.

Go deeper

Want to know whether your CSP and cookie protections actually defend against XSS? Analyse any URL with StackOptic — security, performance and SEO in one free report.

Frequently asked questions

What is cross-site scripting (XSS)?

Cross-site scripting is a web vulnerability that allows an attacker to inject and execute their own JavaScript in the browsers of a site's users. The injected script runs in the context of your site, so it has the same access as your legitimate code: it can read cookies and tokens, capture what users type, modify the page, or redirect them. XSS consistently appears in the OWASP Top 10 as part of the injection category and is one of the most common web vulnerabilities.

What are the three types of XSS?

Stored XSS is when malicious input is saved on the server (for example in a comment or profile field) and then served to other users, so it executes for everyone who views that content. Reflected XSS is when input from a request is immediately echoed back in the response and runs in that one victim's browser, usually via a crafted link. DOM-based XSS happens entirely in the browser, when client-side JavaScript writes untrusted data into the page in an unsafe way.

How do I prevent cross-site scripting?

The primary defence is context-aware output encoding: render any untrusted data as inert text appropriate to where it appears, so the browser never treats it as code. Combine that with input validation, a Content Security Policy that blocks unauthorised scripts from running, framework auto-escaping, HttpOnly cookies, and avoidance of dangerous sinks like innerHTML. No single control is enough alone, so the robust approach is these layers working together as defence in depth.

How does a Content Security Policy help against XSS?

A Content Security Policy tells the browser which sources are allowed to run scripts. Even if an attacker manages to inject a script, a well-configured CSP can prevent it from executing because it does not come from an approved source or carry a valid nonce or hash. CSP does not fix the underlying injection flaw, but it is a powerful backstop that limits the damage when other defences fail. Avoid the 'unsafe-inline' value, which largely undoes that protection.

Does using a modern framework prevent XSS automatically?

Largely, yes, if you use it as intended. Modern front-end and templating frameworks escape interpolated output by default, so data placed into the page is treated as text rather than markup, which prevents most XSS without extra effort. The risk reappears when developers bypass that protection using raw-HTML features (such as setting innerHTML directly or a framework's dangerous-HTML escape hatch). Use those only when essential, and sanitise the input first.

Analyse any website with StackOptic

Get the full technology stack, performance, security and SEO report in seconds — free.

Analyse a website

Related articles