Securing Web forms in React

Securing Web forms in React

Practical Guidelines for Preventing XSS and Data Injection in Web Forms


Introduction

Forms are a key part of most web applications. They handle user data entry, authentication, and configuration. Because they accept external input, forms are also one of the main sources of security vulnerabilities such as:

  • Cross-Site Scripting (XSS)
  • Data Injection
  • Phishing via injected form markup

React automatically escapes user-provided data in JSX, which prevents many common injection attacks. However, as applications scale and integrate with user-generated or external content, teams must apply consistent input sanitization and validation patterns.

This document defines standard practices for React-based projects to ensure that user input is handled safely across the application.


Core Principle: Validate Input, Encode Output

Every piece of user-provided data should be treated as untrusted.

“Never trust input that originates outside your code.”

StageActionExample
InputValidate format, type, and constraintsEmail validation, password length checks
OutputEncode or sanitize before renderingPrevent <script> execution or inline event handlers

React’s Built-in Escaping

React escapes HTML entities in JSX automatically.
For example:

<div>{userInput}</div>

If userInput contains <img src=x onerror=alert('xss')>, React renders it safely as:

<div>&lt;img src=x onerror=alert('xss')&gt;</div>

Clarification:

  • Escaping = converting HTML to text so the browser doesn’t interpret it.
  • Sanitizing = removing or neutralizing malicious HTML elements.

React escapes by default, but it does not sanitize. If you need to render HTML, you must sanitize it before rendering.


Sanitizing Inputs with DOMPurify

Use DOMPurify for cleaning untrusted text or HTML.

Installation

npm install dompurify

Example

import DOMPurify from 'dompurify';

function sanitizeInput(value) {
  return DOMPurify.sanitize(value, { ALLOWED_TAGS: [] });
}

const handleChange = (e) => {
  const clean = sanitizeInput(e.target.value);
  setFormValue(clean);
};

Note:
ALLOWED_TAGS: [] removes all HTML tags. This is recommended for most text inputs such as names, comments, or message bodies.


Use Schema Validation

Libraries such as Yup and Zod provide schema-based validation that can be reused on both client and server.

Example (Yup)

import * as Yup from "yup";

const schema = Yup.object({
  email: Yup.string().email("Invalid email").required("Required"),
  message: Yup.string().max(500, "Max 500 chars").trim()
});

With React Hook Form:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";

const { register, handleSubmit } = useForm({ resolver: yupResolver(schema) });

Always validate input before it’s sent to the backend and revalidate again server-side.


Avoid Dangerous Rendering APIs

❌ Avoid dangerouslySetInnerHTML with untrusted data

<label dangerouslySetInnerHTML={{ __html: userInput }} />

✅ Safe alternative

<label>{userInput}</label>

If rendering HTML is unavoidable (for example, formatted text from a CMS), sanitize it first:

<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userBio) }} />

Input and Form Element Guidelines

ElementGuidelineExample
<input>Use the appropriate type attribute<input type="email" />
<textarea>Strip HTML tagsDOMPurify.sanitize(value, { ALLOWED_TAGS: [] })
<button>Use React onClick, not inline JS<button onClick={handleClick}>
<form>Validate on submitDon’t rely solely on pattern or required

Centralized Sanitization Hook

Create a shared hook to standardize sanitization:

// useSanitizedInput.js
import DOMPurify from "dompurify";
import { useState } from "react";

export const useSanitizedInput = (initialValue = "") => {
  const [value, setValue] = useState(initialValue);

  const onChange = (e) => {
    const clean = DOMPurify.sanitize(e.target.value, { ALLOWED_TAGS: [] });
    setValue(clean);
  };

  return { value, onChange };
};

Usage:

const username = useSanitizedInput();
<input {...username} placeholder="Username" />

This ensures consistent cleaning logic across all components.


Sanitize Data on Render

When displaying user-supplied data (e.g., bios or comments):

const cleanBio = DOMPurify.sanitize(user.bio);
return <p dangerouslySetInnerHTML={{ __html: cleanBio }} />;

This prevents stored XSS attacks, where malicious data from the backend is rendered later.


Apply Content Security Policy (CSP)

CSP headers are configured on the server, not the client.
They limit what scripts and resources can execute in the browser.

Example:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://trusted-cdn.com;
  object-src 'none';
  frame-ancestors 'none';
  base-uri 'self';

Even if unsafe HTML is injected, a strong CSP prevents it from executing.


Trust nothing. Sanitize everything.

Security should be part of standard development workflows, not an afterthought.
Following these guidelines ensures:

  • Reduced XSS and injection risks
  • Consistent input handling
  • Easier maintenance across multiple teams

Comments

Popular posts from this blog

From Snap to Story - Part 1: Building an AI Photo Journal app with React Native, PoML & MCP

From Snap to Story: Building an AI Photo Journal with React Native, PoML & MCP