whatwg / storage

Storage Standard
https://storage.spec.whatwg.org/
Other
124 stars 55 forks source link

Exposing cross-origin resource size #31

Open tomvangoethem opened 8 years ago

tomvangoethem commented 8 years ago

Size-exposing attacks

In its current form, the Storage specification makes it very likely for user agents to develop an implementation that allows attackers to leak the size of opaque Responses. There are at least 3 methods that could be used to do this:

Estimate usage and quota

The Fetch standard describes a method (estimate()) that returns a "rough estimate of the amount of bytes used" and "a conservative estimate of the amount of bytes available". As the terms "rough estimate" and "conservative estimate" are not strictly defined, and security considerations are not mentioned in the standard, all bets are off as to what user agents might come up with (Chrome's current implementation, which is currently still behind a flag, seems to return the exact usage instead of an estimate). An attack that leaks the exact resource size is straightforward, even when a "rough estimate" would be given:

  1. Get estimate
  2. Fetch resource
  3. Store Response in cache
  4. Get estimate, and subtract value from (1).

In case the estimate is used to obscure the resource size, repeat steps (3) and/or (4). E.g. if estimate() is implemented to round to the nearest kB value, storing the resource 1k times will give you the exact size.

Per-site quota

Each site has their own fixed quota, and when trying to store something that doesn't fit in storage, this will obviously fail. These features can be abused to leak the response size in the following way:

  1. Completely fill up site's storage
  2. Free up something like 5MB of storage
  3. Fetch resource, and store Response in cache
  4. Fill up storage byte per byte until this fails
  5. Calculate resource size as 5MB - num_fill_bytes

    Global quota

Similar to the per-site quota, there's also a global quota, and user agents will free up space by first clearing non-persistent boxes. This provides the same properties as above to obtain the exact resource size. An attack looks as follows:

  1. Fill up storage on multiple sites in order to trigger eviction of all other non-persistent boxes not under your control
  2. Force a single box you know the size of to be evicted
  3. In a new origin, fetch resource and store Response in cache
  4. Fill up storage byte per byte until one of your origins gets evicted
  5. Calculate resource size as size_first_evicted_box - num_fill_bytes

Compared to the previous attacks, this one is slightly harder to exploit (especially since the global quota can be substantial), but given the high storage speeds (especially with SSD) the attack is still very practical.

_Note:_ For all browsers that already implement one of the above (i.e. virtually every browser), we managed to devise an attack that exposes the exact size of any resource.

Consequences

Being able to determine the resource size of arbitrary Response objects poses various privacy and security issues. For example, we found that by knowing the exact size of just 5 resources on twitter.com (i.e. https://twitter.com/following, https://twitter.com/followers, …) it is possible to uniquely identify a user from a large set. In an experiment on 500k user profiles, we found that a user could be uniquely identified in 97.62% of the cases. Of course, since virtually every website is sending state-specific responses, the consequences are not just limited to this example, and are applicable to a large number of web services.

Mitigation

Having a usable solution that completely eradicates all size-exposing vectors seems unlikely. Instead, I think it's best to have a solution that limits the practicability that of to existing attacks (i.e. timing attacks). As such, I'd like to suggest an approach where "virtual padding" is applied on Response objects. More concretely: upon creation of a Response object, for instance as the result of a fetch() operation, choose a random value r between 0 and rMax. Next, round up Response.size + r to a multiple of D. The "virtual padding" is then the rounded up value minus Response.size. Note that when a Response object is cloned, it should inherit the same padding value. The padding is virtual in the sense that it is not actually written do disk. Instead, the user agent just uses it as part of its storage bookkeeping, and will also use these values to provide usage/quota estimates. This method works quite well as it prevents an attacker to quickly obtain many measurements (for each measurement, a fetch() operation is required). For values of rMax and D, I'd suggest 100kb and 20kb respectively. Even after 50 measurements, these values seem to obfuscate the actual size somewhere in the range of 4.5kb (for 10 measurements, this is approximately 10kb). I made a little script that allows you to play around with the values a bit, but note that most likely a better method can be used to improve the accuracy of attacks, so these values should only be seen as an upper bound.

annevk commented 8 years ago

@tomvangoethem just to be clear, your research and proposed solution are very much appreciated. I'm trying to get some feedback from implementers to see if they agree.

jakearchibald commented 8 years ago

F2F:

ehsan commented 8 years ago

Gecko bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1290481

annevk commented 8 years ago

Would browsers having different tactics not make those with the worst tactics simply more vulnerable?

mathiasbynens commented 8 years ago

@annevk The tactics could be the same (bucketing + randomization) but the parameters could/should differ to make it harder to write code that performs a timing attack in different browsers.

annevk commented 8 years ago

If that is the case they should probably also differ for a single browser (i.e., be some kind of random input). Otherwise market leaders or niche markets are still vulnerable. And I doubt attackers would hesitate to add some browser sniffing into the mix.

tomvangoethem commented 8 years ago

When the opaque response is compressed, and the Content-Length header is present, it's possible to launch a compression-based attack (described in more detail here) with this size-exposing attack. Similar to https://github.com/w3c/resource-timing/issues/64, this allows an attacker to leak content from cross-origin resources.

annevk commented 8 years ago

@tomvangoethem if we fix this and cannot find a fix for the HEIST issue (other than asking everyone to use same-site cookies...) are we actually making progress?

tomvangoethem commented 8 years ago

@annevk HEIST provides the attacker with the response size after compression, this one provides the attacker with the uncompressed response size. Knowing either is bad, knowing both is worse. While I'd much rather see a generic solution such as disabling 3rd-party cookies by default (or something that provides a transition there, as is being done with the transition to HTTPS), this issue is independent from HEIST, and if there's no viable generic solution, the issues should be mitigated one by one. In addition, I'd say this issue is easier to exploit as it's more stable and convenient (especially when estimate() gives the exact usage).

annevk commented 8 years ago

Thank you!