https://bkardell.com/blog/blessing-strings.html
Author Information
Brian Kardell
[profile]
Betterifying the Web
* Developer Advocate at Igalia
* Original Co-author/Co-signer of The Extensible Web Manifesto
* Co-Founder/Chair, W3C Extensible Web CG
* Member, W3C (OpenJS Foundation)
* Co-author of HitchJS
* Blogger
* Art, Science & History Lover
* Standards Geek
Follow Me On...
* Wordpress
* Medium
* Twitter
* Github
* Mastodon
* Bluesky
* Codepen
* LinkedIn
* Instagram (for art)
* Fine Art America (art for sale)
Posted on 04/03/2024
The Blessing of the Strings
Trusted Types have been a proposal by Google for quite some time at
this point, but it's currently getting a lot of attention and work in
all browsers (Igalia is working on implementations in WebKit and
Gecko, sponsored by Salesforce and Google, respectively). I've been
looking at it a lot and thought it's probably something worth writing
about.
The Trusted Types proposal rides atop Content Security Policy (CSP)
and allows website maintainers to say "require trusted-types". Once
required, lots of the Web Platform's dangerous API surfaces ("sinks")
which currently require a string will now require... well, a
different type.
myElement.innerHTML (and a whole lot of other APIs) for example,
would now require a TrustedHTML object instead of just a string.
You can think of TrustedHTML as an interface indicating that a string
has been somehow specially "blessed" as safe... Sanitized.
the Holy Hand grenade scene from Monty Python's Holy Grail And Saint
Attila raised the string up on high, saying, 'O Lord, bless this thy
string, that with it we may trust that it is free of XSS...' [ref].
Granting Blessings
The interesting thing about this is how one goes about blessing
strings, and how this changes the dynamics of development and safety.
To start with, there is a new global trustedTypes object (available
in both window and workers) with a method called .createPolicy which
can be used to create "policies" for blessing various kinds of input
(createHTML, createScript, and createScriptURL). Trusted Types comes
with the concept of a default policy, and the ability for you to
register a specially named "default"...
//returns a policy, but you
// don't really need to do anything
// with the default one
trustedTypes.createPolicy(
"default",
{
createHTML: s => {
return DOMPurify.sanitize(s)
}
}
);
And now, the practical upshot is that all attempts to set HTML will
be sanitized... So if there's some code that tries to do:
// if str contains
// `<img src="no" onerror="dangerous code" >`;
target.innerHTML = str;
Then the onerror attribute will be automatically stripped (sanitized)
before .innerHTML gets it.
Hey that's pretty cool!
one of the scenes where the castle guard is mocking arthur and his
men It's almost like you just put defenses around all that stuff and
can just peer over the wall at would be attackers and make faces at
them....
But wait... can't someone come along then and just create a more
lenient policy called default?
No! That will throw an exception!
Also, you don't have to create a default. If you don't, and someone
tries to use one of those methods to assign a string, it will throw.
The only thing this enforcement cares about is that it is one of
these "blessed" types. Website administrators can also provide (in
the header) the name of 1 or more policies which should be created.
Any attempts to define a policy not in that list will throw (it's a
bit more complicated than that, see Name your Policy below). Let's
imagine that in the header we specified that a policy named
"sanitize" is allowed to be created.
Maybe you can see some of why that starts to get really interesting.
In order to use any of those APIs (at all), you'd need access to a
policy in order to bless the string. But because the policy which can
do that blessing is a handle, it's up to you what code you give it
to...
{
const sanitizerPolicy =
trustedTypes.createPolicy(
"sanitize",
{
createHTML: s => {
return DOMPurify.sanitize(s)
}
);
// give someOtherModule access to a sanitization policy
someOtherModule.init(sanitizerPolicy)
// yetAnotherModule can't even sanitize, any use of those
// APIs will throw
yetAnotherModule.foo()
}
// Anything out here also doesn't have
// access to a sanitization policy
What's interesting about this is that the thing doing the trusting on
the client, is actually on the client as well - but the pattern
ensures that this becomes a considerably more finite problem. It is
much easier to audit whether the "trust" is warranted. That is, we
can look at the above to see that there is only one policy and it
only supports creating HTML. We can see that the trust there is
placed in DOMPurify, and even that amount of trust is only provided
to select modules.
Finally, most importantly: It is a pattern that is machine
enforceable. Anything that tries to use any of those APIs without a
blessed string (a Trusted Type) will fail... Unless you ask it not
to.
Don't Throw, Just Help?
Shutting down all of those APIs after the fact is hard because all of
those dangerous APIs are also really useful and therefore widely
used. As I said earlier, auditing to find and understand all uses of
them all is pretty difficult. Chances are pretty good that there
might just be a lot more unsafe stuff floating around in your site
than you expected.
Instead of Content-Security-Policy CSP headers, you can send
Content-Security-Policy-Report-Only and include a directive that
includes report-to /csp-violation-report-endpoint/ where /
csp-violation-report-endpoint/ is an endpoint path (on the same
origin). If set, whenever violations occur, browsers should send a
request to report a violation to that endpoint (JSON formatted with
lots of data).
The general idea is that it is then pretty easy to turn this on and
monitor your site to discover where you might have some problems, and
begin to work through them. This should be especially good for your
QA environment. Just keep in mind that the report doesn't actually
prevent the potentially bad things from happening, it just lets you
know they exist.
Shouldn't there just be a standard santizer too?
Yes!! That is also a thing that is being worked on.
Name Your Policy
I'm not going to lie, I found CSP/headers to be both a little
confusing to read and to figure out their relationships. You might
see a header set up to report only....
Content-Security-Policy-Report-Only: report-uri /
csp-violation-report-endpoint; default-src 'self';
require-trusted-types-for 'script'; trusted-types one two;
Believe it or not that's a fairly simple one. Basically though, you
split it up on semi-colons and each of those is a directive. The
directive has a name like "report-uri" followed by whitespace and
then a list of values (potentially containing only 1) which are
whitespace separated. There are also keyword values which are quoted.
So, the last two parts of this are about Trusted Types. The first,
require-trusted-types-for is about what gets some kind of enforcement
and really the only thing you can put there currently is the keyword
'script'. The second, trusted-types is about what policies can be
created.
Note that I said "some kind of enforcement" because the above is
"report only" which means those things will report, but not actually
throw, while if we just change the name of the header from
Content-Security-Policy-Report-Only to Content-Security-Policy lots
of things might start throwing - which didn't greatly help my
exploration. So, here's a little table that might help..
If the directives are... then...
You can create whatever policies you want
(missing) (except duplicates), but they aren't
enforced in any way.
You can create whatever policies you want
(except duplicates), and they are enforced.
All attempts to assign strings to those
require-trusted-types-for sinks will throw. This means if you create
'script'; a policy named default, it will 'bless'
strings through that automatically, but it
also means anyone can create any policy to
'bless' strings too.
trusted-types You cannot create any policies whatsoever.
Attempts to will throw.
trusted-types 'none' Same as with no value.
You can call createPolicy with names 'a'
trusted-types a b and 'b' exactly once. Attempts to call with
other names (including 'default'), or
repeatedly will throw.
You can call createPolicy with names
trusted-types default 'default' exactly once. Attempts to call
with other names, or repeatedly will throw.
You can call createPolicy with names 'a'
exactly once. Attempts to call with other
require-trusted-types-for names (including default), or repeatedly
'script'; trusted-types a will throw. All attempts to assign strings
to those sinks will throw unless they are
'blessed' from a function in a policy named
'a'