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

Exposing cross-origin resource size #31

Open
tomvangoethem opened this issue Jun 3, 2016 · 10 comments
Open

Exposing cross-origin resource size #31

tomvangoethem opened this issue Jun 3, 2016 · 10 comments

Comments

@tomvangoethem
Copy link

tomvangoethem commented Jun 3, 2016

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
Copy link
Member

annevk commented Jun 25, 2016

@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
Copy link
Contributor

F2F:

  • We're going with a combination of bucketing and randomisation to reduce the usefulness of this attack to the level of a timing attack
  • This needs to be a high priority fix
  • We should add some non-normative spec test to instruct UAs to do similar things here, but having browsers do it slightly differently reduces the ability to attack

@ehsan
Copy link

ehsan commented Jul 29, 2016

@annevk
Copy link
Member

annevk commented Jul 31, 2016

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

@mathiasbynens
Copy link
Member

@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
Copy link
Member

annevk commented Aug 4, 2016

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
Copy link
Author

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 w3c/resource-timing#64, this allows an attacker to leak content from cross-origin resources.

@annevk
Copy link
Member

annevk commented Aug 24, 2016

@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
Copy link
Author

@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
Copy link
Member

annevk commented Aug 24, 2016

Thank you!

hubot pushed a commit to WebKit/WebKit-http that referenced this issue Oct 10, 2017
https://bugs.webkit.org/show_bug.cgi?id=177552

Patch by Youenn Fablet <youenn@apple.com> on 2017-10-09
Reviewed by Alex Christensen.

Source/WebCore:

Tests: http/wpt/cache-storage/cache-quota.any.html

Storing padded opaque response body sizes within FetchResponse and CacheStorageConnection.
See whatwg/storage#31 for the rationale about this padding.
Storing in CacheStorageConnection is needed for handling cloned network fetched created responses.
Storing in FetchResponse is needed for handling cloned cache-storage created opaque responses.

Adding internals to query and set the fuzzed size of a response.

* Modules/cache/CacheStorageConnection.cpp:
(WebCore::computeRealBodySize):
(WebCore::CacheStorageConnection::computeRecordBodySize):
(WebCore::CacheStorageConnection::setResponseBodySizeWithPadding):
(WebCore::CacheStorageConnection::responseBodySizeWithPadding const):
* Modules/cache/CacheStorageConnection.h:
* Modules/cache/DOMCache.cpp:
(WebCore::DOMCache::toConnectionRecord):
(WebCore::DOMCache::updateRecords):
* Modules/cache/DOMCache.h:
* Modules/cache/DOMCacheEngine.cpp:
(WebCore::DOMCacheEngine::errorToException):
(WebCore::DOMCacheEngine::Record::copy const):
* Modules/cache/DOMCacheEngine.h:
* Modules/cache/WorkerCacheStorageConnection.cpp:
(WebCore::toCrossThreadRecordData):
(WebCore::fromCrossThreadRecordData):
* Modules/fetch/FetchResponse.cpp:
(WebCore::FetchResponse::clone):
(WebCore::FetchResponse::BodyLoader::didReceiveResponse):
* Modules/fetch/FetchResponse.h:
* Modules/fetch/FetchResponse.idl:
* testing/Internals.cpp:
(WebCore::Internals::setResponseSizeWithPadding):
(WebCore::Internals::responseSizeWithPadding const):
* testing/Internals.h:
* testing/Internals.idl:

Source/WebKit:

Adding support for quota checking in CacheStorage::Caches.
It is passed to NetworkProcess at creation time.
Default quota size is configured to 400Ko by origin per default.
This value is suitable for testing.
Future patch should raise this default value and allows configuring it.

Quota is computed based on the response body size.
This size is padded at WebCore for opaque responses.
Size is stored persistently as opaque response padded size should remain stable.
See whatwg/storage#31 for the rationale about this padding.

In case of putting several records at the same time, the size of all records
is computed so that all records will be written or rejected together.

Sending QuotaExceeded error when quota is exceeded.
Future effort should allow asking UIProcess for quota extension.

* NetworkProcess/NetworkProcess.cpp:
(WebKit::NetworkProcess::cacheStoragePerOriginQuota const):
* NetworkProcess/NetworkProcess.h:
* NetworkProcess/NetworkProcessCreationParameters.cpp:
(WebKit::NetworkProcessCreationParameters::encode const):
(WebKit::NetworkProcessCreationParameters::decode):
* NetworkProcess/NetworkProcessCreationParameters.h:
* NetworkProcess/cache/CacheStorageEngine.cpp:
(WebKit::CacheStorage::Engine::readCachesFromDisk):
* NetworkProcess/cache/CacheStorageEngineCache.cpp:
(WebKit::CacheStorage::Cache::toRecordInformation):
(WebKit::CacheStorage::isolatedCopy):
(WebKit::CacheStorage::Cache::open):
(WebKit::CacheStorage::Cache::storeRecords):
(WebKit::CacheStorage::Cache::put):
(WebKit::CacheStorage::Cache::writeRecordToDisk):
(WebKit::CacheStorage::Cache::updateRecordToDisk):
(WebKit::CacheStorage::Cache::removeRecordFromDisk):
(WebKit::CacheStorage::Cache::encode):
(WebKit::CacheStorage::Cache::decodeRecordHeader):
(WebKit::CacheStorage::Cache::decode):
* NetworkProcess/cache/CacheStorageEngineCache.h:
* NetworkProcess/cache/CacheStorageEngineCaches.cpp:
(WebKit::CacheStorage::Caches::Caches):
(WebKit::CacheStorage::Caches::initialize):
(WebKit::CacheStorage::Caches::initializeSize):
(WebKit::CacheStorage::Caches::requestSpace):
(WebKit::CacheStorage::Caches::writeRecord):
(WebKit::CacheStorage::Caches::removeRecord):
(WebKit::CacheStorage::Caches::removeCacheEntry):
* NetworkProcess/cache/CacheStorageEngineCaches.h:
(WebKit::CacheStorage::Caches::create):
(WebKit::CacheStorage::Caches::hasEnoughSpace const):
* NetworkProcess/cache/NetworkCacheStorage.cpp:
(WebKit::NetworkCache::Storage::traverse):
* NetworkProcess/cocoa/NetworkProcessCocoa.mm:
(WebKit::NetworkProcess::platformInitializeNetworkProcessCocoa):
* Shared/WebCoreArgumentCoders.cpp:
(IPC::ArgumentCoder<DOMCacheEngine::Record>::encode):
(IPC::ArgumentCoder<DOMCacheEngine::Record>::decode):
* UIProcess/API/APIProcessPoolConfiguration.cpp:
(API::ProcessPoolConfiguration::copy):
* UIProcess/API/APIProcessPoolConfiguration.h:
* UIProcess/WebProcessPool.cpp:
(WebKit::WebProcessPool::ensureNetworkProcess):

LayoutTests:

* http/wpt/cache-storage/cache-quota.https.any-expected.txt: Added.
* http/wpt/cache-storage/cache-quota.https.any.html: Added.
* http/wpt/cache-storage/cache-quota.https.any.js: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@223073 268f45cc-cd09-0410-ab3c-d52691b4dbfc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

5 participants