How to Secure Uncontrolled Components in React

Uncontrolled components in React rely on the DOM to handle form data rather than React state, making them a lightweight option for form handling but exposing them to unique security vulnerabilities like Cross-Site Scripting (XSS). This article explains how to secure uncontrolled components by implementing robust input validation, data sanitization, and safe handling of React refs to prevent malicious exploits.

Understand the Security Risk of Uncontrolled Components

In React, controlled components keep form data in the component state, allowing React to filter and control changes. Uncontrolled components use refs to pull field values directly from the DOM when needed.

Because uncontrolled components bypass React’s state management, they can easily bypass React’s built-in XSS protections if you take the retrieved values and render them directly into the DOM using properties like dangerouslySetInnerHTML, or if you pass the raw values to APIs and databases without sanitization.

1. Sanitize All Ref Inputs

The primary defense against XSS in uncontrolled components is sanitizing the data retrieved from refs before it is processed, stored, or rendered.

Never trust user input. Use a trusted sanitization library like DOMPurify to clean HTML and strip out malicious scripts before doing anything with the input value.

import React, { useRef } from 'react';
import DOMPurify from 'dompurify';

function SecureForm() {
  const inputRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    // Retrieve raw value
    const rawInput = inputRef.current.value;
    
    // Sanitize the input
    const cleanInput = DOMPurify.sanitize(rawInput);
    
    // Safely use cleanInput
    console.log(cleanInput);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={inputRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

2. Validate Input Against Strict Rules

Sanitization cleans data, but validation ensures the data adheres to expected formats. Implement strict validation rules for length, type, and character sets on the values pulled from your refs.

const handleSubmit = (event) => {
  event.preventDefault();
  const username = inputRef.current.value;

  // Validate: Alphanumeric only, between 3 and 16 characters
  const usernameRegex = /^[a-zA-Z0-9]{3,16}$/;
  if (!usernameRegex.test(username)) {
    alert('Invalid username format.');
    return;
  }

  // Proceed with validated input
};

3. Avoid Modifying the DOM Directly via Refs

React refs provide direct access to DOM nodes. It is highly insecure to use these refs to manually manipulate DOM elements—especially updating innerHTML with user-supplied values.

If you must update the DOM based on an uncontrolled input, update the React state with the sanitized value and let React handle the rendering safely.

// BAD: Vulnerable to XSS
myDisplayRef.current.innerHTML = inputRef.current.value;

// GOOD: Use textContent to prevent script execution
myDisplayRef.current.textContent = inputRef.current.value;

4. Implement Server-Side Validation and Sanitization

Client-side security measures can be bypassed by malicious actors. Securing uncontrolled components on the front end is only the first line of defense.

Always validate and sanitize all incoming data on your backend server before saving it to a database or returning it to other users. Treat all data coming from uncontrolled React inputs as untrusted.