What is XSS and how does it work? With examples

What is XSS and how does it work? With examples

If you have ever wondered what is XSS and how does it work, the short answer is this: cross site scripting happens when an app takes input from one user and hands it to another user’s browser as code instead of plain text. The browser runs that code with the same trust it gives the real site. That means an attacker can read cookies, change the page, or act as the victim.

What cross site scripting actually is

A web page mixes two kinds of content. There is the markup and script the site author wrote, and there is data, like a comment, a search term, or a username. The browser cannot tell them apart on its own. It trusts whatever the server sends. Cross site scripting is the bug where attacker data crosses over and becomes script.

Here is the core idea. A user types a comment. The app stores it. Later the app prints that comment back into the HTML of a page. If the app prints it raw, and the comment contains a <script> tag, the tag runs in the next reader’s browser. The attacker never touched that reader. The site delivered the payload for them.

The browser runs attacker text as code because the app never told it where the data ends and the markup begins.

Why it matters

Script that runs on the page runs as the logged in user. It can read document.cookie if the session cookie is not protected, submit forms, or quietly change account settings. The attacker does not need the victim’s password. They borrow the victim’s open session.

The three types of XSS, with simple examples

People sort cross site scripting into three buckets based on where the bad input lives and how it reaches the browser. The examples below use an invented app called Acme Notes, a small site where people post public notes and comments. None of these target a real system.

Stored XSS

Stored XSS means the payload is saved in the database and served to everyone who views the page. It is the worst of the three because one submission can hit many users. This is a clear stored xss example.

Imagine the comment box on Acme Notes. A visitor submits this in the comment field:

<script>alert('xss')</script>

The app saves the text as is. When the note page renders, it builds the HTML like this on the server:

<div class="comment">
  <script>alert('xss')</script>
</div>

Now every person who opens that note runs the script. The alert('xss') is harmless on its own. It only pops a box. But a real attacker would swap it for code that reads the session cookie and sends it to a server they control. Same hole, worse payload.

Reflected XSS

Reflected XSS means the payload is not stored. It rides in the request, usually in a URL, and the server reflects it straight back into the response. The victim has to open a crafted link. This is a plain reflected xss example.

Say Acme Notes has a search page that shows what you searched for:

https://acme-notes.example/search?q=hello

The page prints: You searched for: hello. If the app prints the q value raw, an attacker can build a link where q is a script:

https://acme-notes.example/search?q=<script>alert('xss')</script>

Anyone who clicks that link runs the script in their own browser, on the real Acme Notes domain. The attacker sends the link by email or chat. The bug is on the page, but the trigger is the click.

DOM based XSS

DOM based XSS happens fully in the browser. The server may send clean HTML, but client JavaScript reads attacker input and writes it into the page in an unsafe way. The dangerous step is in the script the site already ships.

Suppose Acme Notes shows a welcome banner using the part of the URL after the #:

const name = location.hash.slice(1);
document.getElementById('banner').innerHTML = 'Hi ' + name;

Now an attacker shares this link:

https://acme-notes.example/#<img src=x onerror=alert('xss')>

The innerHTML assignment turns the text into real elements. The broken image fires its onerror handler, and the script runs. The server never saw the payload, because the part after # never leaves the browser. That is why server side filters miss it.

What is XSS and how does it work under the hood

Every variant of cross site scripting comes from one root cause. The app treats untrusted input as trusted output. The fix is to keep data as data the whole way through. There are two layers that do most of the work.

Output encoding

Encode data for the exact spot where it lands. When you put user text inside HTML, convert the characters that have meaning in HTML so the browser shows them instead of running them:

  • < becomes &lt;
  • > becomes &gt;
  • & becomes &amp;
  • " becomes &quot;

After encoding, the earlier stored payload renders as visible text:

<div class="comment">
  &lt;script&gt;alert('xss')&lt;/script&gt;
</div>

The reader sees the literal characters and nothing runs. Most template engines do this for you if you use their normal output syntax instead of a raw or unescaped output. Encoding depends on context. HTML body, an HTML attribute, JavaScript, and a URL each need their own encoding rules, so use a library that knows the difference rather than rolling your own escapes.

Avoid the unsafe sinks

For DOM based bugs, stop feeding untrusted input into sinks that parse HTML or run code. Reach for safe ones instead:

  • Use textContent instead of innerHTML when you only need text.
  • Avoid eval, setTimeout with a string, and document.write on user input.
  • Set attributes with setAttribute rather than building HTML strings by hand.

Content Security Policy

A Content Security Policy is a response header that tells the browser which scripts are allowed to run. It is a second line of defense, not a replacement for encoding. A strict policy blocks inline scripts and scripts from domains you did not approve:

Content-Security-Policy: default-src 'self'; script-src 'self'

With that header, an injected inline <script> is refused even if it slips into the page. Pair it with the HttpOnly flag on session cookies so script cannot read them through document.cookie. Layered defenses mean one mistake does not hand over the account. To find the weak spots in a policy before you ship it, our free Content Security Policy evaluator audits each directive and flags bypassable sources. You can check whether a site sets a strong policy and the rest of its security headers with our free security headers and CSP analyzer, which runs entirely in your browser.

How to spot it before an attacker does

Finding cross site scripting is partly about knowing where input flows. Trace every place the app reads input, then follow it to every place that input is written back out. Comment fields, search boxes, profile names, URL parameters, and error messages that echo your input are common starting points. For a wider tour of input bugs, see our injection and input category.

The hard cases are the ones that depend on how the app assumes its own data behaves, like a field that is encoded in one view and printed raw in another. Those gaps show up when you understand what the app expects, not just when you throw a list of payloads at it. This is exactly the kind of bug an autonomous researcher that tests an app’s assumptions is built to find and then prove with real evidence. You can read more about that approach on our about page.

Frequently asked questions

What is XSS and how does it work?

Cross site scripting, or XSS, happens when an app takes input from one user and hands it to another user’s browser as code instead of plain text. The browser runs that code with the same trust it gives the real site, so an attacker can read cookies, change the page, or act as the victim. The root cause is that the app treats untrusted input as trusted output. Learn more at the PortSwigger Web Security Academy.

What are the three types of XSS?

Stored, reflected, and DOM based. Stored XSS saves the payload in the database and serves it to everyone who views the page, which makes it the worst because one submission can hit many users. Reflected XSS rides in a request such as a URL and is echoed straight back, so the victim has to open a crafted link. DOM based XSS happens fully in the browser when client JavaScript writes attacker input into the page unsafely.

How do I prevent cross site scripting?

Keep data as data the whole way through. Encode user input for the exact context where it lands, so characters like < and > render as visible text instead of running, and let your template engine’s normal output syntax handle it rather than rolling your own escapes. For DOM bugs, prefer safe sinks like textContent over innerHTML and avoid eval and document.write on user input.

Does a Content Security Policy stop XSS on its own?

No. A Content Security Policy is a second line of defense, not a replacement for output encoding. A strict policy like default-src 'self'; script-src 'self' blocks inline scripts and scripts from domains you did not approve, so an injected inline script is refused even if it slips into the page. Pair it with the HttpOnly flag on session cookies so script cannot read them, since layered defenses mean one mistake does not hand over the account.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

Try it yourself: CSP Evaluator lets you paste a Content Security Policy and see which directives actually stop XSS. It runs entirely in your browser, with no signup, and nothing you paste is ever uploaded.