Regex for HTML Tag Stripping — and Why It Is Not Safe for Untrusted Input
Stripping HTML tags with regex is one of the most common "quick fix" snippets developers reach for — generating a plain-text preview, trimming a description for a meta tag, or cleaning up copy-pasted content. It works fine for trusted, well-formed HTML, but it is a genuine security risk when the input comes from users. This guide shows both sides.
Basic Tag Stripping
The simplest approach removes anything between < and >:
function stripTags(html) {
return html.replace(/<[^>]*>/g, '');
}
stripTags('<p>Hello <strong>world</strong>!</p>');
// 'Hello world!'Stripping Tags but Keeping Text Spacing
Block-level tags like <p> and <div> should leave a space or line break behind, or words from adjacent elements will run together:
function stripTagsWithSpacing(html) {
return html
.replace(/<\/(p|div|li|h[1-6]|br)>/gi, ' ') // add space after block-level closing tags
.replace(/<[^>]*>/g, '') // strip remaining tags
.replace(/\s+/g, ' ') // collapse extra whitespace
.trim();
}
stripTagsWithSpacing('<div>Hello</div><div>World</div>');
// 'Hello World' (not 'HelloWorld')Where This Breaks Down
The naive <[^>]*> pattern assumes tags are well-formed and never contain a > inside an attribute value. Real-world (or adversarial) HTML frequently violates that assumption:
// A > inside an attribute breaks the naive regex const tricky = '<img src="x" onerror="alert(1)" title="1 > 0">'; tricky.replace(/<[^>]*>/g, ''); // Result may leave a fragment behind or match incorrectly depending on nesting // Script tags without a clean closing structure const nested = '<scr<script>ipt>alert(1)</scr</script>ipt>'; // Regex-based filters historically failed on nested/broken tags like this, // letting the payload survive the "sanitization" step
This is exactly how many historical XSS filter bypasses worked: an attacker crafts input that looks stripped to a regex but reassembles into an executable tag once the browser's real HTML parser gets hold of it. Regex has no concept of nesting, attribute quoting, or parser state — it is fundamentally the wrong tool for security-sensitive HTML handling.
The Safe Alternative — Real Parsers
For anything user-submitted (comments, bios, rich-text editor output), use a library that actually parses HTML into a tree and applies an allow-list:
// Node.js — using sanitize-html
const sanitizeHtml = require('sanitize-html');
const clean = sanitizeHtml(userInput, {
allowedTags: [], // strip all tags — plain text only
allowedAttributes: {},
});
// Browser — using the DOM itself for text extraction (safe, no execution)
function stripTagsSafely(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
return doc.body.textContent || '';
}DOMParser parses the string exactly like a browser would when rendering it, so textContent reliably reflects only the text nodes — no script execution, no partial-tag leakage.
When Regex Stripping Is Actually Fine
- The HTML source is fully trusted — e.g. generated by your own CMS or markdown renderer, never user-submitted
- You only need a rough plain-text preview (like a search result snippet) where perfect correctness doesn't matter
- The output is never re-rendered as HTML anywhere — it is stored/displayed strictly as plain text
- For anything else — comments, chat messages, profile bios, uploaded content — use a real parser or sanitizer
Frequently Asked Questions
A simple pattern is /<[^>]*>/g used with String.replace to remove anything between angle brackets. It works for quick plain-text previews of trusted, well-formed HTML.
No. Regex-based tag stripping can be bypassed with malformed or nested tags and does not understand HTML parsing rules, which can leave XSS vectors intact. Use a dedicated sanitization library like DOMPurify, or parse with the DOM/an HTML parser, for any untrusted input.
Use a library such as sanitize-html or DOMPurify (with jsdom on the server) which parses the HTML into a real DOM tree and removes disallowed tags and attributes based on an allow-list, rather than pattern-matching text.