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