Last reviewed: 2026-10-19
Direct answer
Prevent cross-site scripting (XSS) in a coding-agent UI patch by tracing each untrusted value to the browser operation that consumes it. Render ordinary text through the framework’s normal escaped text binding or textContent on an ordinary display element. If the feature requires authored HTML, sanitize the final HTML with a maintained library and a reviewed allowlist immediately before insertion. Validate URLs separately, and keep untrusted values out of script, style, and event-handler code.
A useful review question is concrete: should the string <img src=x onerror=probe()> appear as text, become approved formatting, or be rejected? A patch that replaces a text binding with innerHTML has changed that contract. MDN’s innerHTML reference
explains that the setter parses HTML. Although script elements inserted this way do not execute, event-handler payloads can still run. A test containing only a script tag can therefore miss the vulnerability.
The OWASP XSS Prevention Cheat Sheet distinguishes output encoding, HTML sanitization, safe destinations, and dangerous contexts. Apply the defense at the actual rendering boundary; neither a successful API response nor a valid JSON object establishes that its strings are safe HTML.
Who this is for
This guide is for maintainers reviewing agent-written search results, chat transcripts, Markdown previews, administrative tables, notifications, and error displays. Start when a patch introduces raw HTML rendering, linkification, syntax highlighting, a rich-text editor, or a third-party widget that receives external strings.
Treat user submissions, repository files, issue text, model responses, imported documents, and stored database fields according to their origin and intended use. Passing through your server or being generated by a model does not authorize content to become browser code. Include less obvious views such as support dashboards, history pages, copied previews, and failure messages.
Key takeaways
- Specify text, URL, and rich HTML as separate contracts before reviewing the implementation.
- Prefer normal text bindings. Use raw HTML escape hatches only where the feature actually needs markup.
- Sanitize the HTML produced by the complete Markdown or rich-text conversion, then insert it without adding untrusted markup afterward.
- Give rich content the smallest useful tag and attribute set. Links, images, styles, and custom elements each add behavior to review.
- Test the final DOM and observable effects. A clean screenshot or an unchanged JavaScript marker alone is insufficient evidence.
- Keep Content Security Policy (CSP) and Trusted Types as additional enforcement layers, with deliberate browser compatibility and failure behavior.
Sources checked
Three primary public references were fetched successfully on 2026-10-19:
- OWASP: Cross Site Scripting Prevention Cheat Sheet supports context-specific encoding, framework escape-hatch review, URL validation, HTML sanitization, safe text destinations, and the limits of relying on CSP alone.
- MDN: Element innerHTML property
supports HTML parsing behavior, event-handler execution despite inert inserted script elements, plain-text use of
textContent, and Trusted Types enforcement. Its displayed last-modified date is October 12, 2026. - Cure53: DOMPurify documentation supports tag and attribute allowlists, DOM-fragment output, isolated configuration concerns, supported-environment checks, Trusted Types integration, and the risk of modifying sanitized content afterward.
These maintained technical references support an evergreen code-review task. They are not evidence of a new incident. The summary format, acceptance rules, and test matrix below are engineering recommendations derived from those references.
Contract details to verify
Trace the value through its final destination
Record the source, transformation, and browser destination for each changed field:
| Intended content | Rendering contract | Evidence to review |
|---|---|---|
| Search title or error message | Text in an ordinary element | Escaped binding or text-node assignment; literal markup stays visible as text |
| Formatted summary | A fixed subset of HTML | Final converter output passes through the approved sanitizer policy |
| External link | A product-approved URL | Parsed scheme and destination checks, then a fixed link attribute |
| Query value inside a known URL | One encoded URL component | URL construction API or component encoding; separate validation of the base URL |
| Script, event-handler, or style code | No untrusted executable interpolation | Trusted functions and fixed structure; redesign any proposed string-code path |
HTML escaping is specific to an output context. An encoder suitable for text between tags does not validate a complete URL or make a JavaScript expression safe. OWASP recommends validating untrusted href and src values and distinguishes URL-component encoding from URL validation. Define the schemes and destinations the product needs; do not accept a URL simply because it parses.
Keep element and attribute names fixed or explicitly allowlisted. setAttribute() is not a universal safe wrapper: an event-handler attribute or a URL-bearing attribute has different behavior from a text label. Similarly, use textContent for ordinary display elements, not for constructing script or style contents.
Give the agent a bounded task: preserve text rendering for titles and errors; permit only the approved formatting in summaries; add hostile-input fixtures for each changed destination. Inspect whether the final diff introduces a second render path in loading, retry, edit, or error states.
Use a deliberately small formatted-summary policy
This browser-module example assumes a bundler and an installed, reviewed DOMPurify dependency. It accepts basic paragraphs, emphasis, lists, and code. It deliberately excludes links, images, attributes, SVG, MathML, forms, and custom elements. Use a separate purifier instance owned by this module, and do not expose it for unrelated hooks or persistent configuration changes.
import createDOMPurify from "dompurify";
const summaryPurifier = createDOMPurify(window);
const summaryPolicy = {
ALLOWED_TAGS: ["p", "br", "strong", "em", "ul", "ol", "li", "code", "pre"],
ALLOWED_ATTR: [],
ALLOW_DATA_ATTR: false,
ALLOW_ARIA_ATTR: false,
RETURN_DOM_FRAGMENT: true,
};
export function renderPlainText(container, text) {
if (typeof text !== "string") throw new TypeError("summary_type_invalid");
container.textContent = text;
}
export function renderFormattedSummary(container, html) {
if (typeof html !== "string") throw new TypeError("summary_type_invalid");
if (!summaryPurifier.isSupported) throw new Error("sanitizer_unavailable");
const fragment = summaryPurifier.sanitize(html, summaryPolicy);
container.replaceChildren(fragment);
}
The caller supplies a trusted, ordinary container element. Apply input-size limits before expensive conversion or sanitization. If Markdown is accepted, pass the converter’s final HTML to renderFormattedSummary; escaping the original Markdown does not prove the converter’s eventual HTML is safe.
DOMPurify permits data and ARIA attributes by default, so this policy explicitly disables both alongside its empty attribute list. Its documentation also warns that USE_PROFILES overrides ALLOWED_TAGS, and persistent setConfig() makes later per-call configuration arguments ineffective. Avoid mixing those mechanisms into this narrowly owned instance.
The returned fragment is inserted as nodes. Do not serialize it, concatenate an untrusted footer, and parse the combined string again. Review highlighters and other post-processors for new markup or attributes. DOMPurify and OWASP both warn that later modifications can undo sanitization. Widen the allowlist only as a separate feature decision with corresponding tests.
Add enforcement without hiding the original defect
MDN documents that Trusted Types enforcement can make an innerHTML string assignment throw when no applicable default policy exists. A policy should pass input through the reviewed sanitizer; merely wrapping the original string in a trusted type does not remove dangerous content. DOMPurify documents the return-type requirements when it is called inside a policy’s createHTML() callback.
Test the actual CSP, policy names, dependency behavior, and supported browsers together. A compatibility wrapper does not create browser enforcement where the API is unavailable. Keep safe rendering and sanitization effective independently of that support. If the application also renders on a server, review its DOM implementation and sanitizer environment separately; this browser helper is not a server-rendering integration.
Happy-path operator workflow
- Freeze the proposed commit and inventory all changed rendering destinations, including error messages and stored-content views.
- Write the format contract for each field. Remove raw HTML rendering from fields that only need text.
- Trace converters, sanitizer calls, third-party components, and later DOM mutations in execution order. Establish which operation is the last security-sensitive transformation.
- Run ordinary-content cases. Verify readable text, preserved approved formatting, replacement behavior on updates, and the product’s intentional treatment of disallowed markup.
- Run the adversarial matrix below through the real component and supported browser builds. Confirm the tests reach initial rendering, updates, and navigation back to stored content.
- Retain the commit, lockfile revision, sanitizer version, policy identifier, browser versions, fixture IDs, and assertion results with the review.
Include the repository’s security scan gate , while reviewing the field-to-destination contract directly. Static analysis may identify an unsafe assignment; it cannot establish the product’s allowed formatting on its own.
Error-path operator workflow
If a value has the wrong type, the sanitizer is unsupported, or sanitization throws, keep the rendering attempt failed. Show a fixed error message or deliberately render the original value as plain text under a separate reviewed path. Never recover by assigning the original string to innerHTML or a framework’s raw-HTML property. Decide explicitly whether the old view remains visible or is cleared.
If a test observes an unapproved element, attribute, navigation, or request, block the patch and preserve its smallest synthetic reproducer. Check the active policy and post-processing order before changing the fixture. An empty alert list or a CSP-blocked event does not excuse dangerous DOM that violates the contract.
Log fixture identifiers, policy versions, and stable failure classes. Avoid copying user content, model output, browser storage, or full DOM snapshots containing private data into general CI logs. Rerun against the new commit after any repair; the earlier result belongs to the earlier rendering code.
Prove the boundary with adversarial tests
Use disposable content and accounts. Check both DOM structure and observable effects in a browser harness with controlled network destinations.
| Fixture or transition | Required observation |
|---|---|
| Plain text containing angle brackets, quotes, and ampersands | Exact text value; no child elements created from it |
| Approved paragraph, emphasis, list, and code markup | Expected elements remain; no attributes survive the example policy |
| Image with an error handler | No image or handler remains in formatted output; no probe executes |
A link with a javascript: URL | No link survives the example policy; a link-enabled feature needs separate URL tests |
| SVG, MathML, iframe, form, style, or event attributes | Outside-policy elements and attributes are absent from the final DOM |
| Malformed nested markup and encoded code examples | Parsed result stays within the policy and intended text remains readable |
| Markdown conversion followed by highlighting | Assertions inspect the final rendered tree after every post-processor |
| Stored content reopened, edited, or updated | Every render uses the same boundary; no stale raw-HTML branch appears |
| Missing sanitizer support or thrown conversion | Controlled error or explicit text fallback; original HTML never reaches an unsafe destination |
For a harmless execution probe, use a synthetic event handler that sets a dedicated test marker. Prove the harness can observe that marker with an intentionally unsafe positive-control fixture isolated from application tests. Trigger the relevant event and observe completion; an arbitrary short sleep can pass before the event occurs. Assert the DOM allowlist as well, so CSP cannot mask a broken sanitizer test.
The exact helper above passed 70 local assertions using Node.js v22.22.2, DOMPurify 3.4.17, and jsdom 30.1.3. Checks covered 13 string fixtures, approved formatting, replacement, and five invalid value types. The jsdom harness disabled script execution and resource loading: these results verify DOM structure and helper input behavior, not browser execution, CSP, Trusted Types, hydration, or an application’s complete rendering path. Run those browser checks in the repository before approving its patch.
Failure modes
The agent removes only script tags. MDN’s event-handler example shows why that is insufficient. Use text rendering or the maintained sanitizer boundary.
Sanitization happens before the last HTML-producing step. A converter, highlighter, or concatenated footer can add new behavior afterward. Move the boundary or redesign the later step, then inspect the final DOM.
The sanitizer’s default policy becomes the product specification. General-purpose defaults permit more than this summary feature requires. Keep the allowlist explicit and review configuration changes alongside feature changes.
A shared persistent configuration overrides the local policy. Isolate ownership and test the effective configuration. A configuration object beside a call is not evidence that the call used it.
CSP silence is treated as proof of safe markup. Keep structural assertions. A different browser, deployment header, or later policy change can expose a defect that the current header suppresses.
XSS and CSRF reviews are combined into one checkbox. Rendering integrity and authenticated request intent need separate evidence. Use the site’s CSRF review workflow for browser state changes, and retain the XSS rendering checks.
FAQ
Does a modern framework prevent every XSS bug?
No. Its normal escaped text rendering is a useful default, but raw HTML APIs, template escape hatches, direct DOM operations, and third-party components can bypass that protection. Review the actual destination used by the patch.
Can the model be instructed to return safe HTML?
A prompt can specify a format, but it is not an enforcement boundary. Treat the response as untrusted input and apply the same rendering contract as other external content.
Is Markdown safe if raw HTML is disabled?
That setting removes one input path. Links, plugins, generated HTML, and post-processing still need review. Test the final converter output under the feature’s sanitizer and URL policies.
Should sanitized HTML be stored permanently?
Record the policy and dependency version if the application caches sanitized output. Revisit cached content when those change, and prevent less restricted originals from reaching another view. A stored string is not permanently safe for every future output context.
Reader next step
Choose one title, error, or summary field touched by the next coding-agent patch. Write its source-to-destination contract, make text the default where formatting is unnecessary, and add an event-handler fixture plus final-DOM assertions. Approve the rendering change when ordinary content still works, disallowed markup cannot survive its actual render path, and sanitizer failures cannot fall back to raw HTML.