Escaping User Input in React and Vue Components: Secure Implementation Guide

Frontend frameworks abstract DOM manipulation but introduce implicit trust boundaries. This guide details exact escaping mechanisms for React and Vue, mapping untrusted data flows to secure rendering patterns. Establish your architectural baseline using Vulnerability Patterns & Web Mitigation Strategies before implementing component-level controls. Framework auto-escaping is context-dependent and fails under specific injection vectors. This document provides production-grade sanitization pipelines, compliance mappings, and CI/CD enforcement strategies.

Threat Modeling for Component Input Vectors

Identify DOM sinks by mapping data ingress to rendering contexts. Untrusted sources include URL query parameters, API response payloads, localStorage/sessionStorage, and third-party widget callbacks. Each source must be classified as untrusted until explicitly sanitized at the rendering boundary.

Map data flows to XSS triggers:

  • Text Contexts: {value} or {{ value }} (auto-escaped, low risk)
  • Attribute Contexts: href={value}, :src="value" (requires protocol validation)
  • HTML Contexts: dangerouslySetInnerHTML, v-html (requires DOMPurify allowlisting)
  • Event/Style Contexts: onClick={value}, :style="value" (requires strict schema validation)

Align sink classification with enterprise security baselines documented in Cross-Site Scripting (XSS) Mitigation to satisfy compliance audit requirements. Implement a data-flow registry that tags all props and state variables with a trustLevel enum (UNTRUSTED, SANITIZED, INTERNAL). Never rely on client-side validation alone; treat all ingress as hostile until sanitized at the DOM boundary.

React Auto-Escaping Mechanics & Bypass Vectors

React’s JSX compiler escapes string interpolation by default, converting <, >, &, ", and ' to HTML entities. This protection applies only to text nodes. Attribute bindings bypass text escaping and execute in the browser’s attribute parser, allowing protocol-relative and javascript: URI execution. dangerouslySetInnerHTML completely disables the compiler’s escaping layer.

Security Boundary: Never pass raw user input to dangerouslySetInnerHTML. Always route through a context-aware sanitization pipeline.

Secure React HTML Rendering Wrapper

DOMPurify integration with dangerouslySetInnerHTML fallback handling and strict allowlist configuration:

import DOMPurify from 'dompurify';
import { useMemo } from 'react';

interface SanitizedHTMLProps {
 html: string;
 className?: string;
 allowedTags?: string[];
 allowedAttrs?: string[];
}

const DEFAULT_CONFIG = {
 ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote'],
 ALLOWED_ATTR: ['href', 'class', 'rel', 'target', 'title'],
 ALLOW_DATA_ATTR: false,
 ADD_ATTR: ['target'],
 KEEP_CONTENT: true,
 SANITIZE_DOM: true,
 FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button'],
 FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onkeyup', 'onkeydown'],
 RETURN_DOM_FRAGMENT: false,
 RETURN_DOM: false,
};

export const SanitizedHTML: React.FC<SanitizedHTMLProps> = ({
 html,
 className,
 allowedTags = DEFAULT_CONFIG.ALLOWED_TAGS,
 allowedAttrs = DEFAULT_CONFIG.ALLOWED_ATTR,
}) => {
 const sanitized = useMemo(() => {
 if (!html) return '';
 const config = { ...DEFAULT_CONFIG, ALLOWED_TAGS: allowedTags, ALLOWED_ATTR: allowedAttrs };
 return DOMPurify.sanitize(html, config);
 }, [html, allowedTags, allowedAttrs]);

 return <div className={className} dangerouslySetInnerHTML={{ __html: sanitized }} />;
};

Bypass Mitigation: Disable RETURN_DOM and RETURN_DOM_FRAGMENT to prevent prototype pollution in older DOMPurify versions. Explicitly forbid on* attributes and style injection. Validate href values post-sanitization if external links are required.

Vue Template Compilation & v-html Sanitization

Vue’s template compiler auto-escapes {{ }} interpolations. The v-html directive bypasses compilation and injects raw HTML into the DOM. Vue’s reactivity system does not sanitize data; it only tracks mutations. Untrusted data passed to v-html executes immediately upon mount or update.

Security Boundary: Treat v-html as a privileged operation. Replace direct usage with a custom directive that enforces runtime sanitization.

Vue Custom Sanitization Directive

v-sanitize implementation with context-aware DOMPurify config and reactive binding safety:



Reactivity Implications: The updated hook ensures sanitization runs on every reactive mutation. For SSR compatibility, wrap directive logic in if (typeof window !== 'undefined') checks or use @vueuse/core’s useDOMPurify pattern to prevent hydration mismatches.

Edge-Case Handling: Dynamic Attributes & Rich Content

