In this guide
Your project has a comment section. A user posts a comment, and everyone who views the page sees it. Now imagine the "comment" is not text but instructions that run in every visitor's browser — reading their session, changing what they see, or silently performing actions as them. That is cross-site scripting (XSS): attacker-supplied code executing in other users' browsers, wearing your site's identity.
XSS is one of the most common web vulnerabilities precisely because it hides in the most ordinary feature: displaying user input back on a page. This guide explains the three types conceptually, why browsers execute the injected code, and the prevention techniques — output encoding, Content Security Policy, and framework habits — that shut it down. This is the defender's guide: concepts and prevention, no exploit payloads.
The core idea: your site speaks with the attacker's words
Browsers cannot distinguish "HTML the developer wrote" from "HTML a user submitted" once both are in the page. If your application inserts user input into the page without neutralizing it, and a visitor's browser receives that page, the browser executes whatever is there — including scripts the attacker supplied.
The damage comes from context: the script runs as your site, in your users' browsers. It can read page content, observe what users type, and make requests carrying the user's session. The attacker never touches your server; they borrow your users' browsers.
Note: like SQL injection, XSS is fundamentally a confusion of code and data — but on the client side. The database stores the attacker's input faithfully; the failure is at render time, when the application hands un-neutralized input to the browser.
The three types, conceptually
| Type | Where the payload lives | Who is affected | Example scenario |
|---|---|---|---|
| Stored (persistent) | In your database | Everyone who views the page | A malicious comment that executes for every visitor |
| Reflected | In the request (URL/parameter) | Whoever opens the crafted link | A search page echoing the search term into results |
| DOM-based | In client-side JavaScript | The user running the page | Page script reading the URL fragment and writing it into the page |
Stored XSS is the most severe: one submission compromises every future visitor until cleaned. Reflected XSS needs the victim to open a crafted link, but crafted links are cheap to distribute. DOM-based XSS never touches the server at all — the vulnerability is entirely in the page's own JavaScript handling untrusted data unsafely.
What injected scripts can do (conceptually)
Running as your site in a victim's browser, injected script can:
- Read sensitive page content — anything displayed on the page is readable to script running on it.
- Capture input — observe keystrokes in forms, including password fields on the compromised page.
- Act as the user — the browser automatically attaches the user's session cookies to requests the script makes, so actions (changing settings, posting content) execute with the victim's identity.
- Deface or redirect — rewrite page content or send users elsewhere.
Modern browsers' HttpOnly cookie flag blocks direct cookie theft via script, which helps — but the script does not need the cookie value when the browser sends it automatically. HttpOnly is a mitigation, not a fix.
Why it keeps happening in student projects
- Unescaped template output. Inserting user data into HTML with raw/unescaped output in templates. Most modern template engines escape by default — the vulnerability appears where developers override that, or use older string-concatenation rendering.
- innerHTML-style DOM updates. Client-side code building page content by assigning HTML strings containing user data, instead of using safe DOM methods or text assignment.
- Trusting "our users." "Only registered users can comment" is not a defense — one malicious account poisons every viewer.
- Rich-text features. Allowing formatted input (bold, links) requires careful sanitization; naive approaches either break or leave holes.
Prevention: the techniques that work
Output encoding (the primary defense)
Before user data is placed into a page, encode it for the context it is entering: HTML body, HTML attribute, JavaScript string, URL, or CSS each need their own encoding rules. Encoding converts characters with structural meaning into inert text, so the browser displays the input instead of interpreting it.
The practical rule: use your framework's default escaped output everywhere, and treat every override as a security decision. Modern frameworks (React, Vue, Angular, Django templates, and others) escape interpolated values by default — XSS in these stacks overwhelmingly comes from the documented "render raw HTML" escapes. If you use one, you should be able to explain exactly why that specific value is safe.
Content Security Policy (defense in depth)
CSP is an HTTP header that tells the browser which sources of script are allowed to execute. A well-configured policy (scripts only from your own domain, no inline scripts) means that even if an injection slips through, the browser refuses to run the injected code. It does not replace output encoding — it is the net underneath the tightrope.
Deploying CSP takes iteration: start with a report-only policy, watch the violation reports to find legitimate scripts you forgot, then enforce. For student projects, even a basic policy blocking inline scripts eliminates the largest class of XSS impact.
Safe DOM handling
On the client side, prefer assigning user data as text rather than HTML. When you genuinely need HTML from user input (rich text), pass it through a dedicated sanitization library configured with an explicit allowlist — never a hand-rolled regex filter. Hand-rolled sanitizers are where bypasses live.
Context matters
Encoding is context-specific: HTML-encoding a value placed inside a JavaScript string does not protect it. When user data flows into scripts, URLs, or event-handler attributes, apply the encoding for that context — or better, restructure so user data never enters code contexts at all (data attributes read by script are safer than interpolated script).
HttpOnly and SameSite cookies
Mark session cookies HttpOnly (inaccessible to script) and SameSite (not sent on cross-site requests). These blunt entire attack classes — they do not fix XSS, but they shrink what XSS can achieve.
Framework defaults you should know
Your framework choice determines how much XSS protection you get for free:
- React, Vue, Angular: values interpolated into templates are escaped by default. The risk concentrates in the explicit raw-HTML features (documented as dangerous for a reason) — audit every use.
- Server template engines (Django, Jinja2, EJS, Blade, and similar): autoescape user data in normal interpolation; the danger is the raw or unescaped output tags, which exist for legitimate cases (rendering your own trusted HTML) and get misused for user data.
- Vanilla JavaScript: no safety net at all. Every DOM insertion of user data is your manual responsibility — prefer text assignment over HTML assignment, always.
- Markdown renderers: user-supplied Markdown often compiles to HTML with raw-HTML passthrough enabled. If your project renders user Markdown (comments, notes), configure the renderer to strip or escape raw HTML, then sanitize the output.
Two universal rules across all of them: never interpolate user data into inline event-handler attributes or script blocks (use safe data-passing patterns instead), and when upgrading frameworks, check the changelog for escaping-behavior changes — defaults occasionally shift.
Checking your own project
- Identify every place user input is rendered: templates, API responses consumed by frontend code, error messages echoing input.
- Confirm all template output uses default escaping; list and justify every raw-HTML override.
- Search client-side code for HTML-string construction with user data; convert to text assignment or sanitized pipelines.
- Add a Content Security Policy, starting report-only.
- Set HttpOnly and SameSite on session cookies.
- If you accept rich text, verify it goes through an allowlist sanitizer library — and test with obviously hostile input in a local environment.
Note: probe only your own applications running locally. Testing payloads against sites you do not own is unauthorized access, regardless of intent.
Where to go from here
- OWASP Top 10 for student web apps — XSS in the context of the other nine risks your project should address.
- JWT authentication explained for students — how sessions work, and why cookie flags matter once you understand XSS.
- More secure-web builds in the Web Development branch hub.