On this page
A feedback form can become a script delivery mechanism when its submitted text later appears in a preview, moderation screen, or confirmation message. The dangerous step is usually not collecting the value. It is interpreting that value as executable markup somewhere downstream.
Preventing cross-site scripting requires tracing data from input to output and choosing safe handling for each destination. This guide focuses on ordinary forms, text previews, links, and the rich-text exceptions that need a different defense.
Understand where text becomes executable
Untrusted input includes more than an anonymous message. Query parameters, imported records, account names, and data returned by your own API can contain attacker-controlled strings. A database does not make content trustworthy merely by storing it.
Use the HTML Entity Encoder / Decoder below to inspect how characters such as ampersands and angle brackets can be represented as text. That is a useful learning exercise, but HTML encoding is context-specific. It is not a universal sanitizer for JavaScript, CSS, URLs, or every HTML attribute.
Map each form field to its eventual destinations. A support message might appear in the user's preview, an administrator dashboard, an email, and a downloaded report. Each rendering path needs an appropriate rule. A safe public page does not compensate for an unsafe staff interface with broader account privileges.
Prefer text rendering over HTML interpretation
The strongest routine defense is a safe output sink. When the intended result is plain text, assign textContent rather than building a markup string. The browser then treats special characters as content instead of interpreting them as elements or handlers.
This example assumes the page already contains a form with a named message field and a separate preview element. It also checks the field type before reading its value, making accidental markup changes fail clearly during development.
const form = document.querySelector<HTMLFormElement>("#feedback");
const preview = document.querySelector<HTMLElement>("#preview");
if (!form || !preview) {
throw new Error("Feedback form markup is incomplete");
}
form.addEventListener("input", () => {
const field = form.elements.namedItem("message");
if (!(field instanceof HTMLTextAreaElement)) {
throw new Error("Expected a message textarea");
}
preview.textContent = field.value;
});With this approach, text resembling a script or image element stays visible as text. Read the MDN textContent reference for behavior details, including how replacing text affects existing child nodes.
Framework interpolation usually escapes ordinary text by default, but escape hatches can bypass that protection. Audit raw HTML rendering APIs, custom template helpers, and direct DOM manipulation. Do not assume that using a framework makes every rendering operation safe.
Match the defense to the destination
Output context determines the defense. HTML text, attribute values, URLs, scripts, and styles are different parsers with different rules. Encoding a string once and reusing it everywhere is a common way to create either vulnerabilities or broken presentation.
For server templates, use the template engine's automatic escaping and its documented contextual rules. Keep attributes quoted. Avoid inserting user data into inline event handlers, script bodies, or dynamically assembled CSS. Refactor the interface so data stays in ordinary text nodes or well-defined DOM properties instead.
URLs need scheme validation, not just entity encoding. A syntactically valid URL can still use a dangerous or unwanted scheme. This example accepts only absolute HTTPS destinations, which is a deliberate product restriction rather than a universal URL policy.
function approvedExternalLink(raw: string): URL | null {
try {
const url = new URL(raw);
return url.protocol === "https:" ? url : null;
} catch {
return null;
}
}
const destination = approvedExternalLink(submittedWebsite);
if (destination) {
const link = document.createElement("a");
link.href = destination.href;
link.textContent = "Visit submitted website";
container.replaceChildren(link);
}If only specific hosts are allowed, compare normalized hostnames against an explicit allowlist. HTTPS alone does not make a destination trustworthy. Follow the OWASP XSS Prevention Cheat Sheet for the broader distinction between safe contexts and contexts best avoided entirely.
Treat rich text as a separate feature
Sometimes users genuinely need limited formatting, such as paragraphs, emphasis, and links. Escaping everything would display the markup literally, while accepting arbitrary HTML would expose an execution surface. This is a sanitization problem, not an invitation to write a clever regular expression.
Use a maintained HTML sanitizer with a deliberately small allowed feature set. Configure permitted elements, attributes, and URL protocols; keep the sanitizer updated. Run sanitization at the point required by your architecture, and never assume that later string transformations preserve its guarantees.
Keep the rendering pipeline explicit
Markdown is not automatically safe. Some parsers allow raw HTML, and plugins may generate additional markup. Decide whether raw HTML is disabled, validate link destinations, and sanitize generated HTML when required by the renderer's behavior. Review the complete pipeline described in building a custom Markdown editor, not merely the input editor.
Store original content separately from presentation rules when future reprocessing matters. If you store sanitized HTML, record which policy produced it and plan how to handle policy changes. Avoid repeated encode-decode cycles, which can make unsafe content reappear in a later stage.
The entity conversion tool is useful for inspecting a small non-sensitive sample during debugging. It should not become a production security boundary, and real customer messages or secrets should not be pasted into diagnostic tools without an approved data-handling policy.
Test the entire form lifecycle
A useful test suite follows every rendering path. Submit strings containing angle brackets, quotes, ampersands, and URL-like values. Assert that text remains text, unexpected elements do not appear, and link policies reject disallowed schemes. Test saved records as well as immediate previews.
Include validation failures. Applications often render rejected values in error summaries or debugging panels, creating a vulnerable path that a successful submission never exercises. Also inspect administrative screens, exports opened in browsers, and notifications that reuse submitted content.
Add a Content Security Policy as defense in depth, preferably beginning with report-only observation while you remove incompatible inline behavior. A restrictive policy can reduce exploitation opportunities, but it does not repair unsafe rendering. Read the MDN CSP overview before choosing directives or nonce handling.
Keep authorization and server-side validation in scope without confusing them with XSS defenses. Length limits protect usability and resources; authentication controls access; neither makes dangerous output safe. For more implementation patterns, use the frontend article collection alongside security-specific review.
Key takeaways
- Keep plain text as text through safe APIs and automatically escaping templates.
- Apply context-aware defenses rather than treating HTML entities as a universal security layer.
- Validate URL schemes and host restrictions according to the product's actual linking policy.
- Sanitize intentionally supported rich HTML with maintained tooling and a narrow configuration.
- Test previews, errors, saved records, and privileged screens; add CSP as a secondary safeguard, not a replacement.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor