Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How does this work when you have a dependency included twice? #222

Closed
bakkot opened this issue Sep 26, 2019 · 24 comments · Fixed by #245
Closed

How does this work when you have a dependency included twice? #222

bakkot opened this issue Sep 26, 2019 · 24 comments · Fixed by #245
Milestone

Comments

@bakkot
Copy link

bakkot commented Sep 26, 2019

It is very common for multiple versions or copies of a library to be present on a page, even within a single bundle. But if I understand correctly only one of those will be able to call createPolicy with that library's policy name (assuming libraries don't choose to include their version number in their policy name, which, since updating a policy name will require reconfiguring one's server, I imagine they would not). The dependency which doesn't win that race will then presumably be broken.

Is there a better story for this than "avoid having multiple copies of a library on your page"?

@koto
Copy link
Member

koto commented Sep 26, 2019 via email

@koto koto added this to the v1 milestone Oct 10, 2019
@koto
Copy link
Member

koto commented Oct 21, 2019

It looks like policies with a duplicate name will be allowed by using a CSP keyword (e.g. 'allow-duplicate'). This is not yet implemented though.

@koto
Copy link
Member

koto commented Oct 31, 2019

The current specced behavior conflates two things when a wildcard * is used in a trusted-types directive.

  1. any policy name is valid
  2. one can create more than one policy with the same name.

I'd like to separate those tho thing, for the purpose of allowing colliding on policy names (e.g. when a dependency is included twice), without forcing authors into losing control over policy names used in their applications totally.

To remind of the threat model - TT are not there to protect agains malicious developers, or malicious dependencies. Some setups of TT may help catch those cases, but we assume that if you already have JS execution in the application, a capablity of calling DOM XSS sink functions with arbitrary values is not really useful (you can already do much more).

So, my proposal is to:

  1. make trusted-types * mean that arbitrary policy names can be used, but a name collision would throw.
  2. introduce an 'allow-duplicate' CSP keyword for the trusted-types directive. Its presence allows for creating new policies with the name that was already allocated. So trusted-types foo bar 'allow-duplicate' will allow to create many policies, but each of them has to be named either foo or bar.

Does that sound good?

/cc @mikewest @otherdaniel @arturjanc

@Siegrift
Copy link
Contributor

Siegrift commented Nov 3, 2019

Does this mean that when using allow-duplicate in trusted-types directive all of the policy names can be duplicated? If all of them, can this "flag" be used anywhere inside directive?

@koto
Copy link
Member

koto commented Nov 3, 2019 via email

@bakkot
Copy link
Author

bakkot commented Nov 3, 2019

A small comment: allow-duplicates does weaken the security model pretty substantially, since now anyone with access to any part of the codebase can use eval as long as they know the name of any policy which is already present.

