W3C home > Mailing lists > Public > public-webappsec@w3.org > June 2016

Re: Finalizing the shape of CSP ‘unsafe-dynamic’

From: Artur Janc <aaj@google.com>
Date: Sat, 4 Jun 2016 00:10:48 +0200
Message-ID: <CAPYVjqq=_EgL3gHvmNJ8S4wrFvUbWVRhf55Z+sNyenGujcwupw@mail.gmail.com>
To: Devdatta Akhawe <dev.akhawe@gmail.com>
Cc: Brad Hill <hillbrad@gmail.com>, WebAppSec WG <public-webappsec@w3.org>, Christoph Kerschbaumer <ckerschbaumer@mozilla.com>, Daniel Bates <dabates@apple.com>, Devdatta Akhawe <dev@dropbox.com>, Mike West <mkwst@google.com>
On Fri, Jun 3, 2016 at 7:39 PM, Devdatta Akhawe <dev.akhawe@gmail.com>

> My understanding was that 'unsafe-dynamic' will just allow all code
> inserted (not parser generated) scripts to just run. The browser doesn't
> really care where the original script that inserted the new script tag came
> from. So, script-src https://foobar.com/ 'unsafe-dynamic' will allow
> inserting a script tag pointing to cdn.foobar.com too. Same with
> hash-src, nonce-src etc etc.

Pretty much, with the caveat that in the example policy above
https://foobar.com/ would be dropped from the whitelist, and no script
would be allowed to run: "[in the presence of 'unsafe-dynamic'] host-source
and scheme-source expressions, as well as the 'unsafe-inline'
keyword-source will be ignored when loading script".

Because host-source and scheme-source is ignored, effectively the only way
a policy with 'unsafe-dynamic' can allow a script to run is to bless it
with a nonce, hash, or by loading a script dynamically (from a script which
was allowed to execute via a nonce, hash, or was itself loaded dynamically).

