zappz

Articlescross-site scripting

What Is Cross-Site Scripting? Three Kinds and How to Stop Them

Cross-site scripting lets an attacker's script run inside a site you trust. The three kinds, a worked comment box example, and the defenses that hold.

whitehatstoic

A software, security and AI safety practice: it builds web and mobile apps, finds and fixes weak spots in web apps and AI systems, and does AI safety research.

What is cross-site scripting, in one paragraph

What is cross-site scripting? It is a weak spot in a web app that lets an attacker put their own script into a page other people open. The browser cannot tell that script from the site's own code, so it runs it with the site's permissions. Cross-site scripting is usually written XSS, so it is not confused with CSS, the style sheet language. Microsoft security engineers settled on the name in January 2000, and its meaning has grown since then to cover more ways of injecting code. It is one of the weak spots that whitehatstoic, a software, security and AI safety practice, looks for in web apps.

Why the browser runs a stranger's code

Browsers keep sites apart with the same-origin policy. Content from one origin, meaning the same scheme (http or https), the same host name and the same port, shares permissions such as cookies. Content from a different origin has to be granted access separately.

That rule protects you from a random site reading your bank's cookies. It does nothing when the hostile script arrives on the bank's own page. Once the script is part of the page, it has the page's origin. It can read what the page can read, send requests the page can send, and change what you see.

So XSS is not a flaw in the browser. It is a flaw in how a web app builds its pages. Somewhere, text that came from a user ends up in the HTML as code instead of as words.

The three kinds of XSS

There is no single official list, but most people who work on web security split XSS into three kinds. They differ in where the hostile text lives before it runs.

  • Reflected (non-persistent). The script rides in the request itself, most often in a query parameter. The server copies it straight into the response page. A site search that shows "Results for" followed by your exact search words is the classic case. The attacker sends the victim a link, often by email, that points at a trusted site but carries the script in the address.
  • Stored (persistent). The script is saved on the server, in a comment, a profile, a forum post, and shown to every visitor who opens that page. No special link is needed. On social sites, a stored script can copy itself into each victim's own profile and spread like a worm.
  • DOM-based. The hostile data never reaches the server. JavaScript running in the page reads something, such as the part of the address after the # sign, and writes it into the page without care. The server logs look clean, which makes this kind easy to miss.

Stored XSS is usually the most damaging, because the script runs automatically for everyone who visits. Reflected XSS needs a victim to click. DOM-based XSS hides where server-side checks never look.

A worked case: the comment box

Take a small recipe site with a comment form under each recipe. Follow the steps and watch where it goes wrong.

Diagram of a stored cross-site scripting attack in four steps stacked top to bottom: a comment with a script, the site saving it, another reader opening the page, and the script running as the site
Stored XSS needs only one unescaped comment.
  1. A visitor types a comment. Instead of "Great soup", the text is a <script> tag holding a few lines of JavaScript.
  2. The server saves the comment to its database exactly as typed. That part is fine on its own.
  3. When anyone opens the recipe, the server builds the page by pasting each saved comment into the HTML. It does not escape the text, so the angle brackets stay angle brackets.
  4. The next reader's browser parses the page, finds a real script tag, and runs it as part of the recipe site.
  5. The script reads the reader's session cookie and sends it to a server the attacker controls. With that cookie, the attacker can act as the reader until the session ends.

Now change one thing in step 3. The server turns < into &lt; and > into &gt; before it writes the comment into the page. The reader's browser now shows the script as plain text on the screen. Nothing runs. The bug was never the comment. It was the moment the comment turned into code.

Escaping output for the place it lands

The main fix is to escape untrusted text at the moment it is written into a page, and to escape it for the exact spot where it lands. Text inside HTML needs HTML entity encoding. Text inside a JavaScript string needs JavaScript escaping. Text inside a style needs CSS escaping, and text inside a link address needs URL encoding. Using the wrong scheme for the spot can leave the hole open.

For most apps that do not need to accept rich formatting from users, escaping alone removes most of the risk. Watch the places where a page is built by hand: strings glued together in code, a setting that turns escaping off to show formatting, or client code that writes into the page directly.

Some apps do need rich text, such as a comment box that allows bold and links. Those apps need an HTML sanitizer that keeps a short list of safe tags and attributes and drops everything else. Blocking a list of known bad tags, such as script and iframe, is weaker, because a script can be written in ways the list did not expect.

Layers that limit the damage

Escaping is the fix. A few more layers make a missed spot less costly.

  • HttpOnly cookies. A cookie set with the HttpOnly flag cannot be read by scripts in the page. That blocks the cookie theft in the worked case. It does not stop a script from acting inside the page while you are on it, so it limits the damage rather than closing the hole.
  • Content Security Policy. A Content Security Policy is a response header that tells the browser which sources of script, style and other content the site approves. Under a policy, inline scripts are switched off by default, so a script tag injected into a comment will not run. A site that truly needs an inline script can allow that one script with a nonce or a hash instead of allowing all of them.
  • Careful client code. For DOM-based XSS, the same rule applies in JavaScript: write user data into the page as text, not as HTML.

None of these layers is perfect. Researchers published ways around Content Security Policy in 2015, 2016 and 2017, one of them through old script libraries kept on a server the policy already trusted. A policy is a strong second line, not a reason to skip escaping.

Frequently asked questions

Is cross-site scripting the same as SQL injection?

No. Both put untrusted text where code is expected, but SQL injection targets the database on the server, while XSS targets the browser of the person reading the page.

Can XSS happen on a site that uses HTTPS?

Yes. HTTPS protects data on its way between the browser and the server. It does nothing about a script that the server itself puts into the page.

Does a Content Security Policy make escaping unnecessary?

No. A policy blocks many injected scripts, but ways around it have been published, and a site with a loose policy gets little from it. Escaping fixes the cause; the policy limits the harm when a spot is missed.

Why is it called cross-site scripting?

The first attacks loaded the target site from an unrelated attacker's site and ran a script in the target's context, so the script crossed from one site to another. The name stuck even as the term grew to cover stored and DOM-based cases.

Why cross-site scripting keeps coming back

XSS has had a name for more than 25 years, and the fix has been known almost as long. It keeps returning because every new feature that shows user text is a new place to get it wrong: a search page, a profile field, a chat preview, a widget that renders a link. The rule that holds across all of them is short. Text from a user is data until the moment it is shown, and at that moment it is escaped for the place where it lands. When a page breaks that rule once, the browser will run whatever it is handed, with all the trust the site has earned.

  1. sql injectionWhat Is SQL Injection and How to Prevent ItSQL injection happens when an app pastes typed text into a database query. The attack, its main kinds, and how parameterized queries and least privilege stop it. · 7 min read

Keep reading

All articles
  1. 2048Why Does a 2048 Game End? A Full Board With No MergesWhy does a 2048 game end? Only when every square is full and no two equal tiles touch. How to read a crowded board before it locks, on a grid or a cube. · 7 min read
  2. third-party cookiesWhat Is a Third-Party Cookie? How Cross-Site Tracking WorksA third-party cookie is a file set by a domain other than the site being visited, used to follow browsing across sites for ad targeting. · 8 min read
  3. writing headingsHow to Write Headings That Answer a QuestionHeadings that answer a question put the answer in the heading text, so a reader scanning the page gets each point before reading the paragraph. · 8 min read