Dynamic href and src bindings require explicit protocol validation. Browsers execute javascript:, data:, and vbscript: URIs without triggering CSP script restrictions.

export const validateProtocol = (url: string, allowed: string[] = ['http:', 'https:', 'mailto:', 'tel:']): boolean => {
 try {
 const parsed = new URL(url, window.location.origin);
 return allowed.includes(parsed.protocol.toLowerCase());
 } catch {
 return false; // Reject malformed or relative URLs
 }
};

Style Injection: CSS-in-JS or inline :style bindings bypass HTML escaping. Sanitize style strings using css.escape or restrict to predefined utility classes. Never interpolate user input into calc(), url(), or @import.

SVG/MathML Parsing: Namespace injection allows XSS via <svg><script> or <math><mtext><table><mglyph>. Use DOMPurify with ALLOWED_NAMESPACES: ['http://www.w3.org/2000/svg'] or strip SVG entirely unless explicitly required for rich media rendering.

Markdown Conversion: Compile-time markdown parsers (e.g., marked, markdown-it) generate HTML that inherits all XSS risks. Configure parsers to disable raw HTML (html: false) and pipe output through DOMPurify before rendering.

CI/CD Integration & Compliance Workflow Automation

Enforce escaping policies at the pipeline level to prevent regression and satisfy SOC 2 Type II / ISO 27001 A.14.2.5 controls.

CI/CD SAST Configuration

ESLint + eslint-plugin-security rules for automated JSX/Vue template scanning and policy enforcement:

{
  "plugins": ["security", "react", "vue"],
  "rules": {
    "security/detect-object-injection": "error",
    "react/no-danger": "error",
    "vue/no-v-html": "error",
    "no-eval": "error",
    "no-implied-eval": "error"
  },
  "overrides": [
    {
      "files": ["*.tsx", "*.jsx"],
      "rules": { "react/no-danger": ["error", { "allow": ["SanitizedHTML"] }] }
    },
    {
      "files": ["*.vue"],
      "rules": { "vue/no-v-html": ["error", { "allow": ["v-sanitize"] }] }
    }
  ]
}

SAST Pre-Commit Hook: Integrate husky and lint-staged to block commits containing unsanitized sinks.

{
 "lint-staged": {
 "*.{js,jsx,ts,tsx,vue}": ["eslint --fix --max-warnings=0", "prettier --write"]
 }
}

Policy-as-Code Gates: Implement a custom AST scanner or use semgrep rules to flag dangerouslySetInnerHTML and v-html without adjacent DOMPurify imports. Map findings to Jira/Linear tickets with automated SLA tracking. Maintain an audit trail of linting reports and pipeline approvals for compliance reviews.

Common Implementation Failures

  • Assuming framework auto-escaping covers HTML attribute contexts: React/Vue only escape text nodes. Attribute bindings require explicit protocol validation.
  • Using innerHTML/v-html without allowlist sanitization: Blocklists fail against mutation-based XSS. Always use strict DOMPurify allowlists.
  • Ignoring SVG and MathML namespace injection vectors: These parsers bypass standard HTML sanitization. Explicitly configure namespace rules or strip entirely.
  • Bypassing CSP via dynamically generated inline event handlers: on* attributes execute regardless of CSP script-src. Strip all event handlers at the sanitization layer.
  • Failing to validate protocol-relative URLs in dynamic src/href bindings: //evil.com inherits the current page protocol. Always resolve URLs against window.location.origin and enforce explicit schemes.

Frequently Asked Questions

Does React automatically escape all user input? No. React escapes JSX text interpolation ({value}) but does not sanitize attribute bindings, dangerouslySetInnerHTML, or dynamically generated event handlers. Attribute contexts require explicit URL validation, and HTML contexts require runtime sanitization.

How do I safely render markdown in Vue without XSS? Disable raw HTML parsing in your markdown compiler (html: false). Pipe the generated HTML string through DOMPurify with a strict tag/attribute allowlist before binding to v-html or a custom directive. Enforce a strict CSP script-src 'self' to block residual payloads.

What CI/CD tools enforce frontend input escaping? eslint-plugin-security, eslint-plugin-react, eslint-plugin-vue, and semgrep for static analysis. Integrate with husky + lint-staged for pre-commit enforcement. Use custom AST rules to detect unsanitized sinks. Map outputs to compliance dashboards for audit readiness.

How does CSP complement component-level escaping? Component escaping is the primary control; CSP is defense-in-depth. Configure Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'self' to block inline script execution and unauthorized resource loading. Use nonces or strict-dynamic hashes for legitimate dynamic scripts. CSP mitigates escaping failures but does not replace them.