> --dev
> On 3 June 2016 at 09:31, Brad Hill <hillbrad@gmail.com> wrote:
>> Ah, yes, a very smart approach I hadn't considered.  :)  (and I'm not
>> interested in preventing very determined insiders from skirting the policy
>> as much as I appreciate the opportunity for a conversation presented by
>> "hey, can you update the CSP to support my new featureX?")
>> Are people interested in having the dynamic behavior propagate from
>> scripts identified by a hash as well as a nonce?
>> On Thu, Jun 2, 2016 at 7:05 PM Artur Janc <aaj@google.com> wrote:
>>> On Fri, Jun 3, 2016 at 2:21 AM, Brad Hill <hillbrad@gmail.com> wrote:
>>>> <hat="individual">
>>>> I like unsafe-dynamic a lot but it seems that it tries to fix two
>>>> usability concerns with CSP in a single construct.
>>>> 1) Dealing with "widgets" (e.g. a Like button or ReCAPTCHA) where the
>>>> set of domains from which script will ultimately be required is unknown and
>>>> maybe largely dynamic. (e.g. in the case of google.com ->
>>>> google.${ccTLD} redirections)
>>>> 2) Reducing the attack surface of overly broad whitelists like
>>>> google.com which contain many thousands of scripts, some of which may
>>>> be useful in a ROP-like bypass of the protections intended by CSP.
>>>> It both expands some attack surfaces while contracting others. Is it
>>>> valuable to be able to decompose these two goals?  In particular, is it
>>>> valuable to be able to express something like, "I want only the scripts on
>>>> example.com that are included by this nonce-tagged entry point, but
>>>> nothing not on example.com."  And is it maybe valuable to express that
>>>> side-by-side with a policy that works exactly as the prototype of
>>>> unsafe-inline does today?
>>>> Why I ask....
>>>> At Facebook, I have found CSP script-src to be extremely useful outside
>>>> of protecting from local XSS.  Most modern web applications are terrible at
>>>> what I call "origin hygiene".  They import scripts from dozens of origins.
>>>> SRI can help reduce this attack surface for static libraries, but CSP plays
>>>> an important role, as well.  At Facebook, we don't want our TCB to include
>>>> dozens or hundreds of other domains we don't control, but with thousands of
>>>> developers working on it, someone will with fair frequency try to add a
>>>> runtime javascript dependency on some third party.  CSP lets us catch this
>>>> early and enforce our policies.  I would be very hesitant to deploy an
>>>> unsafe-dynamic policy as currently specified because it would mean losing
>>>> this critical control point.
>>> One thing to note is that if your policy includes a nonce a developer
>>> can already get around it -- they can grab a nonce from any script on the
>>> page, set it on a dynamically created script node, and load arbitrary
>>> scripts from outside of the whitelist. So I'd argue that you are catching
>>> only developers who aren't even seriously trying ;-) (which is probably
>>> still valuable in many cases)
>>> That aside, I agree this is an important concern, and we've very
>>> recently had discussions with several teams at Google who use CSP in the
>>> same way; I know Dev also wants to keep enforcing the script-src whitelist
>>> in his apps for this reason. The good news is that you can already do this
>>> alongside 'unsafe-dynamic' by setting two separate policies (this approach
>>> is courtesy of Dev as well):
>>> script-src 'unsafe-inline' https://cdn.example.com
>>> https://geocaptcha.example.com
>>> script-src 'nonce-123456' 'unsafe-dynamic'
>>> The user-agent will check each script against each policy separately (
>>> https://www.w3.org/TR/CSP2/#enforcing-multiple-policies). The first
>>> policy will only allow external scripts from the whitelisted domains and
>>> the second one will only allow scripts with a nonce (and their
>>> non-parser-inserted descendants); so you get both nonce enforcement and you
>>> make sure that all external scripts come from a whitelisted source. To
>>> borrow your example:
>>> <html>
>>> <!--  Allowed: unsafe-inline (#1) and nonce (#2) -->
>>> <script nonce="123456">
>>>   // Allowed: cdn.example.com (#1) and 'unsafe-dynamic' (#2)
>>>   var s = document.createElement('script');
>>>   s.src = 'https://cdn.example.com/dependency.js';
>>>   document.head.appendChild('s');
>>> </script>
>>> <!-- Allowed: geocaptcha.example.com (#1) and nonce (#2) -->
>>> <script src="https://geocaptcha.example.com/geocaptcha.js
>>> " nonce="123456">
>>> </script>
>>> <!-- XSS vuln here -->
>>> <!-- Blocked: cdn.example.com (#1) but not policy #2 (no valid nonce)
>>> -->
>>> <script src="https://cdn.example.com/csp_bypass.js"></script>
>>> <!-- /XSS vuln -->
>>> </html>
>>> A developer who already has a whitelist-based policy should be able to
>>> deploy this by adding nonces to all scripts and just setting another CSP
>>> header, without any changes to the existing policy. Also, in a production
>>> scenario the second policy would likely need to have other keywords for
>>> backwards compatibility (script-src 'unsafe-inline' 'nonce-123456' http:
>>> https: 'unsafe-dynamic')
>>> There are currently some bugs in Chrome and Safari related to handling
>>> nonces when multiple policies are defined, but the general approach should
>>> work. It's less flexible than the proposal you outlined, because you can't
>>> grant any scripts the ability to bypass the whitelist (like you would with
>>> dynamic-entrypoints) but perhaps it's workable?
>>> Cheers,
>>> -Artur
>>>> Let's say a current policy is:
>>>> script-src 'unsafe-inline' https://cdn.example.com
>>>> https://geocaptcha.example.com 'nonce-abcdefg' 'nonce-123456'
>>>> What if we added two new directives: entrypoints and
>>>> dynamic-entrypoints, where "entrypoints" allowed trust to propagate from
>>>> nonce or hash tagged scriptsf but _only within the whitelist_, and
>>>> "dynamic-entrypoints" has the existing unsafe-dynamic behavior?
>>>> Now I could express something like:
>>>> script-src 'unsafe-inline' https://cdn.example.com 'nonce-abcdefg'
>>>> 'nonce-123456'; entrypoints 'nonce-abcdefg'; dynamic-entrypoints
>>>> 'nonce-123456'
>>>> For a page like:
>>>> <html>
>>>> <script nonce="abcdefg">
>>>>   var s = document.createElement('script');
>>>>   s.src = 'https://cdn.example.com/dependency.js';
>>>>   document.head.appendChild('s');
>>>> </script>
>>>> <script src="https://geocaptcha.example.com/geocaptcha.js
>>>> " nonce="123456"></script>
>>>> <!-- XSS vuln here -->
>>>> <script src="https://cdn.example.com/csp_bypass.js"></script>
>>>> <!-- /XSS vuln -->
>>>> </html>
>>>> In a browser that understands CSP 1.0, unsafe-inline would apply,
>>>> everything would run, including the XSS, but the script from
>>>> geocaptcha.example.com might break if it redirects to
>>>> geocaptcha.example.co.uk.
>>>> In a browser that understands CSP 2.0, the behavior would be identical.
>>>> In a browser that understands these new directives, the XSS would not
>>>> run because it doesn't have a nonce, and so would not be a valid execution
>>>> start point under either entrypoint or dynamic-entrypoint directives.  The
>>>> inline and geocaptcha.js scripts would run. The dependency.js script would
>>>> run because it starts in a valid entrypoint and matches the whitelist.  If
>>>> dependency.js tried to load something from evil.com, that load would
>>>> fail.   If geocaptcha.js tried to load something from
>>>> geocaptcha.example.co.uk (or from evil.com) it would succeed.
>>>> If you wanted the current unsafe-dynamic behavior, you could just add a
>>>> " dynamic-entrypoints * " directive.  If you just wanted to close CSP
>>>> bypasses without expanding beyond your whitelist, you could just add an "
>>>> entrypoints * " directive.  Or you can get fancy like the example above.
>>>>  (Facebook, for example, based on my concerns expressed above, might use a
>>>> nonce-based 'entrypoints' directive to reduce attack surfaces on our site,
>>>> but might never use 'dynamic-entrypoints' or use it only with a very
>>>> tightly-controlled set of hash-sources.)
>>>> But maybe this is way TOO fancy and confusing.
>>>> -Brad
>>>> On Thu, Jun 2, 2016 at 5:30 AM Artur Janc <aaj@google.com> wrote:
>>>>> Hey all,
>>>>> Based on the discussion in the F2F and subsequent conversations with
>>>>> cc'ed folks it seems that most people think that ‘unsafe-dynamic’ [1,2] is
>>>>> a reasonable idea. At the same time, some aspects of the current proposal
>>>>> (e.g. the name itself) could probably be improved; I’d like to use this
>>>>> thread to iron out any remaining bits.
>>>>> <recap>
>>>>> ‘unsafe-dynamic’ is meant to enable the creation of policies that use
>>>>> CSP nonces/hashes to allow script execution instead of the current
>>>>> origin-based source whitelists, which are almost always bypassable by
>>>>> attackers [3]. ‘unsafe-dynamic’ makes adopting nonce-based policies easier
>>>>> by allowing already executing, trusted scripts to dynamically include
>>>>> additional scripts via non-parser-inserted APIs [4], without requiring
>>>>> developers to manually pass around the nonce. Additionally, it makes the
>>>>> user agent ignore the script-src whitelist to allow the creation of secure
>>>>> backwards-compatible policies -- older browsers can keep using the
>>>>> whitelist but supporting browsers will rely solely on nonces.
>>>>> In such a nonce-based approach the developer would specify a policy
>>>>> with a script-src containing a nonce and ‘unsafe-dynamic’, and make sure
>>>>> all <script> elements in the response have a valid `nonce’ attribute -- in
>>>>> many applications this can be much simpler (and more secure) than a
>>>>> whitelist-based policy. So far we’ve had very promising results using this
>>>>> approach with popular JS widgets [5] and in several Google applications,
>>>>> and are working on much broader adoption of this way of using CSP.
>>>>> </recap>
>>>>> The main concerns that folks have brought up are that the name doesn’t
>>>>> match the goal of the feature (a policy with ‘unsafe-dynamic’ will be safer
>>>>> because it won’t be vulnerable to whitelist bypasses, and the ‘unsafe-’
>>>>> prefix could hinder adoption), and that it makes script-src more difficult
>>>>> to reason about. A policy with ‘unsafe-dynamic’ might specify
>>>>> ‘unsafe-inline’ and a host whitelist for backwards compatibility, and
>>>>> depending on the level of the user-agent’s CSP support they will sometimes
>>>>> be respected and sometimes ignored.
>>>>> Overall, I think we have the following options:
>>>>> 1. Leave ‘unsafe-dynamic’ as-is, name and everything.
>>>>> 2. Change the name, but otherwise leave the behavior as specified.
>>>>> Some possibly better names that have been thrown around were
>>>>> ‘allow-dynamic’, ‘trust-dynamic’, ‘dynamic-nonce’ and my favorite
>>>>> ‘safe-dynamic’ ;-) (okay, maybe the last one won’t fly)
>>>>> 3. Change the behavior to make the feature more understandable to
>>>>> developers. Ideas for this included introducing another directive (rather
>>>>> than using script-src), or splitting ‘unsafe-dynamic’ into two keywords
>>>>> (e.g. ‘drop-whitelist’ and ‘propagate-nonces’) -- each of those comes with
>>>>> its own set of challenges, but they are probably solvable if it seems like
>>>>> this is the way to go.
>>>>> My choice would be #2, i.e. to change the name, which AFAIK was the
>>>>> only strong concern raised in our discussions, but otherwise leave the
>>>>> behavior as currently written in the spec. But if anyone feels strongly
>>>>> about #3 or has ideas for a cleaner way to accomplish the same goal, it
>>>>> would be great to discuss it now.
>>>>> Cheers,
>>>>> -Artur
>>>>> [1] https://w3c.github.io/webappsec-csp/#unsafe-dynamic-usage
>>>>> [2]
>>>>> https://lists.w3.org/Archives/Public/public-webappsec/2016Feb/0048.html
>>>>> [3]
>>>>> https://github.com/cure53/XSSChallengeWiki/wiki/H5SC-Minichallenge-3:-%22Sh*t,-it's-CSP!%22
>>>>> In a recent review of CSP deployments in the wild we observed that 75%
>>>>> of whitelists contain origins which allow such CSP bypasses because of
>>>>> Angular/JSONP endpoints. As a result, >95% of current CSP policies offer no
>>>>> benefit from markup injection/XSS.
>>>>> [4] https://www.w3.org/TR/html5/scripting-1.html#parser-inserted
>>>>> The reason to allow only non-parser-inserted APIs to be in scope of
>>>>> ‘unsafe-dynamic’ is that parser-inserted APIs (Element.innerHTML,
>>>>> document.write()) tend to suffer from XSS, which would allow attackers to
>>>>> still exploit such bugs.
>>>>> [5] https://csp-experiments.appspot.com/unsafe-dynamic
Received on Friday, 3 June 2016 22:11:36 UTC

This archive was generated by hypermail 2.4.0 : Friday, 17 January 2020 18:54:56 UTC