Security

Preventing XSS Attacks When Rendering User-Generated HTML

SyncTonight Team8 min read2 views0 likes

Cross-site scripting (XSS) happens when an attacker manages to get their own JavaScript to run inside your page, in the context of another user's session. The classic entry point is rendering user-submitted content as raw HTML — a comment, a bio, a product review — without sanitizing it first, allowing something like <script>stealCookies()</script> or an onerror handler on an <img> tag to execute in every other visitor's browser.

Modern frameworks like React, Vue, and Angular actually protect you from this by default, in a way that's easy to take for granted. When you render a variable inside JSX or a template, these frameworks automatically escape special characters, turning < into &lt; and so on, so user input is always displayed as text, never executed as HTML or script. The vulnerability reappears specifically when a developer deliberately opts out of that protection.

In React, that opt-out is dangerouslySetInnerHTML — the name is a genuine warning, not just a stylistic choice. Any string passed to it is rendered as raw HTML, script tags and all, exactly like directly setting innerHTML in vanilla JavaScript. If that string ever contains unsanitized user input, you have an XSS vulnerability, full stop, regardless of how unlikely it seems that anyone would exploit it.

The only safe way to render user-generated HTML — for something like a rich-text comment editor that legitimately needs to support bold, links, and formatting — is to sanitize it through a dedicated library before it ever reaches dangerouslySetInnerHTML or innerHTML. A library like DOMPurify parses the HTML and strips out script tags, event handler attributes (onerror, onclick, and similar), and other dangerous constructs, while preserving safe formatting tags.

It's important to sanitize on the server side, not just the client — client-side-only sanitization can be bypassed by an attacker calling your API directly instead of going through your frontend's sanitization step. If the unsanitized content ever reaches your database and gets served to other users from there, sanitizing only in the browser that submitted it does nothing to protect everyone else.

A related, often-missed source of XSS is URLs. If you're rendering a user-supplied link as an <a href> without validation, an attacker can submit javascript:alert(document.cookie) as the 'URL' — clicking it executes arbitrary JavaScript. Always validate that user-supplied URLs actually start with http:// or https:// before rendering them as a clickable link.

Content Security Policy (CSP) headers are a strong second layer of defense, not a replacement for sanitization. A well-configured CSP can prevent inline scripts from executing at all, even if one somehow slips through your sanitization — but getting CSP right, especially with third-party scripts and frameworks that use inline styles, takes real tuning, and should be treated as defense-in-depth on top of proper input sanitization, not as the only safeguard.

If you're building a comment or rich-text feature and need to test what a sanitizer actually strips out, try feeding it deliberately malicious test strings like <img src=x onerror=alert(1)> during development — if that renders as a broken image with no alert, your sanitization is working; if the alert fires, it isn't, and you've caught the gap before real users could.

Found this helpful?

SyncTonight's tools and guides are free and always will be. If this post saved you some debugging time, a coffee goes a long way — no pressure, just appreciated.

☕ Buy me a coffee

Keep Reading

Also available

We also build websites.

Need a landing page, a full product site, or a custom web app built? We design and develop those too — same speed and no-nonsense approach you see here. Let us know what you're building.

Landing pagesFull websitesWeb appsSaaS MVPsDashboards
Let's talk about your project