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:
Don't
anElement.innerHTML = location.href;
With Trusted Types enabled, the browser throws a
TypeError
and prevents
use of a DOM XSS sink with a string.
To signify that the data was securely processed, create a special object - a Trusted Type.
Do
anElement.innerHTML = aTrustedHTML;
With Trusted Types enabled, the browser accepts a
TrustedHTML
object for sinks that expect HTML snippets. There are also
TrustedScript
and
TrustedScriptURL
objects for other
sensitive sinks.
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
go-csp-collector
, or use one
of the commercial equivalents. You can also debug violations in the browser:
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:
Do
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
Don't
el.innerHTML = '
';
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
.
Further reading