Client-side Template Injection
ID |
javascript.client_side_template_injection |
Severity |
critical |
Resource |
Injection |
Language |
JavaScript |
Tags |
CWE:95, OWASP:2021:A3, PCI-DSS:6.5.1 |
Description
Template engines allow dynamic generation of content (HTML, JSON, email messages…) that is merged with data.
The insecure embedding of untrusted input in executed templates allows Client-Side Template Injection (CSTI) when template rendering is done on the client side (e.g. in a browser using Angular). If an attacker is able to inject expressions into the template, the attack payload could include code execution or sensitive information leaks.
An application that is vulnerable to CSTI can open the door to arbitrary JavaScript code execution in the user agent (browser), including cross-site scripting and related cross-site request forgery attacks, stealing the victim’s cookies, performing actions on behalf of the user, or even logging keystrokes.
Browser cross-site scripting filters are typically unable to prevent CSTI attacks.
Rationale
The check will issue a violation if non-neutralized user-controlled input is evaluated in the template compilation of expression evaluation functions.
For React, the following example uses dangerouslySetInnerHTML
to encode a dynamic template using unsanitized input:
import React, { useState } from 'react';
// Vulnerable Component with CSTI Risk
const VulnerableUserProfile = () => {
const [username, setUsername] = useState('');
const [userDescription, setUserDescription] = useState('');
// DANGEROUS: Using dangerouslySetInnerHTML without sanitization
const handleSubmit = (e) => {
e.preventDefault();
const userProfileHTML = `
<div>
<h2>Welcome, ${username}!</h2>
<p>${userDescription}</p>
</div>
`;
return (
<div
dangerouslySetInnerHTML={{
__html: userProfileHTML
}}
/>
);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<textarea
value={userDescription}
onChange={(e) => setUserDescription(e.target.value)}
placeholder="User Description"
/>
<button type="submit">Create Profile</button>
</form>
);
};
export default VulnerableUserProfile;
Remediation
-
Never use non-trusted content as your (Vue, Angular, React…) component template. Doing so is equivalent to allowing arbitrary JavaScript execution in your application.
-
If possible, avoid using server-side code to dynamically embed user input into client-side templates.
-
If this is not practical, sanitize template expression syntax from user input before embedding it in client-side templates. Review calls to template compilation and expression evaluation in the application code.
Note: Encoding HTML is not sufficient to prevent CSTI attacks.
-
Do not rely on the sandboxing capabilities offered by some template engines. In recent versions of Angular, the sandbox has been removed to avoid a false illusion of security.
-
Check the template engine documentation for security advice.
For the React example, this fix version demonstrates:
-
Using DOMPurify to sanitize inputs
-
Rendering React elements instead of HTML strings
-
Avoiding
dangerouslySetInnerHTML
-
Properly handling user-supplied content
import React, { useState } from 'react';
import DOMPurify from 'dompurify';
// Secure Component Preventing CSTI
const SecureUserProfile = () => {
const [username, setUsername] = useState('');
const [userDescription, setUserDescription] = useState('');
const [profileContent, setProfileContent] = useState(null);
const handleSubmit = (e) => {
e.preventDefault();
// Sanitize user input
const sanitizedUsername = DOMPurify.sanitize(username);
const sanitizedDescription = DOMPurify.sanitize(userDescription);
// Create a safe React element instead of HTML string
const safeProfile = (
<div>
<h2>Welcome, {sanitizedUsername}!</h2>
<p>{sanitizedDescription}</p>
</div>
);
setProfileContent(safeProfile);
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<textarea
value={userDescription}
onChange={(e) => setUserDescription(e.target.value)}
placeholder="User Description"
/>
<button type="submit">Create Profile</button>
</form>
{profileContent}
</div>
);
};
export default SecureUserProfile;
Configuration
The detector has the following configurable parameters:
-
sources
, that indicates the source kinds to check. -
neutralizations
, that indicates the neutralization kinds to check.
Unless you need to change the default behavior, you typically do not need to configure this detector.
References
-
CWE-95 : Improper Neutralization of Data within a Web Page.
-
OWASP Top 10 2021 - A03 : Injection.
-
PortSwigger : Client-side template injection.
-
Use the AOT template compiler in Angular.dev Security.