TypeScript React Unsanitized Property Vulnerability

High Risk Cross-Site Scripting (XSS)
TypeScriptReactXSSProperty InjectionInput SanitizationComponent Security

What it is

React application uses unsanitized user input in component properties or attributes, potentially leading to cross-site scripting (XSS) attacks through property injection.

import React from 'react'; import { useState } from 'react'; interface UserProfileProps { userInput: string; className?: string; style?: React.CSSProperties; } // Vulnerable: Direct user input in properties const UserProfile: React.FC = ({ userInput, className, style }) => { return (

{userInput}

{/* Vulnerable: Unsanitized content */}
); }; // Vulnerable: Dynamic property assignment const DynamicComponent: React.FC = () => { const [userProps, setUserProps] = useState({}); const handleUserInput = (input: string) => { // Dangerous: Direct assignment of user data const props = JSON.parse(input); // Can contain malicious properties setUserProps(props); }; return (
{/* Extremely dangerous: Spread operator with user data */} Content
); }; // Vulnerable: URL and href properties const LinkComponent: React.FC<{ url: string; title: string }> = ({ url, title }) => { return ( Click here ); }; // Vulnerable: Event handler injection const ButtonComponent: React.FC<{ onClick: string; label: string }> = ({ onClick, label }) => { return ( ); };
import React from 'react'; import { useState, useMemo } from 'react'; import DOMPurify from 'dompurify'; interface UserProfileProps { userInput: string; className?: string; style?: React.CSSProperties; } // Input validation helpers const validateClassName = (className: string): string => { if (!className) return ''; // Allow only alphanumeric, hyphens, and underscores const sanitized = className.replace(/[^a-zA-Z0-9\-_\s]/g, ''); // Limit length return sanitized.substring(0, 100); }; const validateStyle = (style: React.CSSProperties | undefined): React.CSSProperties => { if (!style || typeof style !== 'object') return {}; // Allowlist of safe CSS properties const allowedProperties = [ 'color', 'backgroundColor', 'fontSize', 'fontWeight', 'margin', 'padding', 'width', 'height', 'border' ]; const sanitizedStyle: React.CSSProperties = {}; Object.entries(style).forEach(([key, value]) => { if (allowedProperties.includes(key) && typeof value === 'string') { // Basic CSS value validation const cleanValue = value.replace(/[^a-zA-Z0-9\s#%.-]/g, ''); if (cleanValue.length <= 50) { sanitizedStyle[key as keyof React.CSSProperties] = cleanValue; } } }); return sanitizedStyle; }; const sanitizeText = (text: string): string => { if (!text) return ''; // Use DOMPurify for HTML sanitization return DOMPurify.sanitize(text, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }); }; // Secure: Validated user input in properties const UserProfile: React.FC = ({ userInput, className, style }) => { const sanitizedInput = useMemo(() => sanitizeText(userInput), [userInput]); const validClassName = useMemo(() => validateClassName(className || ''), [className]); const validStyle = useMemo(() => validateStyle(style), [style]); return (

{sanitizedInput}

); }; // Secure: Controlled property assignment interface AllowedProps { title?: string; description?: string; theme?: 'light' | 'dark'; } const SecureDynamicComponent: React.FC = () => { const [userProps, setUserProps] = useState({}); const handleUserInput = (input: string) => { try { const parsedInput = JSON.parse(input); // Validate and sanitize each property const validProps: AllowedProps = {}; if (parsedInput.title && typeof parsedInput.title === 'string') { validProps.title = sanitizeText(parsedInput.title).substring(0, 100); } if (parsedInput.description && typeof parsedInput.description === 'string') { validProps.description = sanitizeText(parsedInput.description).substring(0, 500); } if (parsedInput.theme && ['light', 'dark'].includes(parsedInput.theme)) { validProps.theme = parsedInput.theme; } setUserProps(validProps); } catch { console.error('Invalid JSON input'); } }; return (
{userProps.title &&

{userProps.title}

} {userProps.description &&

{userProps.description}

} Content
); }; // Secure: URL validation and safe href handling const validateUrl = (url: string): string | null => { if (!url) return null; try { const parsedUrl = new URL(url); // Only allow safe schemes const allowedSchemes = ['http:', 'https:', 'mailto:']; if (allowedSchemes.includes(parsedUrl.protocol)) { return parsedUrl.toString(); } return null; } catch { return null; } }; const SecureLinkComponent: React.FC<{ url: string; title: string }> = ({ url, title }) => { const validUrl = useMemo(() => validateUrl(url), [url]); const sanitizedTitle = useMemo(() => sanitizeText(title).substring(0, 100), [title]); if (!validUrl) { return Invalid URL; } return ( Click here ); }; // Secure: Predefined event handlers type AllowedAction = 'save' | 'cancel' | 'delete' | 'edit'; interface SecureButtonProps { action: AllowedAction; label: string; onAction: (action: AllowedAction) => void; } const SecureButtonComponent: React.FC = ({ action, label, onAction }) => { const sanitizedLabel = useMemo(() => sanitizeText(label).substring(0, 50), [label]); const handleClick = () => { // Safe: Predefined actions only if (['save', 'cancel', 'delete', 'edit'].includes(action)) { onAction(action); } }; return ( ); }; // Advanced: Property validation with TypeScript interface ValidatedComponentProps { data: { id: number; name: string; email: string; isActive: boolean; }; } const validateComponentData = (data: any): ValidatedComponentProps['data'] | null => { if (!data || typeof data !== 'object') return null; const { id, name, email, isActive } = data; // Type and format validation if (typeof id !== 'number' || id <= 0) return null; if (typeof name !== 'string' || name.length === 0 || name.length > 100) return null; if (typeof email !== 'string' || !email.includes('@') || email.length > 200) return null; if (typeof isActive !== 'boolean') return null; return { id, name: sanitizeText(name), email: sanitizeText(email), isActive }; }; const ValidatedComponent: React.FC<{ rawData: any }> = ({ rawData }) => { const validData = useMemo(() => validateComponentData(rawData), [rawData]); if (!validData) { return
Invalid data provided
; } return (

{validData.name}

{validData.email}

ID: {validData.id}
); }; // Custom hook for safe property handling const useSafeProps = >( props: T, validator: (props: T) => Partial ): Partial => { return useMemo(() => validator(props), [props, validator]); }; export { UserProfile, SecureDynamicComponent, SecureLinkComponent, SecureButtonComponent, ValidatedComponent, useSafeProps };

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

React component sets DOM properties with user input: element.innerHTML = userInput or element.outerHTML = data. Direct DOM manipulation bypasses React's XSS protection. innerHTML interprets HTML. User input with <script> tags executes. Unsanitized assignment enables XSS.

Root causes

Using Unsanitized User Input in DOM Property Assignments

React component sets DOM properties with user input: element.innerHTML = userInput or element.outerHTML = data. Direct DOM manipulation bypasses React's XSS protection. innerHTML interprets HTML. User input with <script> tags executes. Unsanitized assignment enables XSS.

Setting href or src Attributes Without Validation

Dynamic attributes with user data: <a href={userInput}>Link</a> or <img src={params.imageUrl} />. userInput may contain javascript: protocol. javascript:alert(1) executes code. data: URLs with HTML. User-controlled URLs in href/src create XSS vectors.

Using dangerouslySetInnerHTML with User Content

React dangerouslySetInnerHTML: <div dangerouslySetInnerHTML={{__html: userContent}} />. Bypasses React escaping. HTML from user input rendered directly. Script tags and event handlers execute. API responses or database content containing HTML. dangerouslySetInnerHTML requires sanitization.

Setting Event Handlers from String Attributes

Dynamic event handlers: element.setAttribute('onclick', userCode). String-based event handler from user input. onclick evaluates JavaScript. User controls code execution. Any on* attribute with user data vulnerable. Attribute-based handlers bypass CSP protections.

Using eval or Function Constructor with User Data

Code evaluation in React: eval(userInput) or new Function(userCode)(). Arbitrary JavaScript execution. User input as code. eval and Function constructor execute strings. React components calling eval with props or state from untrusted sources.

Fixes

1

Use React's JSX Auto-Escaping, Never Direct DOM Manipulation

Rely on JSX escaping: <div>{userInput}</div>. React automatically escapes. HTML special characters rendered as text. No innerHTML or outerHTML. Use React state and props. Virtual DOM handles updates safely. Auto-escaping prevents XSS.

2

Validate and Sanitize URLs Before Using in href/src

URL validation: const isHttpUrl = (url: string) => url.startsWith('http://') || url.startsWith('https://'); if (!isHttpUrl(url)) throw Error(); <a href={url}>. Reject javascript:, data:, vbscript: schemes. Allowlist approach for protocols. URL sanitization prevents protocol-based XSS.

3

Use DOMPurify to Sanitize HTML Before dangerouslySetInnerHTML

Sanitize with DOMPurify: import DOMPurify from 'dompurify'; const clean = DOMPurify.sanitize(dirtyHTML); <div dangerouslySetInnerHTML={{__html: clean}} />. Removes scripts, event handlers, dangerous attributes. Configure allowed tags. Industry-standard HTML sanitizer. Required for any user-generated HTML.

4

Never Use String-Based Event Handlers, Use JSX Event Props

React event handling: <button onClick={handleClick}>. Function references, not strings. Type-safe handlers. No setAttribute with on* attributes. React synthetic events. JSX syntax prevents code injection through event handlers.

5

Never Use eval or Function Constructor

Avoid eval entirely: no eval(userInput) or new Function(userCode). No safe way to use with untrusted data. Replace with JSON.parse for data. Use parsers for expressions. Sandboxed environments if dynamic code required. eval with user input is code execution.

6

Implement Content Security Policy for Defense-in-Depth

CSP headers: Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'. Blocks inline scripts. Requires nonces for legitimate inline scripts. External scripts from trusted domains only. CSP prevents XSS execution even if escaping bypassed.

Detect This Vulnerability in Your Code

Sourcery automatically identifies typescript react unsanitized property vulnerability and many other security issues in your codebase.