Prevent cross site scripting in rich text and structured data

Cross-site scripting can enter through more than an obvious raw HTML component. A rich-text renderer may allow unsafe links. A structured-data object may be inserted inside a script element with ordinary JSON serialization. The same stored content can be safe in a text node and unsafe in another context. Security review must follow the value to the exact output boundary.
Consider an illustrative blog that accepts editor-authored rich text and embeds article metadata as JSON-LD. Editors are trusted to write content, but imported documents, compromised accounts, and stored links still require safe handling. This article describes a boundary review, not a tested sanitizer configuration. The chosen library and rendering framework must be verified in the application's actual environment.
Inventory sources and sinks
List where content originates: editor input, imports, API responses, stored records, and AI-generated drafts. Then identify every place it becomes browser output: text nodes, attributes, links, raw HTML, and script elements. Mark which paths use the framework's normal escaping and which bypass it.
Do not assume a value is safe because it came from the database or an administrator. Storage preserves content; it does not establish its suitability for every output context. A practical artifact is a map with source, transformations, sink, and required protection. Include preview screens and administrative views, since a payload may target an editor or operator rather than a public visitor. Review the complete path instead of sanitizing one form and declaring the feature protected.
Prefer structured rendering for ordinary content
When rich text is represented as structured blocks, render supported text and formatting through controlled components. Keep the allowed node types and attributes explicit. A paragraph, emphasis, and a link do not require arbitrary HTML execution. Validate link destinations with an allowlist appropriate to the product, rejecting executable or unexpected schemes.
If the product genuinely supports raw HTML, use a maintained sanitizer with a deliberate allowlist of elements, attributes, and URL behavior. Verify its configuration and integration against current documentation. Do not create a sanitizer from a few regular expressions; browser parsing and context differences are too complex for that shortcut. Apply protection at the output boundary and avoid modifying sanitized HTML afterward in ways that reintroduce unsafe structure.
Treat script serialization as a separate boundary
JSON-LD is data, but it is embedded in an HTML script element. Ordinary JSON serialization can include characters that interact with HTML parsing, including a closing script sequence in a string. Use the framework or library's documented safe embedding mechanism, or a reviewed serializer that handles the HTML context explicitly.
Do not hand-build JSON with string concatenation. Validate the metadata contract and serialize data rather than mixing content with executable expressions. Keep schema fields limited to what the feature needs. The correct protection for an HTML body is not automatically correct for a script element, attribute, or URL. Review structured data separately from visible article rendering even when both use the same title and excerpt fields.
Keep previews and imports inside the policy
A preview route should apply the same rendering protections as the public page. Otherwise an editor may encounter a payload before publication. Imported rich text should be normalized to the supported format or sanitized through the same controlled boundary, not trusted because it was produced by another tool.
AI-generated HTML should be treated as untrusted content too. An assistant may produce unsupported tags, unsafe attributes, or malformed structure without malicious intent. Validate its output before storage or rendering. Do not let the model choose the sanitizer policy. The product owns the supported content contract, and the rendering path must enforce it consistently. This also reduces accidental differences between editor preview, server-rendered page, and client navigation.
Build malicious synthetic cases
Create test content containing markup-like text, unsupported tags, unsafe URL schemes, suspicious attributes, and strings resembling closing script sequences. Use synthetic values and a controlled browser environment. Assert that text remains visible where intended and that executable behavior is not introduced.
Inspect both initial HTML and the resulting DOM. A source string alone may not show how the browser parsed it. Verify public and preview paths and any server-side metadata output. Use the selected sanitizer's own documented test guidance where available. Do not claim full XSS protection from a handful of examples; the cases establish that known boundaries behave as intended and should be supplemented by maintained libraries and focused security review.
Add defense in depth without replacing the boundary
A content security policy can reduce impact for some injection paths, but it does not excuse unsafe rendering. Choose a policy compatible with the application and verify it in the deployed environment. Avoid weakening it broadly to make a new widget work without examining the source of violations.
Keep sensitive cookies and session behavior appropriately protected, while recognizing that script execution can still act within a user's privileges. Review dependencies that transform rich text or metadata. A sanitizer upgrade may change supported behavior, so test the product's allowed formatting as well as malicious cases. Defense in depth works best when each layer has a clear role rather than when one security header is presented as a complete substitute for context-aware handling.
Verify useful content still works
Security changes can accidentally remove legitimate links, formatting, or metadata. Test representative articles with headings, emphasis, images if supported, and allowed internal and external links. Confirm server rendering and client navigation produce consistent output. Check that structured data remains valid JSON and contains the intended article fields after safe serialization.
Keep the policy small enough to explain to editors. If a feature is intentionally unsupported, show a clear editing outcome rather than silently discarding important content. Link content metadata contracts when publication fields feed both visible HTML and structured data. The smallest safe fix protects the actual sink while preserving supported content, instead of escaping every value indiscriminately and breaking the product.
Use AI to trace unsafe rendering
Give an assistant the content schema, renderer, metadata serializer, and sanitizer configuration. Ask it to identify values that reach raw HTML, attributes, URLs, or script elements without the appropriate boundary protection. Require file references and concrete payload paths. Do not provide private unpublished content; synthetic examples are enough for this review.
Have it propose tests for each sink and challenge any assumption that database content is trusted. Verify proposed library APIs and configuration in primary documentation. AI can help find forgotten preview paths, but its confidence is not security evidence. Report which render paths and payload classes were actually exercised, which libraries provide protection, and which additional review remains necessary.
Sources
OWASP XSS prevention guidance is the primary reference for context-aware protection and safe sinks.
OWASP threat modeling guidance supports following untrusted content across boundaries. The blog renderer and malicious cases above are illustrative.
About the Author

Kamruz
Full Stack Developer
A passionate developer who loves creating modern web applications and sharing knowledge with the community.
More About Me