Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

We've been toying with this idea in earlier revisions of the spec, basically using the hash as a cache key and not loading the same file from websiteB if it has already been loaded form websiteA.

Unfortunately, this could be used as a cache poisoning attack to bypass Content Security Policy.

See the section about "Content addressable storage" at <https://frederik-braun.com/subresource-integrity.html>.

(If you can come up with a magical solution to this problem, join the W3C web application security group mailing list and send us an email.)



Cache poisoning doesn't make sense when you are using hashes. If someone can generate sha384 collisions in a way that allows them to substitute malicious files in the place of jQuery, we have bigger problems.

> Content injection (XSS)

If we assume XSS, an attacker could simply inject whatever they want. The cache isn't needed. This still wouldn't poison any legitimate cache keys.

> The client still has to find out if the server really hosts this file.

So use (URL, hash) as the key in the permanent cache. This removes most of the bandwidth, and using a CDN allows for one GET per file across many sites.

So what exactly is the attack? I'm really not seeing how someone could attack a permanent cache without first breaking the hashing functions that we already have to trust.

edit: after reading https://news.ycombinator.com/item?id=10311555

This would work in the cases where we allow XSS (which is already a compromised scenario). Simply adding the URL (or maybe even just the hostname) prevents this entirely, and we still get almost all of the benefits for local resources, and we get all of the benefits when using a CDN.

edit2:

There are two issues being discussed. 1) Is the file we loaded form a (possibly 3rd party) site correct? 2) Did we ask for the correct file(s).

Cache poisoning is when you can fool #1, while XSS attacks manipulate #2.


The idea behind content-security policy is that it allows scripts to come only from whitelisted domains. You can't inline evil scripts and you can't link them from any domain. So, in the case of XSS, the attacker CAN'T just do whatever they want. They need to make the browser think that the script is being hosted on a whitelisted domain.

Hence, the attack here is making the victim load that keyed script on a different page, then redirecting them to an XSS hole that links to that script as 'hosted' by a whitelisted domain. Since it seems to be on a whitelisted domain and match the original script's hash, it will execute on the page, which is not ordinarily possible on a page which is running CSP.

I hope this encourages you to not immediately assume that large groups of people working on technically complicated problems are stupid in the future.


This took me a second to understand:

The scenario in question has Protected Site A vulnerable to an XSS attack, but protected from it due to their CSP not allowing scripts from foreign domains (only trusting scripts from `trusted.example.com`). This is what CSP is for: it's not for what you expect to serve, it's for protecting against what you don't expect to serve.

In the theoretical attack content-addressable scripting could open up, the user visits Malicious Site B, which loads a malicious script with the hash `abad1dea`. The owners of Malicious Site B use their XSS attack to insert the (simplified) HTML `<script src="https://trusted.example.com/payload.js" hash="abad1dea">`. If Malicious Site B tried to insert a direct link to their payload at `malicious.example.com/payload.js`, it would be blocked due to the site's CSP - however, if the site trusted the fact that it's seen `abad1dea` from `malicious.example.com` as evidence that it could get the script from `trusted.example.com`, this would open up a vector allowing Malicious Site B to run the `abad1dea` payload in a way that would not be blocked by the CSP. This is why the UA still has to make the request, even though it already has the content.

With the behavior that's been specced, a request will be made to `trusted.example.com` which will either 404 or give a different script, causing the XSS attack to be blocked by the page's CSP.


CSP already has a mechanism for hash-based whitelisting - if this is the only limitation, it'd be just as easy to allow cache-sharing whenever CSP is absent and/or the specific hash is explicitly white-listed.


Exactly, thanks for explaining it a lot more clearly than I did.


If you add the domain, then how is it any different from existing caching? If using a CDN you're already all set; the CDN can return the file with cache forever headers.


> If we assume XSS, an attacker could simply inject whatever they want.

An attacker can inject whatever they want, but they can't run whatever they want. That's the purpose of a Content Security Policy: the problem isn't the content of the script being run, it's the context in which that script is being considered.

Because different scripts are given different permissions (eg. access to cookies) based on their domain of origin, the existence of said content must be verified to be true in the context in which it asserts its presence.

It's not a cache-poisoning attack so much as it is a cache-use attack, but it is a legitimate attack.


>Is the file we loaded form a (possibly 3rd party) site correct?

But there are also parts important to interpreting the file that aren't part of the hash, like the mime type. I think this problem is a lot more complicated than you're saying.


How about an additional attribute named "global", "shared", "public", "use-global-cache", share-with="*", etc. that the developer can use to opt in to the behavior?

A site operator would only opt in to the behavior for assets that are not unique to the site.

A second idea would be to wait until several unique domains had requested the asset before turning on the behavior for that asset. (By unique domain I specifically mean the part of the domain that's written in black text in the URL bar, excluding subdomains that are in gray.)

These are two easiest-to-implement solutions I can think of.


  How about an additional attribute named "global", 
  "shared", "public", "use-global-cache", share-with="*", 
  etc. that the developer can use to opt in to the behavior?
Allowing people to opt-in to a cache poisoning vector seems like a bad idea.

  A second idea would be to wait until several unique domains 
  had requested the asset before turning on the behavior for 
  that asset.
This just raises the bar to a cache poisoning attack from "owns one domain name" to "owns a couple". Some gTLDs are $0.99 per year, or free. (The user would only have to visit a single page, which has a dozen other sites open in invisible iframes)


Could someone explain how cache poisoning would work here? The hash is already being verified, I assume that you would not cache a file if the hash doesn't match, the same way that you would reject a file from the CDN if the hash doesn't match.


There's no hash to verify. Bad site preloads a bad script with hash=123. On good site, XSS injects a script src=bad.js hash=123. The browser, makes a request: GET https://good.com/bad.js. BUT! The hash-cache jumps in and says "Wait, this was requested from the script tag with hash=123. I already have that file. No need to send the request over the network." bad.js now executes in the context of good.com.

If the hash-cache wasn't there, then good.com would have returned a 404. There's no hash collision because the request is completely elided (which is a large part of the perf attractiveness).


Thanks for spelling it out so nicely. I was having a bit of trouble coming up with the scenario too.

And as for the "collisions are unreasonable to expect people to generate", remember the use case: these are going to be extremely long-lived hashes.

With the cache poisoning, once you find a collision against jQuery 2.1.1 (to beat the example horse), you can continue to use that against all requests for jQuery 2.1.1. And we know how wide-applicable targets of cryptographic opportunity typically fair against adversaries with substantial brute-force processing resources...


Once SHA-2 is broken, browsers can simply no longer treat those hashes as safe. The spec suggests browsers don't use anything less than SHA384, including MD5.

The impact of SHA-2 failing would be far, far, larger than poisoning jQuery.


Possibly this could be gotten around with by having the good site serve its own idea of what the hash should be. This would be much smaller than the actual resource.


Well that's what you do, via the HTML. But the concern is that CSP treats the HTML served as untrusted. You could fix it by asking the server, but at that point, you're making a request to the server for the resource, killing some of the point of caching. And it seems wrong to have "if-not-hash" headers as part of this proposal; that'd be better off as an improvement to the HTTP caching stuff overall. But putting the verified hashes in the HTTP headers is fine as CSP already relies on the headers having integrity.


Hi! I'm a bit confused by this.

What would the attacker here be doing, and how? I read the piece on your site, and it's not clear to me what the attacker would be updating, and what effect it would have. Can you explain?


0. evil.com hosts evil.js, <script src=evil.js integrity=foo>.

1. you visit evil.com and the browser stores evil.js with the cache key "foo".

2. you visit victim.com which has an XSS vulnerability, but victim.com thinks it is safe because it uses Content Security Policy and does not allow inline scripts or scripts form evil domains.

3. the XSS attack is loading <script src=www.victim.com/evil.js hash=foo>

4. the browser detects that "foo" is a known hash key and loads the evil.js from cache. Thinking that the file is hosted on victim.com - when the file is in fact not even present.

5. the evil.js script executes in the context of victim.com, even though they use a Content Security Policy to prevent XSS from being exploitable.


I still don't see how it's a problem either way. The browser should check that the hash matches before storing it into the cache so evil.com/evil.js has not the right hash so it's not stored. If they can craft a sha256 collision then, we have other problems anyway and sha256 should be deprecated. If the hashes are the same, then the files are the same.


The hash would be correct. The JS file is the same. The key to the attack is: "the XSS attack is loading <script src=www.victim.com/evil.js hash=foo>". So victim.com was never hosting evil.js and never intended to serve it. The visitor to victim.com gets it because of an XSS vulnerability.

victim.com should be protected because it's content security policy tells the browser not to run scripts from evil.com, but the browser thinks that evil.js came from victim.com, even though victim.com doesn't host evil.js and the browsers cache got evil.js from evil.com.


But if the scenario is an attacker who can inject HTML tags, why wouldn't they simply run their script directly via <script>do_evil();</script>?


Because a properly configured content security policy will block any inlined js (and external js files on non whitelisted domains)


The problem is that www.victim.com/evil.js doesn't exist, and never did, but your browser won't know that if it is in it's cache -- this gives you a way of faking files existing on other servers at the URL of your choices, and as long as they are in the cache you'll get away with it.


Oh I see ! I actually did not consider this case. This requires indeed more reflection than I initially thought.

Edit: But if we only make the global cache work on the same domain, this problem should disapear completely (it's obviously not as powerfull then but still a massive improvement to the current system)


> still a massive improvement to the current system

How so? Browsers have had caches for consistent URIs for some time now.


Yes sure it works but it's still domain based. The browser is still downloading the same copy of jquery 100s of times during a browsing session.


No, the attacker decides the hash, since they inject the <script> tag using XSS.


Maybe use ETag or a similar mechanism? This way we'll need to contact the server, but we save bandwidth that would be spent resending the resource, which is still faster than what we have.

Given something like this:

    <script src="https://code.jquery.com/....." integrity="sha384-R4/....." shared>
(where 'shared' invokes the caching mechanism)

The browser sends a request for it with If-None-Match: "sha384-R4/....." header set.

I think this solves 99% of the problem:

If the integrity tag doesn't match the ETag of the resource, the server interprets it as out-of-date cache and responds with content of that resource. If the integrity tag matches the ETag of the resource, it will respond with '304 Not Modified'.

And that's the remaining 1% of the attack surface: basically the attacker wins iff the site can be tricked into serving a resource with the same ETag as the hash of his payload. We don't need to worry about collisions: even if someone uses ETags that match the form of subresource integrity tags without intending to, the attacker would still need to generate a collision, which is just as hard as finding a collision with any other hash. But if there are servers out there that will serve files with externally-set ETags then they'd be exploitable.


I agree with the guy that said the HEAD request. If you couple that with an "integrity-hash" header that is just like the attribute on the script tag than you can compare the hashes.

What do you think are the downsides to something like that?


That's a clever attack. Possible solution: Have two tables hash -> file and hash -> "set of domains we have verified has the file". If victim.com uses a CSP, then we look in the second table. We see that so far we only know that evil.com has the file. We therefore request the www.victim.com/evil.js and hash it. If it matches, we add it to the set. If it doesn't we bail.

EDIT: Although I guess the current URL based cache may already dedupe, in which case my solution would be roughly equivalent to just turning off hash-based caching for domains with CSP.


What if at least a HEAD request is required?


The browser could submit a special query to get the hash for victim.com/evil.js before running it. If victim.com returns the same hash, it's clean, if it doesn't respond, or responds with a different hash, fail in the same way as if a CDN had modified it.


Thanks!

The original poster said to also use the size. If you include that, my understanding is that crafting a hash collision is into the realms of impossibility.

Am I wrong?

===========

Turns out I was completely misunderstanding. I now do. Thanks!


I'm not seeing how size would prevent the attack. If I understand bugmen0t correctly, in the attack outlined, the file "evil.js" size (and hash and contents) are completely controlled by the attacker. If you did need to specify size, the attack would simply change to:

3. the XSS attack is loading <script src=www.victim.com/evil.js hash=foo size=123>

> If you include that, my understanding is that crafting a hash collision is into the realms of impossibility.

Crafting a hash collision is already in the realm of impossibility. (They're using cryptographic hashes: if you can make SHA256 collide, we have bigger problems.) The attack here isn't that you're getting the wrong file, it's that you're getting a file the webserver does not have, at all. Step 4 is where we go wrong: we load the file from cache, while we should instead request it from the server, which will 404 the request because it does not have the file.

(And it's JS: even if size did matter, you can just add spaces to the end…)


How about you'd do a request saying "hey, I’d like to have victim.com/evil.js, I have a file with hash 93987590837309, is that still up-to-date?", and then you’d hope for 304 Not Modified (and load from evil.com/evil.js), or you’d get a 404 or 200 or whatever.


Sorry, my brain clearly imploded while I was reading that. On re-reading it makes perfect sense. Thanks!


There is no hash collision happening.


I see. Genuine question: I haven't thought this through but why not just execute the file in the context of the original file?


Because if the file was legitimate, the site might need it to run under its own context.


And what if you add filesize checking?

Just do a request and hope that the server returns you a 304 Not-Modified.

This should prevent the issue, right?


Isn't the solution to only allow caching using the key if it's over https (to stop modification) AND in the original HTML (i.e. not added afterwards by JS). Limiting, but would cover above.


If you can inject/alter data over the wire, all protections are usually moot - e.g. you can simply omit the CSP header and inject your code.

Most attackers don't have that capacity, though; XSS is usually done by tricking the page into running your own JS code (for example, by finding a publicly editable text area which doesn't properly escape HTML). Those restrictions wouldn't stop this attack.


6. you just need a collision where evil.js would generate the same sha3sum "foo"


Let a resource have one hash and potentially many source domains. Define a CSP whitelist consisting of trusted domains. This list is applied to resource loading in the browser, and it is also applied to cached resources in the following way:

   cache = {
	b0af301e782bf5e2a8ccce919b6ca3b70aa771db: 
        {domains: ['evil.com','airbnb.com'], content: '...'},
	35d778783c4155c20360d269c9dd000fdcd39548: 
        {domains: ['javajosh.com'], content:'...'}
   }
You go to secure.com, but a malicious user has put the b0af301 script in your path. CSP's white list for secure.com is [secure.com, javajosh.com]. The browser dereferences the hash, checks against the associated domains, and rejects if a whitelisted domain isn't in that list. Your browser running secure.com would reject the b0af301 script.

(Something I personally would like would be for for orgs like EFF.org to post known-good hashes, so I can always add the EFF hashes to my site's CSP whitelist, and have a warm-and-fuzzy feeling.)


Possible solutions:

1. Add an If-Hash-Mismatch header so you don't need to transfer the body

2. Add a list of hashes to accept to the content security policy headers

3. Add a list of public keys to accept to the content security policy, and allow the content if it's signed by one of those (this requires some standard way of signing things, maybe PGP/MIME or a dedicated HTTP header)

4. Only allow this from <script> and <style> tags that are in the <head>, or that are at "end" of <body> (meaning there are no tags other than <script> or <style> afterwards), or resources referenced from CSS and JavaScript files loaded that way.

EDIT: 5. Add an ECC public key (Curve25519?) to the content security policy, and accept hashes where an extra attribute is specified providing an inline signature of the hash with the key

The idea of the last one is that XSS would usually happen in in the middle of the body and not in the head or footer.

That said, you can XSS with inline script, so it seems this only mitigates XSS vulnerability with length limitations on the payload (EDIT: nope, CSP blocks inline script).


CSP allows blocking inline scripts right?

But 1. should exist regardless to complement the existing caching options. It shouldn't be sent by default to avoid adding another tracking method, but if the source page specifies a hash, and you have that hash, then If-Hash-Mismatch is perfect.

2. Bingo, winner.


How about this:

There's a warehouse 'owner' internal to the browser (and not exposed to pages/extension), who 'remembers' the resources the browser has, and the times it took to access them (for commonly-accessed resources). When a page requests the cached resource, the owner 'returns' the resource with a delay of whatever the original access time was, fudged around by some noise.

A weakness of this would be that websites would be able to 'communicate' with each other by engineering response times to your browser, and then checking how long it takes your browser to access that. But this is a different scenario than random websites trying to figure out where else you've been: here the pages need to be in collusion with each other.

The servers might try to use fancy algorithms to try to figure out if you're using cached versions by hitting different distributed servers and figuring out if the resource load time is an outlier. But that's prone to a lot of noise and other issues, and lesser of an issue than the original concern. Right?

This obviously won't help with page load speeds, but will help network load for bot users and servers. One possible issue might be: if you've been in a slow connection previously, all your connections will seem slow even after you are in a faster connection. For that, you can just purge the cache and force the browser to reload the resources.

Edited for formatting.


The danger is not that malicious websites figure out where you've been, the danger is that a malicious website could poison the cache for a critical piece of JS used in, for example, gmail. Visit a malicious site, boom, Russians can read your email.


I think both of these things (history snooping, XSS), plus the Dropbox problem of injecting a hash without ever actually having the file, will need to be addressed.


This would require generating collisions for hash (e.g. sha385). We can trust the hash because SR?I already assumes the hash function works in the integrity="" attribute.


What about using HTTP headers? So websiteB wants to load something by hash, it would have to whitelist it with "AllowedCAS: hash1 hash2 hash3". In fact, CSP already does this for inline scripts. So add another attribute to the CSP header, like 'cache-src' or something, listing good hashes. XSS can't modify the CSP header, so isn't this safe?


If you submit a request with Etag: <integrity>, the server can validate with 304, or deny with 4xx/5xx

I think this would also allow the server to "pre-validate" with HTTP2 push.


And what if you’d just check the filesize? In the same way as you check for modified resources with existing caching methods?


Couldn't you just store the URL and hash together, or salt the hash with the URL?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: