Search

How XSS Attacks Work

The short answer

Quick answer: Cross-site scripting (XSS) is a vulnerability in which a website includes untrusted content in a page without making it safe, so that the visitor's browser treats that content as code and runs it. Because the script runs as part of the trusted site, it can do anything the user can do there: read page content, act on the user's behalf, or capture what they type. The main defence is output encoding: converting special characters so that user-supplied text is displayed as text and never interpreted as HTML or JavaScript. Modern frameworks do this automatically, and a Content Security Policy adds a second layer.

Why the browser cannot tell the difference

A browser receives a page as one stream of HTML. It has no idea which parts the developer wrote and which parts came from a user's comment, a search term or a database field. If the stream contains a <script> element, the browser runs it.

Suppose a site displays comments by inserting them straight into the page:

<div >USER COMMENT GOES HERE</div>

If a comment contains HTML markup, and the site inserts it unchanged, that markup becomes part of the page. Markup that includes script will execute in the browser of everyone who views the comment.

The cause is the same as in SQL injection: data was mixed into code. There it was a database query; here it is a web page.

Why it matters

The browser's same-origin policy keeps one site's scripts away from another site's data. A script injected through XSS gets round it, because it runs as the vulnerable site. See how CORS works for the policy itself.

Such a script can:

  • Act as the user: change account details, post content, make purchases or transfers.
  • Read anything on the page, including personal data.
  • Take session tokens that JavaScript can access.
  • Record keystrokes or alter forms to capture passwords.
  • Change what the page shows, for example by displaying a fake login prompt on the genuine site.
  • Spread itself, if the injected content is stored and shown to other users.

On a banking, email or admin site, that amounts to account takeover.

The three types

TypeWhere the malicious input livesWho is affected
StoredSaved on the server: a comment, a profile field, a product reviewEveryone who views that content
ReflectedIn the request itself, such as a URL parameter that the server echoes back into the pageSomeone who follows a crafted link
DOM-basedNever reaches the server; the page's own JavaScript reads untrusted data and writes it into the page unsafelySomeone who follows a crafted link

Stored XSS is the most serious, because it needs no further action from the attacker and reaches every visitor.

Reflected XSS typically arises on search pages and error messages: "No results for [whatever was in the URL]".

DOM-based XSS has grown with single-page applications. The server response may be perfectly safe, and the flaw is in client-side code that takes something like the URL fragment and inserts it with innerHTML.

Defence 1: Encode output for its context

The central rule: when you put untrusted data into a page, encode it for the place it is going.

For HTML body content, that means replacing the characters HTML treats as special:

CharacterBecomes
<&lt;
>&gt;
&&amp;
"&quot;
'&#x27;

After encoding, a comment containing a script tag is displayed on screen as literal text, angle brackets and all. It is no longer markup.

Context matters. The correct encoding differs depending on where the data lands:

ContextExampleNeeds
HTML body<p>DATA</p>HTML entity encoding
HTML attribute<input value="DATA">Attribute encoding, and always quote the attribute
JavaScript<script>var x = "DATA"</script>JavaScript string encoding; better, avoid this pattern
URL<a href="/search?q=DATA">URL encoding
CSSclass="page_speed_4cce1733b8a52719d5f6e28b6f4b0492"Strict validation; avoid if possible

The OWASP XSS Prevention Cheat Sheet covers each context in detail.

Defence 2: Use a framework that escapes by default

You should rarely write this encoding by hand. Modern frameworks do it automatically:

  • React, Vue, Angular and Svelte escape values inserted into templates.
  • Server-side template engines such as Jinja2, Django templates, Razor and ERB escape by default.
// Safe: React escapes this
<p>{comment.text}</p>

XSS in modern applications nearly always comes from opting out of that protection:

FrameworkThe escape hatch
ReactdangerouslySetInnerHTML
Vuev-html
AngularbypassSecurityTrust...
Plain JavaScriptinnerHTML, document.write, eval
Template enginessafe or raw filters

Treat each use as something that needs a specific justification and review. In plain JavaScript, prefer textContent, which always treats its input as text.

Two more traps that frameworks do not cover:

  • URLs in href and src. A link whose address uses the javascript: scheme runs code when clicked. Check that user-supplied URLs begin with http: or https:.
  • Data placed inside inline scripts or event handler attributes.

Defence 3: Sanitise HTML you must allow

Sometimes you need to accept real HTML: a rich text editor, rendered Markdown, an email viewer. Encoding would destroy the formatting.

Use a dedicated sanitiser library, such as DOMPurify, which parses the HTML and removes everything not on an allow-list of safe tags and attributes. Do not attempt this with regular expressions; HTML is too irregular. See how regular expressions work.

Defence 4: Content Security Policy

A Content Security Policy (CSP) is a response header that tells the browser which sources of script are allowed.

Content-Security-Policy: script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'

With a strict policy:

  • Inline scripts are blocked unless they carry the correct one-time nonce.
  • Scripts may load only from approved origins.

So even if markup is injected, the browser refuses to run it. CSP is a safety net for when encoding is missed somewhere. It is not a replacement for encoding.

Defence 5: Limit the damage

  • HttpOnly cookies. A session cookie marked HttpOnly cannot be read by JavaScript, so an injected script cannot copy it. It can still make requests while the user is on the page, so this reduces harm without removing it. See cookies vs localStorage vs sessions.
  • Be careful where tokens live. Anything in localStorage is readable by any script on the page. This is central to the JWT vs sessions debate.
  • Re-authenticate for sensitive actions, such as changing a password or email address.
  • Trusted Types. A browser feature that blocks dangerous DOM operations unless the value has passed through an approved sanitiser.
  • Validate input as well. It does not replace output encoding, but it narrows what can get in.

What does not work

  • Filtering out <script>. There are many other ways to run script in HTML, and many ways to disguise markup.
  • Encoding on input and storing the result. The right encoding depends on where the data is later used, and stored encoded data causes double-encoding problems. Encode at output.
  • Relying on a firewall alone.
  • Client-side checks only.

XSS and CSRF are different

XSSCSRF
What happensAttacker's script runs inside the trusted siteAnother site causes the user's browser to send a request to the trusted site
What is abusedThe user's trust in the siteThe site's trust in the user's browser
Main defenceOutput encoding, CSPAnti-CSRF tokens, SameSite cookies

An XSS flaw can defeat CSRF protections, since the script runs on the site itself and can read the tokens.

Frequently asked questions

What does XSS stand for?

Cross-site scripting. The abbreviation uses an X to avoid confusion with CSS, the stylesheet language.

What is the difference between stored and reflected XSS?

Stored XSS is saved on the server and served to every visitor of the affected page. Reflected XSS comes from the request itself and affects only someone who opens a crafted link.

Does React prevent XSS?

It escapes values in JSX by default, which prevents most cases. Risks remain in dangerouslySetInnerHTML, user-controlled URLs and direct DOM manipulation.

Is a Content Security Policy enough on its own?

No. It is a valuable second layer, but the primary fix is correct output encoding.

Conclusion

XSS happens when a page lets data become code. The answer is to keep them apart: encode on output for the right context, let your framework escape by default, sanitise any HTML you must accept, and add a Content Security Policy and HttpOnly cookies to contain mistakes. With those in place, a malicious comment is just some odd-looking text.

Related articles

Sources and further reading

Usama Muneer

Usama Muneer

Coder, Blogger, Tech Speaker & Web Technologies Enthusiast. Passionate about working on open-source Programming languages & Tools while utilizing my Product Development skills.

Your experience on this site will be improved by allowing cookies Cookie Policy