Has there been any thought given to allowing policy names to occur multiple times, with the behavior that you could create only that many instances of a given policy? So for example Content-Security-Policy: trusted-types foo foo would allow creating two foo policies, but no more. (It may be that this is already allowed and I've missed it.)

@koto
Copy link
Member

koto commented Nov 4, 2019

A small comment: allow-duplicates does weaken the security model pretty substantially, since now anyone with access to any part of the codebase can use eval as long as they know the name of any policy which is already present.

Correct - it does relax the controls over which policies can be created in the code. However the threat model of TT is not to protect against malicious authors (of dependencies or 1st party ones). To protect the application from threats like that in the client alone, an integrity check on the whole codebase - including the dynamically introduced code - would be required. In other words, TT are not to stop XSS attacks via backdorred code - they cannot achieve that. For example, developers able to pass code to eval can just execute the final payload, instead of creating a new lax policy to then trigger a DOM XSS.

Has there been any thought given to allowing policy names to occur multiple times, with the behavior that you could create only that many instances of a given policy? So for example Content-Security-Policy: trusted-types foo foo would allow creating two foo policies, but no more. (It may be that this is already allowed and I've missed it.)

We are thinking on how to enable more precise control, even when some policy names are used multiple times - the repeated name is one example (others were e.g. foo*3) , but so far there's no solution we'd be happy with, as the syntax is not intuitive, which might lead to bugs.

It seems like for now we were just looking into how to define the header (more precisely: directive) syntax that doesn't block us from introducing more precise controls later. The narrow problem we know we need to fix now is to allow name collisions while still controlling the names used, as this in an improvement over how trusted-types * behaves now.

@otherdaniel
Copy link
Member

I think I understand the problem, but I'm not super convinced of the solutions. I wonder: The named policy store is essentially a security-critical, global variable. Who is supposed to have the last word over its content? Whoever writes the CSP? Or each dependency for their own domain?


Brainstorming a few alternatives for discussion purposes:
(Several of these are terrible; I'm expressedly not endorsing any of my own proposals. :-/ )

Alternative:
Have two calls on the factory, createPolicy and createOrRetrievePolicy, where the former will except no duplicate names, while the latter will return the previous policy on duplicate names. (Then a dependency would need to provide for the multiple inclusion case by using the latter.)

Alternative:
Every policy name specifies an arity of sorts, e.g. a ! as suffix for exactly 1 and a * as suffix for many. (Like: trusted-types: abc! def* ghi*)

Alternative:
Allow duplicates, but when duplicate policy names are used implicitly "and" them together. So if I register three policies "mypolicy", then a value only passes if all three policies generate the same value.
(I'd hate to implement this one, but still.)

Alternative:
Dump named policies, and force each user to provide their own store if they want. (That is, a dependency which wishes to export policies needs to provide some way of retrieving them. Then whatever happens with any of their other APIs when included via different paths also happens to policies.) This would presumably necessitate changing the CSP directive from names to source files that are allowed to create policies.
(I'd also hate to implement this one, mainly due to expected ripple effects of last sentence.)

Alternative:
Only allow the top-level script to register named policies. Any dependency that wishes to export one for general use needs to do so using its own APIs, and then hope that the top-level script will endorse it by explicitly putting it into the named store.

@bakkot
Copy link
Author

bakkot commented Nov 4, 2019

However the threat model of TT is not to protect against malicious authors (of dependencies or 1st party ones).

Hm. I had understood from discussions re: tc39/proposal-dynamic-code-brand-checks#1 that part of the purpose of this proposal was to allow code bases to be more effectively audited: that is, to make it so that a security team with control over the CSP of a page could review all current uses of eval in an app and then, crucially, forbid any new ones. That is, the point was not to protect against malicious authors who are directly introducing an attack, but rather to protect against sloppy ones who are accidentally introducing a vulnerability (by an unsafe use of eval).

Is that not the case?

@koto
Copy link
Member

koto commented Nov 4, 2019

However the threat model of TT is not to protect against malicious authors (of dependencies or 1st party ones).

Hm. I had understood from discussions re: tc39/proposal-dynamic-code-brand-checks#1 that part of the purpose of this proposal was to allow code bases to be more effectively audited: that is, to make it so that a security team with control over the CSP of a page could review all current uses of eval in an app and then, crucially, forbid any new ones. That is, the point was not to protect against malicious authors who are directly introducing an attack, but rather to protect against sloppy ones who are accidentally introducing a vulnerability (by an unsafe use of eval).

Is that not the case?

It is the case. Debugging which policy names are still available to use, and intentionally creating one that doesn't violate the explicit restrictions set for the application is hardly accidental.

@Siegrift
Copy link
Contributor

Siegrift commented Nov 4, 2019

Maybe I am still missing something but for me this makes most sense to restrict this as much as possible. I personally, like the following syntax the most (but the syntax is not really important):

trusted-types foo{1} bar{2} foobar{*} with a special case trusted-types * (which allows everything)

This of course contradicts #222 (comment).

I think, that if we can implement the API with stronger policy restrictions, why not implement it? I think the same as in #222 (comment) and I guess the debugging and auditing might be easier with tighter rules. Because if you are able to specify the exact amount of policies, you know exactly how many to look for in the codebase (And I think this is very useful).

@koto
Copy link
Member

koto commented Nov 4, 2019

I think I understand the problem, but I'm not super convinced of the solutions. I wonder: The named policy store is essentially a security-critical, global variable. Who is supposed to have the last word over its content? Whoever writes the CSP? Or each dependency for their own domain?

I'd argue it should be whoever controls the headers (CSP in this case).

Brainstorming a few alternatives for discussion purposes:
(Several of these are terrible; I'm expressedly not endorsing any of my own proposals. :-/ )

Thanks for those, some are quite interesting! I'll put some comments inline.

Alternative:
Have two calls on the factory, createPolicy and createOrRetrievePolicy, where the former will except no duplicate names, while the latter will return the previous policy on duplicate names. (Then a dependency would need to provide for the multiple inclusion case by using the latter.)

That's feels like a reintroduction of the expose flag. The problem I could see is that this can not be used by code that creates intentionally blank policies for the internal usage - so the policies that can potentially collide (or here - be used like singletons) have to be safe. So this probably would have to be opt-in at creation time.

Alternative:
Every policy name specifies an arity of sorts, e.g. a ! as suffix for exactly 1 and a * as suffix for many. (Like: trusted-types: abc! def* ghi*)

That is more precise solution, the only problem I see is the syntax. Perhaps trusted-types foo:1 bar:* ? with :1 being the default suffix?

As such:

  • trusted-types * or trusted-types *:1 would allow you to create arbitrary unique names,
  • trusted-types *:* is YOLO.

Alternative:
Allow duplicates, but when duplicate policy names are used implicitly "and" them together. So if I register three policies "mypolicy", then a value only passes if all three policies generate the same value.
(I'd hate to implement this one, but still.)

Ouch. Sounds like quite surprising behavior (e.g. the code from a different module would run unexpectedly).

Alternative:
Dump named policies, and force each user to provide their own store if they want. (That is, a dependency which wishes to export policies needs to provide some way of retrieving them. Then whatever happens with any of their other APIs when included via different paths also happens to policies.) This would presumably necessitate changing the CSP directive from names to source files that are allowed to create policies.
(I'd also hate to implement this one, mainly due to expected ripple effects of last sentence.)

I didn't fully get it, sorry, but one problem I see is tying this to script URLs. Unfortunately these change often in the application, and the authors don't expect behavior changes just because they repackaged the application, or the algorithm for the bundler changed.

Alternative:
Only allow the top-level script to register named policies. Any dependency that wishes to export one for general use needs to do so using its own APIs, and then hope that the top-level script will endorse it by explicitly putting it into the named store.

The problem here is that dependencies we're talking about can be packaged in various ways, e.g. together with the main script, added as a second <script src=> element, loaded as ES modules, loaded dynamically via createElement('script') etc. It'd be great if TT were agnostic of that.

@bakkot
Copy link
Author

bakkot commented Nov 4, 2019

Debugging which policy names are still available to use, and intentionally creating one that doesn't violate the explicit restrictions set for the application is hardly accidental.

Sure, but neither is any other use of eval. If non-technical controls were sufficient to stop people from using whatever was at hand to build webapps, we wouldn't need trusted types at all.

Perhaps trusted-types foo:1 bar:* ? with :1 being the default suffix?

This is my favorite of the suggestions so far, fwiw.

@xtofian
Copy link

xtofian commented Nov 6, 2019

Perhaps we should think of approaches that remove specific mechanisms for controlling policy creation from the standard, and delegate this function to application-deployer or application-framework provided code.

We are evidently finding it difficult to design an approach based on header-declared named policies that is both sufficiently rigorous (in that it achieves the goal of reasonably strongly confining security-relevant code into areas that are straightforwardly reviewed) and also sufficiently flexible. This suggests that it's possibly a mistake to bake a specific way of controlling policy creation into the standard.

Proposal

Remove the procedure by which policy creation is guarded based on CSP header (section 4.5.3) from the spec. Instead, provide a mechanism by which an application can install exactly one "meta policy" (some thoughts on details thereof below).

This meta policy takes the form of a callback that is invoked by the TrustedTypePolicyFactory when createPolicy is called. The callback decides whether a createPolicy invocation should succeed.

This permits application deployers / security reviewers to control policy creations using a mechanism that is suitable for their environment. In environments where all application source (including third-party dependencies) is processed by a compiler/bundler, more rigorous control can be achieved; in environments where this is not the case, less so.

This "meta policy" callback can control policy creation along a variety of approaches, such as:

monitoring only: Allow all createPolicy calls, but record policy names and stack traces of each createPolicy call. This record can be sent back as telemetry to servers, or stashed in a global where a custom web application scanner can analyse it and look for improper usage.

name-based: Essentially what the current standard proposal does. The "meta policy" callback would refer to a list of allowed policy names, for example emitted by the server into a <meta> tag. To exactly re-create the current behavior, the callback would have to have access to the value of the trusted-types CSP header. Variations would accommodate duplicate policy creation.

call-stack-based: Instead of just looking at policy names, the "meta policy" callback could inspect the stack trace of the createPolicy call to more closely constrain the call-sites of a createPolicy call; this would prevent mis-use of policies by unrelated code.

policy-content-based: Policies could be allowed not based on their name, but rather a hash of the stringified policy code.

name-based, with compiler/bundler-created labels: A compiler (compiler plugin) would collect all createPolicy calls in scope of the JS bundle / assembly and its dependencies. It would generate call-site and bundle-specific policy names/labels, and generate a "meta policy" callback implementation that only admits those compiler-generated labels. (details tbd, the compiler/bundler would probably write generated names into metadata output to permit generation of an umbrella meta-policy that accommodates multiple assemblies loaded into a page).

In the latter approach, the (compiler-generated) policy names/labels function effectively as nonces. This would afford an application deployer / security auditor fairly rigorous control (and opportunity to review) over all code that creates TT policies:

  • Only createPolicy call sites that were "seen" by the compiler/bundler will succeed at runtime, because a callsite not seen by the compiler will not have a label/nonce that will be admitted by the meta policy. (Aside: for this to work we'd have to get rid of the getPolicyNames() API).
  • Hence the compiler (plugin) can provide a comprehensive report of all createPolicy calls to be reviewed. Or, a compiler can statically constrain call sites of createPolicy to a pre-reviewed set (see Closure's JS Conformance Framework).

Security review happens at the source code level. This approach provides a strong binding between code that has been reviewed at the source level (and could be statically identified as security-sensitive) with code that is permitted to create TT policies at runtime.

Meta Policy Callback Registration

This of course begs the question where the code for the "meta policy" callback comes from.

  • Default: The default meta policy callback would allow all createPolicy callbacks (equivalent to 'trusted-types *' in the current spec).

  • Callback Registration: there'd be a setMetaPolicyCallback() function in TrustedTypePolicyFactory. Once setMetaPolicyCallback has been called, subsequent invocations are an error.

  • Policy Registration constrained by CSP: Perhaps there could be a 'trusted-types-policy-src https://example.com/tt-policy.js' CSP directive which causes the meta policy callback implementation to be loaded from the specified URL (this may be difficult to implement).

Note: Libraries should never call setMetaPolicyCallback. This is strictly the job of code that bootstraps a page, and typically would be emitted by a compiler/bundler. Alternatively, generic implementations of "name based" or "monitoring only" meta policies would be instantiated by a <script> tag that occurs early (first) in the page.

Conventions for library authors remain the same as before: They should call createPolicy with a label that is specific to the library. In less rigorous approaches to enforcement (monitoring only, name-based), those labels can be searched for to facilitate ad-hoc code review.

@koto
Copy link
Member

koto commented Nov 6, 2019

I'm worried that this may only work for some authors, namely those for which it's easy to control the JS code that runs in their application and add a code block will be included only once and will run first.
Some applications are just a mashup of various script nodes using different frameworks, and there might be no easy way to add a global bootstrap code equivalent. I'd like us to come up with an
acceptable solution for such authors as well.

We've already had this issue with a default policy - the fact that it's a singleton, and has to be registered early in JS, makes it problematic to use.

The problem indeed is that config in JS is easier to do than in declarative CSP. That said, if CSP syntax would be enough to configure a practical enforcement that's "good enough" for a lot of authors, I think that has a value as well for simplicity purposes. The warning sign is indeed if we try to stretch the CSP syntax too much.

OTOH I see the value of 'your security rules are part of your JS program' approach, I just worry it might be an overkill to ask authors to set up this to gain the benefits of TT. Sometimes the argument comes up that controlling JS is easier than controlling response headers.

At some expense of complexity, we can have both solutions, with authors choosing the best method for their application. We need to make sure that this doesn't affect libraries, that should still simply call createPolicy(name).

We're actually quite close to already supporting the config-in-js. Right now trustedTypes.createPolicy is [Unforgeable], but it it wasn't, the bootstrap code could easily redefine it to add custom behavior before, or after a policy gets created. It should probably be a proper event or callback function instead of such hack, though.

I want us to come up with an extendable MVP solution that doesn't unneccessarily raise the bar of adopting TT. We know already we have to address duplicate names, and that policy creator should specify a name.

How about if we went with a list of names (or a *) + 'allow-duplicates' keyword in CSP, and decide that anything more precise / custom has to be configured otherwise, likely in JS (you then likely want to use trusted-types * 'allow-duplicate').

So we should probably at least remove the [Unforgeable] attribute from createPolicy, or have another way how authors may want to poke at policy creation, and especially reject a policy creation programatically.

Does that sound reasonable to all on a thread? We are trying to come up with some middle ground that works for majority of cases, without locking ourselves out from supporting the other cases as well. I don't feel there's a perfect solution here, there can only be an acceptable one ;)

@koto
Copy link
Member

koto commented Nov 6, 2019

If policy name or rules would only be inspected, but not modified by the JS meta policy callback, and (apart from arbitrary side effects) the decision relevant to TT from the meta callback could be simply to stop the policy creation, then using regular events would work. In an imaginary implementation:

trustedTypes.browserInternalCreatePolicyImpl_ = function(name, rules) {
  const details = {"detail": {
    name: name, 
    rules: /* shallow clone to prevent mutation */ Object.assign({}, rules}}};

  Object.freeze(details);

  var event = new CustomEvent("beforecreatepolicy", details);
  
  if (!trustedTypes.dispatchEvent(event)) {
    // policy creation prevented. console.log something.
    return;
  }
  actuallyCreatePolicy_(name, rules);
}

the following code would work:

trustedTypes.addEventListener('beforecreatepolicy', (e) => {
 // inspect e.details.name or e.details.rules
 // throw an Error to inspect the stack
  e.preventDefault(); // don't let this policy be created.
}); 

In this model, multiple event handlers could be setup. Any of them could cancel policy creation, but none could undo that decision. You get inspectability into the body of the policies (so secrets in policy bodies could be revealed), and you could even call the inner policy functions, but that seems fine (inner policy functions return strings, not trusted types)

Any code can already do that by playing with prototypes, so we're not giving capabilities that don't already exist. It also seems to reuse a model familiar to authors, and gives enough control to implement practical limitations without allowing too much surprising and hard to debug behavior (e.g. the policy rules being suddenly replaced for a dependency from the event handler).

@xtofian
Copy link

xtofian commented Nov 6, 2019

One downside of the event-based approach is that it essentially "fails open" and it's harder to centrally ensure that the event handler is actually in effect. A middleware or other centralized mechanism in a web framework can't easily ensure or verify that the event handler is installed (other than by modifying or inspecting HTML responses, which is pretty awkward).

That's where I was going with

trusted-types-policy-src https://example.com/tt-policy.js

With that, a central server/framework middleware can ensure the custom policy is in place by adding a header.

Other than that, I like the idea of combining the simpler header-based approach with a mechanism that allows custom code to inspect and approve/reject policies. I agree that the meta-policy callback (or event handler, however it's implemented) should not be able to modify policy, only approve/deny.

Re:

We've already had this issue with a default policy - the fact that it's a singleton, and has to be registered early in JS, makes it problematic to use.

It's seems this might be a slightly different situation: The default policy modifies how individual DOM XSS sinks behave, and library authors would likely be inclined to set a default policy because it might remove the need to modify call sites to pass proper types.

With regards to the meta policy, there would be really no incentive for individual libraries to set one. The default is already maximally permissive (or is whatever is given by the headers in your hybrid proposal). There shouldn't be a reason for a library or framework author to further restrict that. IOW, library/framework authors will want to just createPolicy what their code relies on, but not try to restrict what policies are in effect globally.

@xtofian
Copy link

xtofian commented Nov 6, 2019

Pulling up a side-note as a separate comment:

With 'allow-duplicates' in place, I'd be a bit worried that

trustedTypes.createPolicy(trustedTypes.getPolicyNames()[0], {...}}.

becomes a common idiom to create policy without getting a security review.

It might be worth getting rid of getPolicyNames; it doesn't seem all that valuable. For detailed monitoring/telemetry a callback or event that can also see stack traces would be more appropriate.

@otherdaniel
Copy link
Member

"meta policy": I like that it moves more logic into the script, but this does sound like a lot of complexity.

Generally, it strikes me that the combination of script-chosen name plus name-based enforcement has more problems than I would have anticipated.

Maybe an alternative would be to use the CSP to list all the resources that may create policies? (This would be similar to other CSP items, which I think are mostly resource lists.) E.g.:
trusted-types: 'self' *example.com

@Sora2455
Copy link

Sora2455 commented Nov 7, 2019

What if, instead of or in addition to policy names, we included policy hashes in the CSP header instead?

  • It works with bundling tools (they can generated the hashes at build time) and with manual building
  • It resolves the "multiple hashes with the same name" issue (once you decide if the first or last policy with that name takes precedence)
  • No need to stress out about "meta policies" or other complex logic
  • It even has precedence in CSP itself

@koto
Copy link
Member

koto commented Nov 8, 2019

What if, instead of or in addition to policy names, we included policy hashes in the CSP header

I don't think CSP, given even its syntax limitations is the best medium for introducing that precise control over the policies. For example, hashing is sensitive even to whitespace changes, and syncing a JS bundle with what headers are emitted is not a trivial task for every author. I'd rather the CSP controls the baseline checks (e.g. policy names) that may give enough control for most authors, and move more precise rules to JS.

Paradoxically, only being able to whitelisting by hash vs policy names might offer less security. I imagine there might be policies that are less strict in their rules, and end up being secure because the data that flows to them is trusted (e.g. is sanitized server side, or comes directly from the API). It would still be advisable if such lax policy rules are not used throughout the application, but only looking at the hashes one cannot prevent that.

@koto
Copy link
Member

koto commented Nov 8, 2019

Maybe an alternative would be to use the CSP to list all the resources that may create policies?

That forces the authors to restructure how their application is bundled i.e. to get the benefits they have to move the TT logic not just to separate functions, but to separate bundles.

In general whitelisting URLs via static lists didn't work so well for script-src (it is not easy to maintain a URL whitelist, and the rules end up being generic enough to allow for bypasses as URLs change too frequently). Additionally we might be facing SOP limitations when URLs redirect cross-origin (we could only use the origin granularity because of https://lists.w3.org/Archives/Public/public-webappsec/2014Feb/0036.html). And defining that policies in my application may be created in scripts from this CDN is ... less than ideal ;)

@koto
Copy link
Member

koto commented Nov 13, 2019

One downside of the event-based approach is that it essentially "fails open" and it's harder to centrally ensure that the event handler is actually in effect.

The flexibility of defining rules in the JS as opposed to declaratively indeed makes asserting that the rules were installed more tricky. That said, there are easy ways of making certain a given function was called in a runtime - just make it send a beacon and warn on its absence. In general, the closer the security rules are to your program, the easier they are to test, and the less likely they are to go out of sync and break your application.

Any other method of defining the rules can fail as well (a CSP header is not sent, there's a typo in a header name, the script 404s etc.).

I actually like the decentralization here (i.e. that there may be multiple event handlers), as that allows for modularity, iow creating experiments for a subset of audience, implementing custom report-only behavior for a subset of policies etc.

On a more philosophical note: Perhaps asserting security posture by looking at the headers only gives us a false impression that we're actually controlling anything and are delivering value, as it's hard to see the bugs from such a vantage point. Maybe we should just switch to more dev-friendly approaches even if it requires rewiring some of the security controls we're used to.

@koto
Copy link
Member

koto commented Dec 2, 2019

#245 allows for duplicate names by two means:

  • require-trusted-types-for 'script' without trusted-types directive removes all name restrictions (also allowing duplicate names)
  • require-trusted-types-for 'script'; trusted-types foo bar 'allow-duplicates' allows for foo and bar names to be created multiple times.

Recently, we added character set restrictions for policy names, allowing us to introduce a new syntax later - it does feel like a JS-based mechanism for restricting policy creation would be a better fit for authors that want more precise control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants