CSP: Using Trusted Types Policies for Enhanced Web Security

Pavlo DatsiukPavlo Datsiuk
6 min read

Modern web applications must guard against cross-site scripting (XSS) and similar injection attacks. Trusted Types is a browser feature designed to enforce strict handling of sensitive data—such as script URLs and HTML—by only allowing values that have been vetted by developer-defined policies. In this article, we explore how Trusted Types policies work, address common questions about policy creation and reuse, and examine potential risks—like global variable overrides—and how to mitigate them.


1. What Is a Trusted Types Policy and How Does createScriptURL Work?

At its core, a Trusted Types policy is a developer-defined set of rules that converts untrusted input (usually raw strings) into “trusted” objects. One key method is createScriptURL:

  • Purpose:
    It takes a raw URL string and validates it—typically ensuring it comes from an allowed domain—then returns a TrustedScriptURL object. This trusted object can then safely be assigned to a <script> element’s src attribute.

  • Example:

      const myPolicy = trustedTypes.createPolicy('myPolicy', {
        createScriptURL: (url) => {
          if (url.startsWith('https://trusted-domain.com/')) {
            return url; // Implicitly converted to a TrustedScriptURL
          }
          throw new Error('Invalid script URL');
        }
      });
    

    In this example, only URLs starting with https://trusted-domain.com/ are allowed. If a URL doesn’t meet this criterion, an error is thrown, preventing a malicious script from being loaded.


2. How Do Trusted Types Prevent Attacks?

Trusted Types enforce a strict discipline:

  • Sanitization Gate:
    By requiring that sensitive sinks (like script.src) accept only objects created by a Trusted Types policy, the browser prevents direct assignment of raw, potentially dangerous strings.

  • Immutable Policy Behavior:
    Once a policy is created, its rules are fixed. This means that even if an attacker manages to run some script, they cannot change how your policy sanitizes input.

  • CSP Integration:
    When paired with a Content Security Policy (CSP) directive (e.g., require-trusted-types-for 'script'), the browser rejects any script-related operations that do not use a TrustedScriptURL.


3. How Is Policy Creation Protected from Malicious Injection?

One common question is:
“What prevents a hacker from injecting code like window.trustedTypes.createPolicy('myPolicy', …) and making their own ‘trusted’ policy?”

The answer lies in several key mechanisms:

  • Unique Policy Names:
    A policy must be created with a unique name. If your application has already defined myPolicy, any attempt to recreate it will fail. This prevents an attacker from simply overriding your trusted configuration.

  • Content Security Policy (CSP):
    CSP directives such as

      Content-Security-Policy: script-src 'self' https://trusted-cdn.com; trusted-types myPolicy; require-trusted-types-for 'script'
    

    enforce that only policies with the allowed name(s) can be used and that all script sinks require Trusted Types. Any policy injected with a different name would be blocked.

  • Immutable Policy Objects:
    Even if an attacker somehow accesses the original policy object, its internal rules (e.g., the logic inside createScriptURL) cannot be modified.


4. Reusing a Trusted Types Policy Across Your Application

You might wonder, “Can I retrieve an already created policy and apply it in different parts of my website?”
The answer is yes. When you create a Trusted Types policy, you typically store the returned object in a variable and reuse it across your modules:

  • Example Using a Module:

      // In policy.js
      export const myPolicy = trustedTypes.createPolicy('myPolicy', {
        createScriptURL: (url) => {
          if (url.startsWith('https://trusted-domain.com/')) {
            return url;
          }
          throw new Error('Invalid URL');
        }
      });
    
      // In main.js
      import { myPolicy } from './policy.js';
    
      function loadScript(url) {
        const trustedUrl = myPolicy.createScriptURL(url);
        const script = document.createElement('script');
        script.src = trustedUrl;
        document.body.appendChild(script);
      }
    
      loadScript('https://trusted-domain.com/script.js');
    

    This approach ensures consistent sanitization across your application without redefinition.


5. What Happens When You Attempt to Override or Create Duplicate Policies?

By design, the Trusted Types API does not allow redefinition of an existing policy with the same name. If you try to create a policy named myPolicy twice, the second call will throw an error.
However, there is a twist when using CSP:

  • Using allow-duplicates:
    With a CSP directive like

      Content-Security-Policy: trusted-types myPolicy 'allow-duplicates'
    

    you are instructing the browser to permit multiple policy instances with the same name.

    • Impact: Even though two distinct policy objects (say, policy1 and policy2) may share the name myPolicy, each retains its own rules. When you explicitly call policy1.createScriptURL(url), it uses the rules defined in policy1—the existence of policy2 does not affect it.

    • Best Practice: Generally, you should avoid creating duplicate policies to maintain clarity and reduce risk.


6. Global Exposure and the Risk of Policy Overriding

A critical security question is:
“If I attach my policy instance to the global window object (e.g., window.myPolicy), can an attacker override it, causing subsequent code to use a malicious policy?”

The Risk

  • Global Variables Are Mutable:
    Although the policy object itself (once created) cannot be modified, the variable window.myPolicy can be reassigned by any script in the same execution context. If an attacker injects code that runs before your other modules, they could reassign window.myPolicy to a new, malicious policy.

  • Potential Impact:
    Code that runs later and references window.myPolicy would unknowingly use the attacker’s policy. This could allow the attacker to bypass your intended sanitization logic and load malicious scripts.

Mitigation Strategies

To prevent this vulnerability:

  • Encapsulate the Policy:
    Instead of storing the policy in a globally accessible variable, encapsulate it within a module or closure. For example:

      const myModule = (() => {
        const myPolicy = trustedTypes.createPolicy('myPolicy', {
          createScriptURL: (url) => {
            if (url.startsWith('https://trusted-domain.com/')) {
              return url;
            }
            throw new Error('Invalid URL');
          }
        });
        return {
          loadScript: (url) => {
            const trustedUrl = myPolicy.createScriptURL(url);
            const script = document.createElement('script');
            script.src = trustedUrl;
            document.body.appendChild(script);
          }
        };
      })();
    
      myModule.loadScript('https://trusted-domain.com/script.js');
    

    This approach minimizes the risk by keeping myPolicy private.

  • Strict CSP without allow-duplicates:
    Removing the allow-duplicates directive (when possible) enforces unique policy creation, making it harder for an attacker to create a competing policy.

  • Minimize Global Exposure:
    Only expose what is absolutely necessary to the global scope. Keeping security-critical objects out of window reduces the attack surface.


7. Conclusion

Trusted Types is a powerful security tool for mitigating XSS by forcing all sensitive data (like script URLs) through strict, developer-defined sanitization policies. When used correctly, they ensure that only validated input reaches critical DOM sinks. However, as our discussion reveals:

  • Policy Creation and Reuse:
    Policies must be created once and reused consistently. Although CSP can allow duplicates via allow-duplicates, doing so comes with risks that must be managed carefully.

  • Global Variable Risks:
    Attaching a policy to a global object (e.g., window.myPolicy) can expose it to potential reassignment by malicious code. Encapsulation in modules and a strict CSP can help mitigate this risk.

By understanding these subtleties and following best practices—like encapsulating policies and limiting global exposure—you can leverage Trusted Types to greatly enhance your web application’s security.

0
Subscribe to my newsletter

Read articles from Pavlo Datsiuk directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pavlo Datsiuk
Pavlo Datsiuk

🚀