Commit a8e28e15 authored by Robbie Gibson's avatar Robbie Gibson Committed by Commit Bot

[iOS] Block CacheStorage API from Javascript

This adds CacheStorage blocking to the list of other storage methods
blocked as part of the cookie blocking process on iOS. To more closely
emulate desktop, it returns a rejected promise with the same error
message.

Bug: 1085123
Change-Id: If6ceed88e601b594c74c2580c26f8a183c9fba7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2247883Reviewed-by: default avatardpapad <dpapad@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Commit-Queue: Robbie Gibson <rkgibson@google.com>
Cr-Commit-Position: refs/heads/master@{#780187}
parent e45ad808
......@@ -48,6 +48,9 @@ NSString* const kLocalStorageErrorMessage =
NSString* const kSessionStorageErrorMessage =
@"Failed to read the 'sessionStorage' property from 'window': Access is "
@"denied for this document";
NSString* const kCacheNotAvailableErrorMessage = @"Can't find variable: caches";
NSString* const kCacheErrorMessage = @"An attempt was made to break through "
@"the security policy of the user agent.";
}
namespace web {
......@@ -549,6 +552,106 @@ TEST_F(CookieBlockingTest, SessionStorageBlockedUndeletable) {
EXPECT_NSEQ(nil, result);
}
// Tests that Cache Storage is accessible from JavaScript in frames
// when the blocking mode is set to allow.
TEST_F(CookieBlockingTest, CacheStorageAllowed) {
__block bool success = false;
GetBrowserState()->SetCookieBlockingMode(CookieBlockingMode::kAllow,
base::BindOnce(^{
success = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return success;
}));
// Use arbitrary third party url for iframe.
GURL iframe_url = third_party_server_.GetURL(kIFrameUrl);
std::string url_spec = kPageUrl + net::EscapeQueryParamValue(
iframe_url.spec(), /*use_plus=*/true);
test::LoadUrl(web_state(), server_.GetURL(url_spec));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return web_state()->GetWebFramesManager()->GetAllWebFrames().size() == 2;
}));
bool one_frame_succeeded = false;
for (WebFrame* frame :
web_state()->GetWebFramesManager()->GetAllWebFrames()) {
NSString* error_message;
EXPECT_TRUE(
web::test::SetCache(frame, web_state(), @"x", @"value", &error_message))
<< FailureMessage(frame);
if ([error_message isEqualToString:kCacheNotAvailableErrorMessage]) {
// Sometimes, the Cache API is not available. In these cases, the test
// shouldn't fail.
continue;
}
EXPECT_NSEQ(nil, error_message) << FailureMessage(frame);
error_message = nil;
NSString* result;
EXPECT_TRUE(
web::test::GetCache(frame, web_state(), @"x", &result, &error_message))
<< FailureMessage(frame);
EXPECT_NSEQ(nil, error_message) << FailureMessage(frame);
EXPECT_NSEQ(@"value", result) << FailureMessage(frame);
one_frame_succeeded = true;
}
EXPECT_TRUE(one_frame_succeeded);
}
// Tests that Cache Storage is blocked from JavaScript in frames
// when the blocking mode is set to blocked.
TEST_F(CookieBlockingTest, CacheStorageBlocked) {
__block bool success = false;
GetBrowserState()->SetCookieBlockingMode(CookieBlockingMode::kBlock,
base::BindOnce(^{
success = true;
}));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return success;
}));
// Use arbitrary third party url for iframe.
GURL iframe_url = third_party_server_.GetURL(kIFrameUrl);
std::string url_spec = kPageUrl + net::EscapeQueryParamValue(
iframe_url.spec(), /*use_plus=*/true);
test::LoadUrl(web_state(), server_.GetURL(url_spec));
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
return web_state()->GetWebFramesManager()->GetAllWebFrames().size() == 2;
}));
bool one_frame_succeeded = false;
for (WebFrame* frame :
web_state()->GetWebFramesManager()->GetAllWebFrames()) {
NSString* error_message;
EXPECT_TRUE(
web::test::SetCache(frame, web_state(), @"x", @"value", &error_message))
<< FailureMessage(frame);
if ([error_message isEqualToString:kCacheNotAvailableErrorMessage]) {
// Sometimes, the Cache API is not available. In these cases, the test
// shouldn't fail.
continue;
}
EXPECT_NSEQ(kCacheErrorMessage, error_message) << FailureMessage(frame);
error_message = nil;
NSString* result;
EXPECT_TRUE(
web::test::GetCache(frame, web_state(), @"x", &result, &error_message))
<< FailureMessage(frame);
EXPECT_NSEQ(kCacheErrorMessage, error_message) << FailureMessage(frame);
EXPECT_NSEQ(nil, result) << FailureMessage(frame);
one_frame_succeeded = true;
}
EXPECT_TRUE(one_frame_succeeded);
}
// Tests that the cookies sent in HTTP headers are allowed.
TEST_F(CookieBlockingTest, RequestCookiesAllowed) {
__block bool success = false;
......
......@@ -10,6 +10,7 @@
namespace web {
class WebFrame;
class WebState;
namespace test {
......@@ -33,12 +34,12 @@ bool SetLocalStorage(WebFrame* web_frame,
NSString** error_message);
// Reads the value for the given |key| from local storage on |web_frame| and
// places it in |value|. If |error_message| is provided, then if an error
// places it in |result|. If |error_message| is provided, then if an error
// occurs, it will be filled with the error message. Returns true on success and
// false on failure.
bool GetLocalStorage(WebFrame* web_frame,
NSString* key,
NSString** value,
NSString** result,
NSString** error_message);
// Stores a given |key|, |value| in session storage on |web_frame|. If
......@@ -50,14 +51,33 @@ bool SetSessionStorage(WebFrame* web_frame,
NSString** error_message);
// Reads the value for the given |key| from session storage on |web_frame| and
// places it in |value|. If |error_message| is provided, then if an error
// places it in |result|. If |error_message| is provided, then if an error
// occurs, it will be filled with the error message. Returns true on success and
// false on failure.
bool GetSessionStorage(WebFrame* web_frame,
NSString* key,
NSString** value,
NSString** result,
NSString** error_message);
// Stores a given |key|, |value| in cache storage on |web_frame| + |web_state|.
// If |error_message| is provided, then if an error occurs, it will be filled
// with the error message. Returns true on success and false on failure.
bool SetCache(WebFrame* web_frame,
WebState* web_state,
NSString* key,
NSString* value,
NSString** error_message);
// Reads the value for the given |key| from session storage on |web_frame| +
// |web_state| and places it in |result|. If |error_message| is provided, then
// if an error occurs, it will be filled with the error message. Returns true on
// success and false on failure.
bool GetCache(WebFrame* web_frame,
WebState* web_state,
NSString* key,
NSString** result,
NSString** error_message);
} // namespace test
} // namespace web
......
......@@ -11,6 +11,7 @@
#import "base/test/ios/wait_util.h"
#include "base/values.h"
#include "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/web_state.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
......@@ -91,16 +92,15 @@ bool SetStorage(web::WebFrame* web_frame,
return success && set_success;
}
// Reads the value for the given |key| from session storage on |web_frame| using
// the
// __gCrWeb function |name|. The read value will be placed in |value| and any
// JavaScript error will be placed in |error_message|.
// Reads the value for the given |key| from storage on |web_frame| using
// the __gCrWeb function |name|. The read value will be placed in |result| and
// any JavaScript error will be placed in |error_message|.
bool GetStorage(web::WebFrame* web_frame,
const std::string& get_function,
NSString* key,
NSString** value,
NSString** result,
NSString** error_message) {
__block NSString* result;
__block NSString* block_result;
__block NSString* block_error_message;
__block bool lookup_success = false;
std::vector<base::Value> params;
......@@ -109,7 +109,7 @@ bool GetStorage(web::WebFrame* web_frame,
web_frame, get_function, params,
base::BindOnce(^(const base::Value* value) {
if (value->is_string()) {
result = base::SysUTF8ToNSString(value->GetString());
block_result = base::SysUTF8ToNSString(value->GetString());
lookup_success = true;
} else if (value->is_dict()) {
block_error_message =
......@@ -124,8 +124,8 @@ bool GetStorage(web::WebFrame* web_frame,
if (error_message) {
*error_message = block_error_message;
}
if (value) {
*value = result;
if (result) {
*result = block_result;
}
return success && lookup_success;
}
......@@ -170,9 +170,9 @@ bool SetLocalStorage(WebFrame* web_frame,
bool GetLocalStorage(WebFrame* web_frame,
NSString* key,
NSString** value,
NSString** result,
NSString** error_message) {
return GetStorage(web_frame, "cookieTest.getLocalStorage", key, value,
return GetStorage(web_frame, "cookieTest.getLocalStorage", key, result,
error_message);
}
......@@ -186,11 +186,107 @@ bool SetSessionStorage(WebFrame* web_frame,
bool GetSessionStorage(WebFrame* web_frame,
NSString* key,
NSString** value,
NSString** result,
NSString** error_message) {
return GetStorage(web_frame, "cookieTest.getSessionStorage", key, value,
return GetStorage(web_frame, "cookieTest.getSessionStorage", key, result,
error_message);
}
bool SetCache(WebFrame* web_frame,
WebState* web_state,
NSString* key,
NSString* value,
NSString** error_message) {
// Cache is an async api. The test injected javascript will send a message
// when the async is done, so listen for that here.
__block bool async_success = false;
__block NSString* block_error_message;
std::unique_ptr<WebState::ScriptCommandSubscription> subscription_ =
web_state->AddScriptCommandCallback(
base::BindRepeating(^(const base::DictionaryValue& message,
const GURL& page_url, bool user_is_interacting,
web::WebFrame* sender_frame) {
const base::Value* result = message.FindPath("result");
if (!result) {
return;
}
if (result->is_bool()) {
async_success = result->GetBool();
} else {
block_error_message = base::SysUTF8ToNSString(
result->FindPath("message")->GetString());
async_success = true;
}
}),
"cookieTest");
if (!SetStorage(web_frame, "cookieTest.setCache", key, value, nil)) {
return false;
}
bool success =
WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return async_success;
});
if (error_message) {
*error_message = block_error_message;
}
return success;
}
bool GetCache(WebFrame* web_frame,
WebState* web_state,
NSString* key,
NSString** result,
NSString** error_message) {
// Cache is an async api. The test injected javascript will send a message
// when the async is done, so listen for that here.
__block bool async_success = false;
__block NSString* block_result;
__block NSString* block_error_message;
std::unique_ptr<WebState::ScriptCommandSubscription> subscription_ =
web_state->AddScriptCommandCallback(
base::BindRepeating(^(const base::DictionaryValue& message,
const GURL& page_url, bool user_is_interacting,
web::WebFrame* sender_frame) {
const base::Value* javascript_result = message.FindPath("result");
if (!javascript_result) {
return;
}
if (javascript_result->is_string()) {
block_result =
base::SysUTF8ToNSString(javascript_result->GetString());
async_success = true;
} else if (javascript_result->is_dict()) {
block_error_message = base::SysUTF8ToNSString(
javascript_result->FindPath("message")->GetString());
async_success = true;
} else {
async_success = false;
}
}),
"cookieTest");
if (!GetStorage(web_frame, "cookieTest.getCache", key, nil, nil)) {
return false;
}
bool success =
WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
return async_success;
});
if (result) {
*result = block_result;
}
if (error_message) {
*error_message = block_error_message;
}
return success;
}
} // namespace test
} // namespace web
......@@ -77,3 +77,56 @@ __gCrWeb.cookieTest.setSessionStorage = function(key, value) {
return false;
}
};
async function setCache(key, value) {
const cache = await caches.open('cache');
return cache.put(`/${key}`, new Response(value));
}
async function getCache(key) {
const cache = await caches.open('cache');
const result = await cache.match(new Request(`/${key}`));
return result && result.text();
}
function onError(error) {
if (error instanceof DOMException || error instanceof ReferenceError) {
__gCrWeb.message.invokeOnHost({
command: 'cookieTest.result',
result: {message: error.message}
});
return;
}
__gCrWeb.message.invokeOnHost(
{command: 'cookieTest.result', result: false});
}
async function getCacheWrapper(key) {
try {
const value = await getCache(key);
__gCrWeb.message.invokeOnHost(
{command: 'cookieTest.result', result: value})
} catch (error) {
onError(error);
}
}
async function setCacheWrapper(key, value) {
try {
await setCache(key, value);
__gCrWeb.message.invokeOnHost(
{command: 'cookieTest.result', result: true})
} catch (error) {
onError(error);
}
}
__gCrWeb.cookieTest.setCache = function(key, value) {
setCacheWrapper(key, value);
return true;
};
__gCrWeb.cookieTest.getCache = function(key) {
getCacheWrapper(key);
return 'This is an async function.';
};
......@@ -80,9 +80,46 @@ addOverride(window, window, 'sessionStorage', function() {
'SecurityError');
}, null);
// Caches are only supported in a SecureContext. Only add the override if
// caches are supported.
if (window.caches) {
const promiseException = new DOMException(
'An attempt was made to break through the security policy of the user ' +
'agent.',
'SecurityError');
addOverride(caches, CacheStorage.prototype, 'match', function() {
return function(request, options) {
return Promise.reject(promiseException);
}
});
addOverride(caches, CacheStorage.prototype, 'has', function() {
return function(cacheName) {
return Promise.reject(promiseException);
}
});
addOverride(caches, CacheStorage.prototype, 'open', function() {
return function(cacheName) {
return Promise.reject(promiseException);
}
});
addOverride(caches, CacheStorage.prototype, 'delete', function() {
return function(cacheName) {
return Promise.reject(promiseException);
}
});
addOverride(caches, CacheStorage.prototype, 'keys', function() {
return function() {
return Promise.reject(promiseException);
}
});
}
if (brokenOverrides.length > 0) {
__gCrWeb.message.invokeOnHost(
{'command': 'cookie.error', 'brokenOverrides': brokenOverrides});
}
}());
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment