Stockio

How to Prevent XSS Attacks in Web Forms

Protect form data from XSS with context-aware output handling, safe DOM APIs, URL validation, and practical tests for previews, saved content, and errors.

By Chidi Nwosu

Backend Engineer ·

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.
Featured sponsor

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

Keep reading

Related articles

More in Backend & APIs
HTML & CSS

10 Best HTML Email Template Resources

Find ten reliable starting points for HTML email layouts, from visual builders and coded templates to tested frameworks, plus guidance on accessibility and inbox checks.

Includes: HTML entitiesMarcus BellMarcus Bell
Accessibility

8 Best Accessible UI Component Libraries

Find an accessible component library for React, Vue, or Bootstrap projects, with practical notes on keyboard behaviour, styling freedom, and what your team must still test itself.

Includes: HTML entitiesNoor HaddadNoor Haddad
HTML & CSS

15 Best Free Resources to Learn HTML and CSS

Start learning HTML and CSS with fifteen respected free tutorials, interactive exercises, and reference sites, then choose a next step that suits your schedule and goals.

Includes: HTML entitiesJonah WeissJonah Weiss
Accessibility

8 Best Screen Readers for Testing Your Website

Compare eight screen readers across Windows, Apple, Android, and ChromeOS, with concrete keyboard and browser pairings plus practical checks for forms, headings, focus, and dynamic updates.

Includes: HTML entitiesTheo OkaforTheo Okafor
JavaScript & Frameworks

Event Delegation in JavaScript: DOM Event Patterns

Build resilient DOM interactions with delegated listeners, clear event ownership, safe target matching, and lifecycle cleanup for dynamically changing interfaces.

Includes: HTML entitiesDaniel KimDaniel Kim
HTML & CSS

Semantic HTML Landmarks: header, nav, main and footer

Give page regions clear meaning with native HTML elements, distinguish landmarks from articles, and verify navigation without adding redundant ARIA roles.

Includes: HTML entitiesJonah WeissJonah Weiss