DOM-based cross-site scripting (DOM XSS) happens when data from a
user-controlled source (like a username, or a redirect URL taken from the URL
fragment) reaches a sink, which is a function like eval()
or a property
setter like .innerHTML
that can execute arbitrary JavaScript code.
DOM XSS is one of the most common web security vulnerabilities, and it's common for dev teams to accidentally introduce it in their apps. Trusted Types give you the tools to write, security review, and keep applications free of DOM XSS vulnerabilities by making dangerous web API functions secure by default. Trusted Types are available as a polyfill for browsers that don't yet support them.
Background
For many years DOM XSS has been one of the most prevalent and dangerous web security vulnerabilities.
There are two kinds of cross-site scripting. Some XSS vulnerabilities are caused by server-side code that insecurely creates the HTML code forming the website. Others have a root cause on the client, where the JavaScript code calls dangerous functions with user-controlled content.
To prevent server-side XSS, don't generate HTML by concatenating strings. Use safe contextual-autoescaping templating libraries instead, along with a nonce-based Content Security Policy for additional bug mitigation.
Now browsers can also help prevent client-side DOM-based XSS by using Trusted Types.
API introduction
Trusted Types work by locking down the following risky sink functions. You might already recognize some of them, because browser vendors and web frameworks already steer you away from using these features for security reasons.
- Script manipulation:
<script src>
and setting text content of<script>
elements. - Generating HTML from a string:
- Executing plugin content:
- Runtime JavaScript code compilation:
eval
setTimeout
setInterval
new Function()
Trusted Types require you to process the data before passing it to these sink functions. Using only a string fails, because the browser doesn't know if the data is trustworthy:
anElement.innerHTML = location.href;
To signify that the data was securely processed, create a special object - a Trusted Type.
anElement.innerHTML = aTrustedHTML;
Trusted Types significantly reduce the DOM XSS attack surface of your application. It simplifies security reviews, and lets you enforce the type-based security checks done when compiling, linting, or bundling your code at runtime, in the browser.
How to use Trusted Types
Prepare for Content Security Policy violation reports
You can deploy a report collector, such as the open-source reporting-api-processor or go-csp-collector, or use one of the commercial equivalents. You can also add custom logging and debug violations in the browser using a ReportingObserver:
const observer = new ReportingObserver((reports, observer) => {
for (const report of reports) {
if (report.type !== 'csp-violation' ||
report.body.effectiveDirective !== 'require-trusted-types-for') {
continue;
}
const violation = report.body;
console.log('Trusted Types Violation:', violation);
// ... (rest of your logging and reporting logic)
}
}, { buffered: true });
observer.observe();
or by adding an event listener:
document.addEventListener('securitypolicyviolation',
console.error.bind(console));
Add a report-only CSP header
Add the following HTTP Response header to documents that you want to migrate to Trusted Types:
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
Now all the violations are reported to //my-csp-endpoint.example
, but the
website continues to work. The next section explains how
//my-csp-endpoint.example
works.
Identify Trusted Types violations
From now on, every time Trusted Types detect a violation, the browser sends a
report to a configured report-uri
. For example, when your application
passes a string to innerHTML
, the browser sends the following report:
{
"csp-report": {
"document-uri": "https://my.url.example",
"violated-directive": "require-trusted-types-for",
"disposition": "report",
"blocked-uri": "trusted-types-sink",
"line-number": 39,
"column-number": 12,
"source-file": "https://my.url.example/script.js",
"status-code": 0,
"script-sample": "Element innerHTML <img src=x"
}
}
This says that in https://my.url.example/script.js
on line 39, innerHTML
was
called with the string beginning with <img src=x
. This information should help
you narrow down which parts of code might be introducing DOM XSS and need to change.
Fix the violations
There are a couple of options for fixing a Trusted Type violation. You can remove the offending code, use a library, create a Trusted Type policy or, as a last resort, create a default policy.
Rewrite the offending code
It's possible that the non-conforming code isn't needed anymore, or can be rewritten without the functions that cause the violations:
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
el.innerHTML = '<img src=xyz.jpg>';
Use a library
Some libraries already generate Trusted Types that you can pass to the sink functions. For example, you can use DOMPurify to sanitize an HTML snippet, removing XSS payloads.
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});
DOMPurify supports Trusted Types
and returns sanitized HTML wrapped in a TrustedHTML
object so that the browser
doesn't generate a violation.
Create a Trusted Type policy
Sometimes you can't remove the code causing the violation, and there's no library to sanitize the value and create a Trusted Type for you. In those cases, you can create a Trusted Type object yourself.
First, create a policy. Policies are factories for Trusted Types that enforce certain security rules on their input:
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
createHTML: string => string.replace(/\</g, '<')
});
}
This code creates a policy called myEscapePolicy
that can produce TrustedHTML
objects using its createHTML()
function. The defined rules HTML-escape <
characters to prevent the creation of new HTML elements.
Use the policy like this:
const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML); // true
el.innerHTML = escaped; // '<img src=x onerror=alert(1)>'
Use a default policy
Sometimes you can't change the offending code, for example, if you're loading a third-party library from a CDN. In that case, use a default policy:
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
trustedTypes.createPolicy('default', {
createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
});
}
The policy named default
is used wherever a string is used in a sink that only
accepts Trusted Type.
Switch to enforcing Content Security Policy
When your application no longer produces violations, you can start enforcing Trusted Types:
Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
Now, no matter how complex your web application is, the only thing that can introduce a DOM XSS vulnerability is the code in one of your policies, and you can lock that down even more by limiting policy creation.