Stop 13 · SecurityTopics · 3/12

XSS Prevention

Part of the Backend Developer Roadmap

In short

XSS lets an attacker run their JavaScript inside your users' browsers, on your domain. That means stolen sessions, fake login forms and posts made in someone else's name.

Cross-Site Scripting — preventing attacker-controlled scripts from being injected into pages your API's data ends up rendered on, primarily by escaping/sanitizing output on the frontend, but also validating input on the backend.

The blob slides a pinned note into a clear plastic sleeve on a cork notice board.

Think of it like this

The notice board with a booby-trapped note

Anyone can pin a note on the hostel notice board. Most are harmless, but one note is actually a spring-loaded trap that snaps at whoever reads it. Escaping output is putting every note inside a clear plastic sleeve: you can still read it, but nothing on it can jump out.

Watch it happen

A stored XSS attack, and the fix

Attacker
Your server
Victim browser

POST /comments

1/5The comment text contains <img src=x onerror=...>, a tag that runs code when the image fails to load.

The big ideas

What you need to know

Escape on output

Turn < > & " ' into HTML entities when inserting text into a page. Template engines and React do this by default.

Sanitise when you allow HTML

If users may post bold or links, run their HTML through an allowlist sanitiser that keeps safe tags and drops the rest.

CSP is your seatbelt

A Content-Security-Policy header can block inline scripts, so even a missed escape doesn't run attacker code.

HttpOnly cookies

Session cookies marked HttpOnly can't be read by JavaScript, so a successful XSS can't simply copy them out.

See it in code

Walk through the code

ts
1import sanitizeHtml from "sanitize-html";2 3// Comment arrives as: "<img src=x onerror=alert(1)> Nice post!"4app.post("/comments", async (req, res) => {5  const clean = sanitizeHtml(req.body.text, {6    allowedTags: ["b", "i", "a"],7    allowedAttributes: { a: ["href"] },8  });9  await db.comment.create({ data: { text: clean } });10  res.status(201).json({ ok: true });11});

1/3A typical payload: an image that fails to load and runs JavaScript in its onerror attribute.

Sanitising keeps only the tags you list, so the img and its onerror vanish. You still escape when rendering plain text fields.

Save yourself the pain

Common mistakes

Oops

Using dangerouslySetInnerHTML or innerHTML with user content because it 'looked fine'.

Do this instead

Render user text as text. If you must render HTML, sanitise it first with an allowlist library.

Oops

Writing a regex to strip <script> tags.

Do this instead

There are dozens of ways to run JS without a script tag. Use a proper sanitiser and a CSP header.

Your mission

Spot the XSS in a comment box

~25 min

Quick check

Your site escapes all output, but an attacker still finds one missed spot. What limits the damage most?

Go deeper

  1. 1Learn how stored XSS (malicious data saved to your DB) differs from reflected XSS.
  2. 2Sanitize or reject HTML/script content in user-submitted text fields.
  3. 3Understand the backend's role is defense-in-depth — the frontend must also escape output correctly.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge