diff options
author | dsjang@chromium.org <dsjang@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-23 23:55:37 +0000 |
---|---|---|
committer | dsjang@chromium.org <dsjang@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-23 23:55:37 +0000 |
commit | 15b5a54cb34d019f03a621c50757f89396139860 (patch) | |
tree | f016cd5a0955620cf517b78de1bf543623ae60ec /content/child | |
parent | 63a5a15d632771566fb2f5861e7cb26e5f2357fe (diff) | |
download | chromium_src-15b5a54cb34d019f03a621c50757f89396139860.zip chromium_src-15b5a54cb34d019f03a621c50757f89396139860.tar.gz chromium_src-15b5a54cb34d019f03a621c50757f89396139860.tar.bz2 |
UMA data collector for cross-site documents(XSD)
Intercept cross-site documents and apply a couple of blocking filters to measure how our cross-site document blocking policy affects the renderer behavior. This doesn't actually block anything, but just records UMA data about how these filters work. It does three things: 1) whitelists legitimate XSDs (responses with whitelisted mime types or with valid CORS headers) 2) applies an appropriate content sniffing algorithm depending on the mime type of the response, 3) if it is sniffed as a blocked document, reports its status code and the context (img, script, etc) where the request is originally issued to measure the compatibility impact of the blocking.
BUG=268640
Related Doc: https://docs.google.com/a/google.com/document/d/1nB3GruRqQmtA7OPZZAhWOsZDvfWpYpKQXE3cxGxTVfs/edit
Review URL: https://chromiumcodereview.appspot.com/22254005
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@219383 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content/child')
-rw-r--r-- | content/child/request_extra_data.cc | 2 | ||||
-rw-r--r-- | content/child/request_extra_data.h | 3 | ||||
-rw-r--r-- | content/child/resource_dispatcher.cc | 47 | ||||
-rw-r--r-- | content/child/resource_dispatcher.h | 7 | ||||
-rw-r--r-- | content/child/resource_dispatcher_unittest.cc | 3 | ||||
-rw-r--r-- | content/child/site_isolation_policy.cc | 561 | ||||
-rw-r--r-- | content/child/site_isolation_policy.h | 179 | ||||
-rw-r--r-- | content/child/site_isolation_policy_browsertest.cc | 106 | ||||
-rw-r--r-- | content/child/site_isolation_policy_unittest.cc | 125 |
9 files changed, 1026 insertions, 7 deletions
diff --git a/content/child/request_extra_data.cc b/content/child/request_extra_data.cc index 419fbb4..2c935ce 100644 --- a/content/child/request_extra_data.cc +++ b/content/child/request_extra_data.cc @@ -14,6 +14,7 @@ RequestExtraData::RequestExtraData(WebReferrerPolicy referrer_policy, bool was_after_preconnect_request, bool is_main_frame, int64 frame_id, + const GURL& frame_origin, bool parent_is_main_frame, int64 parent_frame_id, bool allow_download, @@ -25,6 +26,7 @@ RequestExtraData::RequestExtraData(WebReferrerPolicy referrer_policy, was_after_preconnect_request), is_main_frame_(is_main_frame), frame_id_(frame_id), + frame_origin_(frame_origin), parent_is_main_frame_(parent_is_main_frame), parent_frame_id_(parent_frame_id), allow_download_(allow_download), diff --git a/content/child/request_extra_data.h b/content/child/request_extra_data.h index 83adf28..0b3d30a 100644 --- a/content/child/request_extra_data.h +++ b/content/child/request_extra_data.h @@ -22,6 +22,7 @@ class CONTENT_EXPORT RequestExtraData bool was_after_preconnect_request, bool is_main_frame, int64 frame_id, + const GURL& frame_origin, bool parent_is_main_frame, int64 parent_frame_id, bool allow_download, @@ -32,6 +33,7 @@ class CONTENT_EXPORT RequestExtraData bool is_main_frame() const { return is_main_frame_; } int64 frame_id() const { return frame_id_; } + GURL frame_origin() const { return frame_origin_; } bool parent_is_main_frame() const { return parent_is_main_frame_; } int64 parent_frame_id() const { return parent_frame_id_; } bool allow_download() const { return allow_download_; } @@ -46,6 +48,7 @@ class CONTENT_EXPORT RequestExtraData private: bool is_main_frame_; int64 frame_id_; + GURL frame_origin_; bool parent_is_main_frame_; int64 parent_frame_id_; bool allow_download_; diff --git a/content/child/resource_dispatcher.cc b/content/child/resource_dispatcher.cc index 424902f..31e6f6a 100644 --- a/content/child/resource_dispatcher.cc +++ b/content/child/resource_dispatcher.cc @@ -16,6 +16,7 @@ #include "base/metrics/histogram.h" #include "base/strings/string_util.h" #include "content/child/request_extra_data.h" +#include "content/child/site_isolation_policy.h" #include "content/common/inter_process_time_ticks_converter.h" #include "content/common/resource_messages.h" #include "content/public/child/resource_dispatcher_delegate.h" @@ -96,6 +97,9 @@ class IPCResourceLoaderBridge : public ResourceLoaderBridge { // The routing id used when sending IPC messages. int routing_id_; + // The security origin of the frame that initiates this request. + GURL frame_origin_; + bool is_synchronous_request_; }; @@ -135,6 +139,7 @@ IPCResourceLoaderBridge::IPCResourceLoaderBridge( extra_data->transferred_request_child_id(); request_.transferred_request_request_id = extra_data->transferred_request_request_id(); + frame_origin_ = extra_data->frame_origin(); } else { request_.is_main_frame = false; request_.frame_id = -1; @@ -179,7 +184,7 @@ bool IPCResourceLoaderBridge::Start(Peer* peer) { // generate the request ID, and append it to the message request_id_ = dispatcher_->AddPendingRequest( - peer_, request_.resource_type, request_.url); + peer_, request_.resource_type, frame_origin_, request_.url); return dispatcher_->message_sender()->Send( new ResourceHostMsg_RequestResource(routing_id_, request_id_, request_)); @@ -346,6 +351,11 @@ void ResourceDispatcher::OnReceivedResponse( ResourceResponseInfo renderer_response_info; ToResourceResponseInfo(*request_info, response_head, &renderer_response_info); + SiteIsolationPolicy::OnReceivedResponse(request_id, + request_info->frame_origin, + request_info->response_url, + request_info->resource_type, + renderer_response_info); request_info->peer->OnReceivedResponse(renderer_response_info); } @@ -411,10 +421,24 @@ void ResourceDispatcher::OnReceivedData(const IPC::Message& message, CHECK(data_ptr); CHECK(data_ptr + data_offset); - request_info->peer->OnReceivedData( - data_ptr + data_offset, - data_length, - encoded_data_length); + // Check whether this response data is compliant with our cross-site + // document blocking policy. + std::string alternative_data; + bool blocked_response = SiteIsolationPolicy::ShouldBlockResponse( + request_id, data_ptr + data_offset, data_length, &alternative_data); + + // When the response is not blocked. + if (!blocked_response) { + request_info->peer->OnReceivedData( + data_ptr + data_offset, data_length, encoded_data_length); + } else if (alternative_data.size() > 0) { + // When the response is blocked, and when we have any alternative data to + // send to the renderer. When |alternative_data| is zero-sized, we do not + // call peer's callback. + request_info->peer->OnReceivedData(alternative_data.data(), + alternative_data.size(), + alternative_data.size()); + } UMA_HISTOGRAM_TIMES("ResourceDispatcher.OnReceivedDataTime", base::TimeTicks::Now() - time_start); @@ -462,6 +486,9 @@ void ResourceDispatcher::OnReceivedRedirect( request_info = GetPendingRequestInfo(request_id); if (!request_info) return; + // We update the response_url here so that we can send it to + // SiteIsolationPolicy later when OnReceivedResponse is called. + request_info->response_url = new_url; request_info->pending_redirect_message.reset( new ResourceHostMsg_FollowRedirect(routing_id, request_id, has_new_first_party_for_cookies, @@ -488,6 +515,8 @@ void ResourceDispatcher::OnRequestComplete( bool was_ignored_by_handler, const std::string& security_info, const base::TimeTicks& browser_completion_time) { + SiteIsolationPolicy::OnRequestComplete(request_id); + PendingRequestInfo* request_info = GetPendingRequestInfo(request_id); if (!request_info) return; @@ -517,11 +546,12 @@ void ResourceDispatcher::OnRequestComplete( int ResourceDispatcher::AddPendingRequest( ResourceLoaderBridge::Peer* callback, ResourceType::Type resource_type, + const GURL& frame_origin, const GURL& request_url) { // Compute a unique request_id for this renderer process. int id = MakeRequestID(); pending_requests_[id] = - PendingRequestInfo(callback, resource_type, request_url); + PendingRequestInfo(callback, resource_type, frame_origin, request_url); return id; } @@ -530,6 +560,7 @@ bool ResourceDispatcher::RemovePendingRequest(int request_id) { if (it == pending_requests_.end()) return false; + SiteIsolationPolicy::OnRequestComplete(request_id); PendingRequestInfo& request_info = it->second; ReleaseResourcesInMessageQueue(&request_info.deferred_message_queue); pending_requests_.erase(it); @@ -545,6 +576,7 @@ void ResourceDispatcher::CancelPendingRequest(int routing_id, return; } + SiteIsolationPolicy::OnRequestComplete(request_id); PendingRequestInfo& request_info = it->second; ReleaseResourcesInMessageQueue(&request_info.deferred_message_queue); pending_requests_.erase(it); @@ -592,11 +624,14 @@ ResourceDispatcher::PendingRequestInfo::PendingRequestInfo() ResourceDispatcher::PendingRequestInfo::PendingRequestInfo( webkit_glue::ResourceLoaderBridge::Peer* peer, ResourceType::Type resource_type, + const GURL& frame_origin, const GURL& request_url) : peer(peer), resource_type(resource_type), is_deferred(false), url(request_url), + frame_origin(frame_origin), + response_url(request_url), request_start(base::TimeTicks::Now()) { } diff --git a/content/child/resource_dispatcher.h b/content/child/resource_dispatcher.h index 66416f4..465a0a3 100644 --- a/content/child/resource_dispatcher.h +++ b/content/child/resource_dispatcher.h @@ -45,6 +45,7 @@ class CONTENT_EXPORT ResourceDispatcher : public IPC::Listener { // requests' ID int AddPendingRequest(webkit_glue::ResourceLoaderBridge::Peer* callback, ResourceType::Type resource_type, + const GURL& frame_origin, const GURL& request_url); // Removes a request from the pending_requests_ list, returning true if the @@ -85,6 +86,7 @@ class CONTENT_EXPORT ResourceDispatcher : public IPC::Listener { PendingRequestInfo(webkit_glue::ResourceLoaderBridge::Peer* peer, ResourceType::Type resource_type, + const GURL& frame_origin, const GURL& request_url); ~PendingRequestInfo(); @@ -93,7 +95,12 @@ class CONTENT_EXPORT ResourceDispatcher : public IPC::Listener { ResourceType::Type resource_type; MessageQueue deferred_message_queue; bool is_deferred; + // Original requested url. GURL url; + // The security origin of the frame that initiates this request. + GURL frame_origin; + // The url of the latest response even in case of redirection. + GURL response_url; linked_ptr<IPC::Message> pending_redirect_message; base::TimeTicks request_start; base::TimeTicks response_start; diff --git a/content/child/resource_dispatcher_unittest.cc b/content/child/resource_dispatcher_unittest.cc index f537496..d3a33c2 100644 --- a/content/child/resource_dispatcher_unittest.cc +++ b/content/child/resource_dispatcher_unittest.cc @@ -176,7 +176,8 @@ class ResourceDispatcherTest : public testing::Test, public IPC::Sender { request_info.routing_id = 0; RequestExtraData extra_data(WebKit::WebReferrerPolicyDefault, WebKit::WebString(), - false, true, 0, false, -1, true, + false, true, 0, GURL(), + false, -1, true, PAGE_TRANSITION_LINK, -1, -1); request_info.extra_data = &extra_data; diff --git a/content/child/site_isolation_policy.cc b/content/child/site_isolation_policy.cc new file mode 100644 index 0000000..20f0903 --- /dev/null +++ b/content/child/site_isolation_policy.cc @@ -0,0 +1,561 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/child/site_isolation_policy.h" + +#include "base/basictypes.h" +#include "base/command_line.h" +#include "base/logging.h" +#include "base/metrics/histogram.h" +#include "base/strings/string_util.h" +#include "content/public/common/content_switches.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "net/http/http_response_headers.h" +#include "third_party/WebKit/public/platform/WebHTTPHeaderVisitor.h" +#include "third_party/WebKit/public/platform/WebString.h" +#include "third_party/WebKit/public/platform/WebURL.h" +#include "third_party/WebKit/public/platform/WebURLRequest.h" +#include "third_party/WebKit/public/platform/WebURLResponse.h" +#include "third_party/WebKit/public/web/WebDocument.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "third_party/WebKit/public/web/WebFrameClient.h" +#include "third_party/WebKit/public/web/WebSecurityOrigin.h" + +using WebKit::WebDocument; +using WebKit::WebString; +using WebKit::WebURL; +using WebKit::WebURLResponse; +using WebKit::WebURLRequest; + +namespace content { + +namespace { + +// MIME types +const char kTextHtml[] = "text/html"; +const char kTextXml[] = "text/xml"; +const char xAppRssXml[] = "application/rss+xml"; +const char kAppXml[] = "application/xml"; +const char kAppJson[] = "application/json"; +const char kTextJson[] = "text/json"; +const char kTextXjson[] = "text/x-json"; +const char kTextPlain[] = "text/plain"; + +} // anonymous namespace + +SiteIsolationPolicy::ResponseMetaData::ResponseMetaData() {} + +void SiteIsolationPolicy::OnReceivedResponse( + int request_id, + GURL& frame_origin, + GURL& response_url, + ResourceType::Type resource_type, + const webkit_glue::ResourceResponseInfo& info) { + UMA_HISTOGRAM_COUNTS("SiteIsolation.AllResponses", 1); + + // See if this is for navigation. If it is, don't block it, under the + // assumption that we will put it in an appropriate process. + if (ResourceType::IsFrame(resource_type)) + return; + + if (!IsBlockableScheme(response_url)) + return; + + if (IsSameSite(frame_origin, response_url)) + return; + + SiteIsolationPolicy::ResponseMetaData::CanonicalMimeType canonical_mime_type = + GetCanonicalMimeType(info.mime_type); + + if (canonical_mime_type == SiteIsolationPolicy::ResponseMetaData::Others) + return; + + // Every CORS request should have the Access-Control-Allow-Origin header even + // if it is preceded by a pre-flight request. Therefore, if this is a CORS + // request, it has this header. response.httpHeaderField() internally uses + // case-insensitive matching for the header name. + std::string access_control_origin; + + // We can use a case-insensitive header name for EnumerateHeader(). + info.headers->EnumerateHeader( + NULL, "access-control-allow-origin", &access_control_origin); + if (IsValidCorsHeaderSet(frame_origin, response_url, access_control_origin)) + return; + + // Real XSD data collection starts from here. + std::string no_sniff; + info.headers->EnumerateHeader(NULL, "x-content-type-options", &no_sniff); + + ResponseMetaData resp_data; + resp_data.frame_origin = frame_origin.spec(); + resp_data.response_url = response_url; + resp_data.resource_type = resource_type; + resp_data.canonical_mime_type = canonical_mime_type; + resp_data.http_status_code = info.headers->response_code(); + resp_data.no_sniff = LowerCaseEqualsASCII(no_sniff, "nosniff"); + + RequestIdToMetaDataMap* metadata_map = GetRequestIdToMetaDataMap(); + (*metadata_map)[request_id] = resp_data; +} + +// These macros are defined here so that we prevent code size bloat-up due to +// the UMA_HISTOGRAM_* macros. Similar logic is used for recording UMA stats for +// different MIME types, but we cannot create a helper function for this since +// UMA_HISTOGRAM_* macros do not accept variables as their bucket names. As a +// solution, macros are used instead to capture the repeated pattern for +// recording UMA stats. TODO(dsjang): this is only needed for collecting UMA +// stat. Will be deleted when this class is used for actual blocking. + +#define SITE_ISOLATION_POLICY_COUNT_BLOCK(BUCKET_PREFIX) \ + UMA_HISTOGRAM_COUNTS( BUCKET_PREFIX ".Blocked", 1); \ + result = true; \ + if (renderable_status_code) { \ + UMA_HISTOGRAM_ENUMERATION( \ + BUCKET_PREFIX ".Blocked.RenderableStatusCode", \ + resp_data.resource_type, \ + WebURLRequest::TargetIsUnspecified + 1); \ + } else { \ + UMA_HISTOGRAM_COUNTS(BUCKET_PREFIX ".Blocked.NonRenderableStatusCode",1);\ + } + +#define SITE_ISOLATION_POLICY_COUNT_NO_SNIFF_BLOCK(BUCKET_PREFIX) \ + UMA_HISTOGRAM_COUNTS( BUCKET_PREFIX ".NoSniffBlocked", 1); \ + result = true; \ + if (renderable_status_code) { \ + UMA_HISTOGRAM_ENUMERATION( \ + BUCKET_PREFIX ".NoSniffBlocked.RenderableStatusCode", \ + resp_data.resource_type, \ + WebURLRequest::TargetIsUnspecified + 1); \ + } else { \ + UMA_HISTOGRAM_ENUMERATION( \ + BUCKET_PREFIX ".NoSniffBlocked.NonRenderableStatusCode", \ + resp_data.resource_type, \ + WebURLRequest::TargetIsUnspecified + 1); \ + } + +#define SITE_ISOLATION_POLICY_COUNT_NOTBLOCK(BUCKET_PREFIX) \ + UMA_HISTOGRAM_COUNTS(BUCKET_PREFIX ".NotBlocked", 1); \ + if (is_sniffed_for_js) \ + UMA_HISTOGRAM_COUNTS(BUCKET_PREFIX ".NotBlocked.MaybeJS", 1); \ + +#define SITE_ISOLATION_POLICY_SNIFF_AND_COUNT(SNIFF_EXPR,BUCKET_PREFIX) \ + if (SNIFF_EXPR) { \ + SITE_ISOLATION_POLICY_COUNT_BLOCK(BUCKET_PREFIX) \ + } else { \ + if (resp_data.no_sniff) { \ + SITE_ISOLATION_POLICY_COUNT_NO_SNIFF_BLOCK(BUCKET_PREFIX) \ + } else { \ + SITE_ISOLATION_POLICY_COUNT_NOTBLOCK(BUCKET_PREFIX) \ + } \ + } + +bool SiteIsolationPolicy::ShouldBlockResponse( + int request_id, + const char* data, + int length, + std::string* alternative_data) { + RequestIdToMetaDataMap* metadata_map = GetRequestIdToMetaDataMap(); + RequestIdToResultMap* result_map = GetRequestIdToResultMap(); + + // If there's an entry for |request_id| in blocked_map, this request's first + // data packet has already been examined. We can return the result here. + if (result_map->count(request_id) != 0) { + if ((*result_map)[request_id]) { + // Here, the blocking result has been set for the previous run of + // ShouldBlockResponse(), so we set alternative data to an empty string so + // that ResourceDispatcher doesn't call its peer's onReceivedData() with + // the alternative data. + alternative_data->erase(); + return true; + } + return false; + } + + // If result_map doesn't have an entry for |request_id|, we're receiving the + // first data packet for request_id. If request_id is not registered, this + // request is identified as a non-target of our policy. So we return true. + if (metadata_map->count(request_id) == 0) { + // We set request_id to true so that we always return true for this request. + (*result_map)[request_id] = false; + return false; + } + + // We now look at the first data packet received for request_id. + ResponseMetaData resp_data = (*metadata_map)[request_id]; + metadata_map->erase(request_id); + + // Record the length of the first received network packet to see if it's + // enough for sniffing. + UMA_HISTOGRAM_COUNTS("SiteIsolation.XSD.DataLength", length); + + // Record the number of cross-site document responses with a specific mime + // type (text/html, text/xml, etc). + UMA_HISTOGRAM_ENUMERATION( + "SiteIsolation.XSD.MimeType", + resp_data.canonical_mime_type, + SiteIsolationPolicy::ResponseMetaData::MaxCanonicalMimeType); + + // Store the result of cross-site document blocking analysis. True means we + // can return this document to the renderer, false means that we have to block + // the response data. + bool result = false; + + // The content is blocked if it is sniffed for HTML/JSON/XML. When the blocked + // response is with an error status code, it is not disruptive by the + // following reasons : 1) the blocked content is not a binary object (such as + // an image) since it is sniffed for text; 2) then, this blocking only breaks + // the renderer behavior only if it is either JavaScript or CSS. However, the + // renderer doesn't use the contents of JS/CSS with unaffected status code + // (e.g, 404). 3) the renderer is expected not to use the cross-site document + // content for purposes other than JS/CSS (e.g, XHR). + bool renderable_status_code = IsRenderableStatusCodeForDocument( + resp_data.http_status_code); + + // This is only used for false-negative analysis for non-blocked resources. + bool is_sniffed_for_js = SniffForJS(data, length); + + // Record the number of responses whose content is sniffed for what its mime + // type claims it to be. For example, we apply a HTML sniffer for a document + // tagged with text/html here. Whenever this check becomes true, we'll block + // the response. + switch (resp_data.canonical_mime_type) { + case SiteIsolationPolicy::ResponseMetaData::HTML: + SITE_ISOLATION_POLICY_SNIFF_AND_COUNT(SniffForHTML(data, length), + "SiteIsolation.XSD.HTML"); + break; + case SiteIsolationPolicy::ResponseMetaData::XML: + SITE_ISOLATION_POLICY_SNIFF_AND_COUNT(SniffForXML(data, length), + "SiteIsolation.XSD.XML"); + break; + case SiteIsolationPolicy::ResponseMetaData::JSON: + SITE_ISOLATION_POLICY_SNIFF_AND_COUNT(SniffForJSON(data, length), + "SiteIsolation.XSD.JSON"); + break; + case SiteIsolationPolicy::ResponseMetaData::Plain: + if (SniffForHTML(data, length)) { + SITE_ISOLATION_POLICY_COUNT_BLOCK( + "SiteIsolation.XSD.Plain.HTML"); + } else if (SniffForXML(data, length)) { + SITE_ISOLATION_POLICY_COUNT_BLOCK( + "SiteIsolation.XSD.Plain.XML"); + } else if (SniffForJSON(data, length)) { + SITE_ISOLATION_POLICY_COUNT_BLOCK( + "SiteIsolation.XSD.Plain.JSON"); + } else if (is_sniffed_for_js) { + if (resp_data.no_sniff) { + SITE_ISOLATION_POLICY_COUNT_NO_SNIFF_BLOCK( + "SiteIsolation.XSD.Plain"); + } else { + SITE_ISOLATION_POLICY_COUNT_NOTBLOCK( + "SiteIsolation.XSD.Plain"); + } + } + break; + default : + NOTREACHED() << + "Not a blockable mime type. This mime type shouldn't reach here."; + break; + } + + const CommandLine& command_line = *CommandLine::ForCurrentProcess(); + if (!command_line.HasSwitch(switches::kBlockCrossSiteDocuments)) + result = false; + (*result_map)[request_id] = result; + + if (result) { + alternative_data->erase(); + alternative_data->insert(0, " "); + LOG(ERROR) << resp_data.response_url + << " is blocked as an illegal cross-site document from " + << resp_data.frame_origin; + + } + return result; +} + +#undef SITE_ISOLATION_POLICY_COUNT_NOTBLOCK +#undef SITE_ISOLATION_POLICY_SNIFF_AND_COUNT +#undef SITE_ISOLATION_POLICY_COUNT_BLOCK + +void SiteIsolationPolicy::OnRequestComplete(int request_id) { + RequestIdToMetaDataMap* metadata_map = GetRequestIdToMetaDataMap(); + RequestIdToResultMap* result_map = GetRequestIdToResultMap(); + metadata_map->erase(request_id); + result_map->erase(request_id); +} + +SiteIsolationPolicy::ResponseMetaData::CanonicalMimeType +SiteIsolationPolicy::GetCanonicalMimeType(const std::string& mime_type) { + if (LowerCaseEqualsASCII(mime_type, kTextHtml)) { + return SiteIsolationPolicy::ResponseMetaData::HTML; + } + + if (LowerCaseEqualsASCII(mime_type, kTextPlain)) { + return SiteIsolationPolicy::ResponseMetaData::Plain; + } + + if (LowerCaseEqualsASCII(mime_type, kAppJson) || + LowerCaseEqualsASCII(mime_type, kTextJson) || + LowerCaseEqualsASCII(mime_type, kTextXjson)) { + return SiteIsolationPolicy::ResponseMetaData::JSON; + } + + if (LowerCaseEqualsASCII(mime_type, kTextXml) || + LowerCaseEqualsASCII(mime_type, xAppRssXml) || + LowerCaseEqualsASCII(mime_type, kAppXml)) { + return SiteIsolationPolicy::ResponseMetaData::XML; + } + + return SiteIsolationPolicy::ResponseMetaData::Others; + +} + +bool SiteIsolationPolicy::IsBlockableScheme(const GURL& url) { + // We exclude ftp:// from here. FTP doesn't provide a Content-Type + // header which our policy depends on, so we cannot protect any + // document from FTP servers. + return url.SchemeIs("http") || url.SchemeIs("https"); +} + +bool SiteIsolationPolicy::IsSameSite(const GURL& frame_origin, + const GURL& response_url) { + + if (!frame_origin.is_valid() || !response_url.is_valid()) + return false; + + if (frame_origin.scheme() != response_url.scheme()) + return false; + + // SameDomainOrHost() extracts the effective domains (public suffix plus one) + // from the two URLs and compare them. + // TODO(dsjang): use INCLUDE_PRIVATE_REGISTRIES when http://crbug.com/7988 is + // fixed. + return net::registry_controlled_domains::SameDomainOrHost( + frame_origin, + response_url, + net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); +} + +bool SiteIsolationPolicy::IsFrameNavigating(WebKit::WebFrame* frame) { + // When a navigation starts, frame->provisionalDataSource() is set + // to a not-null value which stands for the request made for the + // navigation. As soon as the network request is committed to the + // frame, frame->provisionalDataSource() is converted to null, and + // the committed data source is moved to frame->dataSource(). This + // is the most reliable way to detect whether the frame is in + // navigation or not. + return frame->provisionalDataSource() != NULL; +} + +// We don't use Webkit's existing CORS policy implementation since +// their policy works in terms of origins, not sites. For example, +// when frame is sub.a.com and it is not allowed to access a document +// with sub1.a.com. But under Site Isolation, it's allowed. +bool SiteIsolationPolicy::IsValidCorsHeaderSet( + GURL& frame_origin, + GURL& website_origin, + std::string access_control_origin) { + // Many websites are sending back "\"*\"" instead of "*". This is + // non-standard practice, and not supported by Chrome. Refer to + // CrossOriginAccessControl::passesAccessControlCheck(). + + // TODO(dsjang): * is not allowed for the response from a request + // with cookies. This allows for more than what the renderer will + // eventually be able to receive, so we won't see illegal cross-site + // documents allowed by this. We have to find a way to see if this + // response is from a cookie-tagged request or not in the future. + if (access_control_origin == "*") + return true; + + // TODO(dsjang): The CORS spec only treats a fully specified URL, except for + // "*", but many websites are using just a domain for access_control_origin, + // and this is blocked by Webkit's CORS logic here : + // CrossOriginAccessControl::passesAccessControlCheck(). GURL is set + // is_valid() to false when it is created from a URL containing * in the + // domain part. + + GURL cors_origin(access_control_origin); + return IsSameSite(frame_origin, cors_origin); +} + +// This function is a slight modification of |net::SniffForHTML|. +bool SiteIsolationPolicy::SniffForHTML(const char* data, size_t length) { + // The content sniffer used by Chrome and Firefox are using "<!--" + // as one of the HTML signatures, but it also appears in valid + // JavaScript, considered as well-formed JS by the browser. Since + // we do not want to block any JS, we exclude it from our HTML + // signatures. This can weaken our document block policy, but we can + // break less websites. + // TODO(dsjang): parameterize |net::SniffForHTML| with an option + // that decides whether to include <!-- or not, so that we can + // remove this function. + const char* html_signatures[] = {"<!DOCTYPE html", // HTML5 spec + "<script", // HTML5 spec, Mozilla + "<html", // HTML5 spec, Mozilla + "<head", // HTML5 spec, Mozilla + "<iframe", // Mozilla + "<h1", // Mozilla + "<div", // Mozilla + "<font", // Mozilla + "<table", // Mozilla + "<a", // Mozilla + "<style", // Mozilla + "<title", // Mozilla + "<b", // Mozilla + "<body", // Mozilla + "<br", "<p", // Mozilla + "<?xml" // Mozilla + }; + + if (MatchesSignature( + data, length, html_signatures, arraysize(html_signatures))) + return true; + + // "<!--" is specially treated since web JS can use "<!--" "-->" pair for + // comments. + const char* comment_begins[] = {"<!--" }; + + if (MatchesSignature( + data, length, comment_begins, arraysize(comment_begins))) { + // Search for --> and do SniffForHTML after that. If we can find the + // comment's end, we start HTML sniffing from there again. + const char end_comment[] = "-->"; + const size_t end_comment_size = strlen(end_comment); + + for (size_t i = 0; i <= length - end_comment_size; ++i) { + if (!strncmp(data + i, end_comment, end_comment_size)) { + size_t skipped = i + end_comment_size; + return SniffForHTML(data + skipped, length - skipped); + } + } + } + + return false; +} + +bool SiteIsolationPolicy::SniffForXML(const char* data, size_t length) { + // TODO(dsjang): Chrome's mime_sniffer is using strncasecmp() for + // this signature. However, XML is case-sensitive. Don't we have to + // be more lenient only to block documents starting with the exact + // string <?xml rather than <?XML ? + const char* xml_signatures[] = {"<?xml" // Mozilla + }; + return MatchesSignature( + data, length, xml_signatures, arraysize(xml_signatures)); +} + +bool SiteIsolationPolicy::SniffForJSON(const char* data, size_t length) { + // TODO(dsjang): We have to come up with a better way to sniff + // JSON. However, even RE cannot help us that much due to the fact + // that we don't do full parsing. This DFA starts with state 0, and + // finds {, "/' and : in that order. We're avoiding adding a + // dependency on a regular expression library. + const int kInitState = 0; + const int kLeftBraceState = 1; + const int kLeftQuoteState = 2; + const int kColonState = 3; + const int kDeadState = 4; + + int state = kInitState; + for (size_t i = 0; i < length && state < kColonState; ++i) { + const char c = data[i]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + continue; + + switch (state) { + case kInitState: + if (c == '{') + state = kLeftBraceState; + else + state = kDeadState; + break; + case kLeftBraceState: + if (c == '\"' || c == '\'') + state = kLeftQuoteState; + else + state = kDeadState; + break; + case kLeftQuoteState: + if (c == ':') + state = kColonState; + break; + default: + NOTREACHED(); + break; + } + } + return state == kColonState; +} + +bool SiteIsolationPolicy::MatchesSignature(const char* raw_data, + size_t raw_length, + const char* signatures[], + size_t arr_size) { + size_t start = 0; + // Skip white characters at the beginning of the document. + for (start = 0; start < raw_length; ++start) { + char c = raw_data[start]; + if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) + break; + } + + // There is no not-whitespace character in this document. + if (!(start < raw_length)) + return false; + + const char* data = raw_data + start; + size_t length = raw_length - start; + + for (size_t sig_index = 0; sig_index < arr_size; ++sig_index) { + const char* signature = signatures[sig_index]; + size_t signature_length = strlen(signature); + + if (length < signature_length) + continue; + + if (!base::strncasecmp(signature, data, signature_length)) + return true; + } + return false; +} + +bool SiteIsolationPolicy::IsRenderableStatusCodeForDocument(int status_code) { + // Chrome only uses the content of a response with one of these status codes + // for CSS/JavaScript. For images, Chrome just ignores status code. + const int renderable_status_code[] = {200, 201, 202, 203, 206, 300, 301, 302, + 303, 305, 306, 307}; + for (size_t i = 0; i < arraysize(renderable_status_code); ++i) { + if (renderable_status_code[i] == status_code) + return true; + } + return false; +} + +bool SiteIsolationPolicy::SniffForJS(const char* data, size_t length) { + // TODO(dsjang): This is a real hack. The only purpose of this function is to + // try to see if there's any possibility that this data can be JavaScript + // (superset of JS). This function will be removed once UMA stats are + // gathered. + + // Search for "var " for JS detection. + for (size_t i = 0; i < length - 3; ++i) { + if (strncmp(data + i, "var ", 4) == 0) + return true; + } + return false; +} + +SiteIsolationPolicy::RequestIdToMetaDataMap* +SiteIsolationPolicy::GetRequestIdToMetaDataMap() { + CR_DEFINE_STATIC_LOCAL(RequestIdToMetaDataMap, metadata_map_, ()); + return &metadata_map_; +} + +SiteIsolationPolicy::RequestIdToResultMap* +SiteIsolationPolicy::GetRequestIdToResultMap() { + CR_DEFINE_STATIC_LOCAL(RequestIdToResultMap, result_map_, ()); + return &result_map_; +} + +} // namespace content diff --git a/content/child/site_isolation_policy.h b/content/child/site_isolation_policy.h new file mode 100644 index 0000000..bf816a9 --- /dev/null +++ b/content/child/site_isolation_policy.h @@ -0,0 +1,179 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_CHILD_SITE_ISOLATION_POLICY_H_ +#define CONTENT_CHILD_SITE_ISOLATION_POLICY_H_ + +#include <map> +#include <utility> + +#include "base/gtest_prod_util.h" +#include "content/common/content_export.h" +#include "third_party/WebKit/public/platform/WebURLRequest.h" +#include "third_party/WebKit/public/platform/WebURLResponse.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "webkit/common/resource_response_info.h" +#include "webkit/common/resource_type.h" + +namespace content { + +// SiteIsolationPolicy implements the cross-site document blocking policy (XSDP) +// for Site Isolation. XSDP will monitor network responses to a renderer and +// block illegal responses so that a compromised renderer cannot steal private +// information from other sites. For now SiteIsolationPolicy monitors responses +// to gather various UMA stats to see the compatibility impact of actual +// deployment of the policy. The UMA stat categories SiteIsolationPolicy gathers +// are as follows: +// +// SiteIsolation.AllResponses : # of all network responses. +// SiteIsolation.XSD.DataLength : the length of the first packet of a response. +// SiteIsolation.XSD.MimeType (enum): +// # of responses from other sites, tagged with a document mime type. +// 0:HTML, 1:XML, 2:JSON, 3:Plain, 4:Others +// SiteIsolation.XSD.[%MIMETYPE].Blocked : +// blocked # of cross-site document responses grouped by sniffed MIME type. +// SiteIsolation.XSD.[%MIMETYPE].Blocked.RenderableStatusCode : +// # of responses with renderable status code, +// out of SiteIsolation.XSD.[%MIMETYPE].Blocked. +// SiteIsolation.XSD.[%MIMETYPE].Blocked.NonRenderableStatusCode : +// # of responses with non-renderable status code, +// out of SiteIsolation.XSD.[%MIMETYPE].Blocked. +// SiteIsolation.XSD.[%MIMETYPE].NoSniffBlocked.RenderableStatusCode : +// # of responses failed to be sniffed for its MIME type, but blocked by +// "X-Content-Type-Options: nosniff" header, and with renderable status code +// out of SiteIsolation.XSD.[%MIMETYPE].Blocked. +// SiteIsolation.XSD.[%MIMETYPE].NoSniffBlocked.NonRenderableStatusCode : +// # of responses failed to be sniffed for its MIME type, but blocked by +// "X-Content-Type-Options: nosniff" header, and with non-renderable status +// code out of SiteIsolation.XSD.[%MIMETYPE].Blocked. +// SiteIsolation.XSD.[%MIMETYPE].NotBlocked : +// # of responses, but not blocked due to failure of mime sniffing. +// SiteIsolation.XSD.[%MIMETYPE].NotBlocked.MaybeJS : +// # of responses that are plausibly sniffed to be JavaScript. + +class CONTENT_EXPORT SiteIsolationPolicy { + public: + + // Records the bookkeeping data about the HTTP header information for the + // request identified by |request_id|. The bookkeeping data is used by + // ShouldBlockResponse. We have to make sure to call OnRequestComplete to free + // the bookkeeping data. + static void OnReceivedResponse(int request_id, + GURL& frame_origin, + GURL& response_url, + ResourceType::Type resource_type, + const webkit_glue::ResourceResponseInfo& info); + + // Examines the first network packet in case response_url is registered as a + // cross-site document by DidReceiveResponse(). In case that this response is + // blocked, it returns an alternative data to be sent to the renderer in + // |alternative_data|. This records various kinds of UMA data stats. This + // function is called only if the length of received data is non-zero. + static bool ShouldBlockResponse(int request_id, + const char* payload, + int length, + std::string* alternative_data); + + // Clean up booking data registered by OnReceiveResponse and OnReceivedData. + static void OnRequestComplete(int request_id); + + struct ResponseMetaData { + + enum CanonicalMimeType { + HTML = 0, + XML = 1, + JSON = 2, + Plain = 3, + Others = 4, + MaxCanonicalMimeType, + }; + + ResponseMetaData(); + + std::string frame_origin; + GURL response_url; + ResourceType::Type resource_type; + CanonicalMimeType canonical_mime_type; + int http_status_code; + bool no_sniff; + }; + + typedef std::map<int, ResponseMetaData> RequestIdToMetaDataMap; + typedef std::map<int, bool> RequestIdToResultMap; + +private: + FRIEND_TEST_ALL_PREFIXES(SiteIsolationPolicyTest, IsBlockableScheme); + FRIEND_TEST_ALL_PREFIXES(SiteIsolationPolicyTest, IsSameSite); + FRIEND_TEST_ALL_PREFIXES(SiteIsolationPolicyTest, IsValidCorsHeaderSet); + FRIEND_TEST_ALL_PREFIXES(SiteIsolationPolicyTest, SniffForHTML); + FRIEND_TEST_ALL_PREFIXES(SiteIsolationPolicyTest, SniffForXML); + FRIEND_TEST_ALL_PREFIXES(SiteIsolationPolicyTest, SniffForJSON); + FRIEND_TEST_ALL_PREFIXES(SiteIsolationPolicyTest, SniffForJS); + + // Returns the representative mime type enum value of the mime type of + // response. For example, this returns the same value for all text/xml mime + // type families such as application/xml, application/rss+xml. + static ResponseMetaData::CanonicalMimeType GetCanonicalMimeType( + const std::string& mime_type); + + // Returns whether this scheme is a target of cross-site document + // policy(XSDP). This returns true only for http://* and https://* urls. + static bool IsBlockableScheme(const GURL& frame_origin); + + // Returns whether the two urls belong to the same sites. + static bool IsSameSite(const GURL& frame_origin, const GURL& response_url); + + // Returns whether there's a valid CORS header for frame_origin. This is + // simliar to CrossOriginAccessControl::passesAccessControlCheck(), but we use + // sites as our security domain, not origins. + // TODO(dsjang): this must be improved to be more accurate to the actual CORS + // specification. For now, this works conservatively, allowing XSDs that are + // not allowed by actual CORS rules by ignoring 1) credentials and 2) + // methods. Preflight requests don't matter here since they are not used to + // decide whether to block a document or not on the client side. + static bool IsValidCorsHeaderSet(GURL& frame_origin, + GURL& website_origin, + std::string access_control_origin); + + // Returns whether the given frame is navigating. When this is true, the frame + // is requesting is a web page to be loaded. + static bool IsFrameNavigating(WebKit::WebFrame* frame); + + static bool SniffForHTML(const char* data, size_t length); + static bool SniffForXML(const char* data, size_t length); + static bool SniffForJSON(const char* data, size_t length); + + static bool MatchesSignature(const char* data, + size_t length, + const char* signatures[], + size_t arr_size); + + // TODO(dsjang): this is only needed for collecting UMA stat. Will be deleted + // when this class is used for actual blocking. + static bool SniffForJS(const char* data, size_t length); + + // TODO(dsjang): this is only needed for collecting UMA stat. Will be deleted + // when this class is used for actual blocking. + static bool IsRenderableStatusCodeForDocument(int status_code); + + // Maintain the bookkeeping data between OnReceivedResponse and + // OnReceivedData. The key is a request id maintained by ResourceDispatcher. + static RequestIdToMetaDataMap* GetRequestIdToMetaDataMap(); + + // Maintain the bookkeeping data for OnReceivedData. Blocking decision is made + // when OnReceivedData is called for the first time for a request, and the + // decision will remain the same for following data. This map maintains the + // decision. The key is a request id maintained by ResourceDispatcher. + static RequestIdToResultMap* GetRequestIdToResultMap(); + + // Never needs to be constructed/destructed. + SiteIsolationPolicy() {} + ~SiteIsolationPolicy() {} + + DISALLOW_COPY_AND_ASSIGN(SiteIsolationPolicy); +}; + +} // namespace content + +#endif // CONTENT_CHILD_SITE_ISOLATION_POLICY_H_ diff --git a/content/child/site_isolation_policy_browsertest.cc b/content/child/site_isolation_policy_browsertest.cc new file mode 100644 index 0000000..15cd06e --- /dev/null +++ b/content/child/site_isolation_policy_browsertest.cc @@ -0,0 +1,106 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/command_line.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test_utils.h" +#include "content/test/content_browser_test.h" +#include "content/test/content_browser_test_utils.h" + +namespace content { + +// These tests simulate exploited renderer processes, which can fetch arbitrary +// resources from other websites, not constrained by the Same Origin Policy. We +// are trying to verify that the renderer cannot fetch any cross-site document +// responses even when the Same Origin Policy is turned off inside the renderer. +class SiteIsolationPolicyBrowserTest : public ContentBrowserTest { + public: + SiteIsolationPolicyBrowserTest() {} + virtual ~SiteIsolationPolicyBrowserTest() {} + + virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { + ASSERT_TRUE(test_server()->Start()); + net::SpawnedTestServer https_server( + net::SpawnedTestServer::TYPE_HTTPS, + net::SpawnedTestServer::kLocalhost, + base::FilePath(FILE_PATH_LITERAL("content/test/data"))); + ASSERT_TRUE(https_server.Start()); + + // Add a host resolver rule to map all outgoing requests to the test server. + // This allows us to use "real" hostnames in URLs, which we can use to + // create arbitrary SiteInstances. + command_line->AppendSwitchASCII( + switches::kHostResolverRules, + "MAP * " + test_server()->host_port_pair().ToString() + + ",EXCLUDE localhost"); + + // Since we assume exploited renderer process, it can bypass the same origin + // policy at will. Simulate that by passing the disable-web-security flag. + command_line->AppendSwitch(switches::kDisableWebSecurity); + + // We assume that we're using our cross-site document blocking logic which + // is turned on even when the Same Origin Policy is turned off. + command_line->AppendSwitch(switches::kBlockCrossSiteDocuments); + } + + private: + DISALLOW_COPY_AND_ASSIGN(SiteIsolationPolicyBrowserTest); +}; + +// TODO(dsjang): we cannot run these tests on Android since SetUpCommandLine() +// is executed before the I/O thread is created on Android. After this bug +// (crbug.com/278425) is resolved, we can enable this test case on Android. +#if defined(OS_ANDROID) +#define MAYBE_CrossSiteDocumentBlockingForMimeType \ + DISABLED_CrossSiteDocumentBlockingForMimeType +#else +#define MAYBE_CrossSiteDocumentBlockingForMimeType \ + CrossSiteDocumentBlockingForMimeType +#endif + +IN_PROC_BROWSER_TEST_F(SiteIsolationPolicyBrowserTest, + MAYBE_CrossSiteDocumentBlockingForMimeType) { + // Load a page that issues illegal cross-site document requests to bar.com. + // The page uses XHR to request HTML/XML/JSON documents from bar.com, and + // inspects if any of them were successfully received. The XHR requests will + // get a one character string ' ' for a blocked response. This test is only + // possible since we run the browser without the same origin policy. + GURL foo("http://foo.com/files/cross_site_document_request.html"); + + content::DOMMessageQueue msg_queue; + + NavigateToURL(shell(), foo); + + std::string status; + // The page will return 1 from the DOMAutomationController if it succeeds, + // otherwise it will return 0. + std::string expected_status("1"); + EXPECT_TRUE(msg_queue.WaitForMessage(&status)); + EXPECT_STREQ(status.c_str(), expected_status.c_str()); +} + +// TODO(dsjang): we cannot run these tests on Android since SetUpCommandLine() +// is executed before the I/O thread is created on Android. After this bug +// (crbug.com/278425) is resolved, we can enable this test case on Android. +#if defined(OS_ANDROID) +#define MAYBE_CrossSiteDocumentBlockingForDifferentTargets \ + DISABLED_CrossSiteDocumentBlockingForDifferentTargets +#else +#define MAYBE_CrossSiteDocumentBlockingForDifferentTargets \ + CrossSiteDocumentBlockingForDifferentTargets +#endif + +IN_PROC_BROWSER_TEST_F(SiteIsolationPolicyBrowserTest, + MAYBE_CrossSiteDocumentBlockingForDifferentTargets) { + // This webpage loads a cross-site HTML page in different targets such as + // <img>,<link>,<embed>, etc. Since the requested document is blocked, and one + // character string (' ') is returned instead, this tests that the renderer + // does not crash even when it receives a response body which is " ", whose + // length is different from what's described in "content-length" for such + // different targets. + GURL foo("http://foo.com/files/cross_site_document_request_target.html"); + NavigateToURL(shell(), foo); +} + +} diff --git a/content/child/site_isolation_policy_unittest.cc b/content/child/site_isolation_policy_unittest.cc new file mode 100644 index 0000000..15740d3 --- /dev/null +++ b/content/child/site_isolation_policy_unittest.cc @@ -0,0 +1,125 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/strings/utf_string_conversions.h" +#include "content/child/site_isolation_policy.h" +#include "content/public/common/context_menu_params.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/WebKit/public/platform/WebURLResponse.h" +#include "ui/base/range/range.h" + + +namespace content { + +TEST(SiteIsolationPolicyTest, IsBlockableScheme) { + GURL data_url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA=="); + GURL ftp_url("ftp://google.com"); + GURL mailto_url("mailto:google@google.com"); + GURL about_url("about:chrome"); + GURL http_url("http://google.com"); + GURL https_url("https://google.com"); + + EXPECT_FALSE(SiteIsolationPolicy::IsBlockableScheme(data_url)); + EXPECT_FALSE(SiteIsolationPolicy::IsBlockableScheme(ftp_url)); + EXPECT_FALSE(SiteIsolationPolicy::IsBlockableScheme(mailto_url)); + EXPECT_FALSE(SiteIsolationPolicy::IsBlockableScheme(about_url)); + EXPECT_TRUE(SiteIsolationPolicy::IsBlockableScheme(http_url)); + EXPECT_TRUE(SiteIsolationPolicy::IsBlockableScheme(https_url)); +} + +TEST(SiteIsolationPolicyTest, IsSameSite) { + GURL a_com_url0("https://mock1.a.com:8080/page1.html"); + GURL a_com_url1("https://mock2.a.com:9090/page2.html"); + GURL a_com_url2("https://a.com/page3.html"); + EXPECT_TRUE(SiteIsolationPolicy::IsSameSite(a_com_url0, a_com_url1)); + EXPECT_TRUE(SiteIsolationPolicy::IsSameSite(a_com_url1, a_com_url2)); + EXPECT_TRUE(SiteIsolationPolicy::IsSameSite(a_com_url2, a_com_url0)); + + GURL b_com_url0("https://mock1.b.com/index.html"); + EXPECT_FALSE(SiteIsolationPolicy::IsSameSite(a_com_url0, b_com_url0)); + + GURL about_blank_url("about:blank"); + EXPECT_FALSE(SiteIsolationPolicy::IsSameSite(a_com_url0, about_blank_url)); + + GURL chrome_url("chrome://extension"); + EXPECT_FALSE(SiteIsolationPolicy::IsSameSite(a_com_url0, chrome_url)); + + GURL empty_url(""); + EXPECT_FALSE(SiteIsolationPolicy::IsSameSite(a_com_url0, empty_url)); +} + +TEST(SiteIsolationPolicyTest, IsValidCorsHeaderSet) { + GURL frame_origin("http://www.google.com"); + GURL site_origin("http://www.yahoo.com"); + + EXPECT_TRUE(SiteIsolationPolicy::IsValidCorsHeaderSet( + frame_origin, site_origin, "*")); + EXPECT_FALSE(SiteIsolationPolicy::IsValidCorsHeaderSet( + frame_origin, site_origin, "\"*\"")); + EXPECT_TRUE(SiteIsolationPolicy::IsValidCorsHeaderSet( + frame_origin, site_origin, "http://mail.google.com")); + EXPECT_FALSE(SiteIsolationPolicy::IsValidCorsHeaderSet( + frame_origin, site_origin, "https://mail.google.com")); + EXPECT_FALSE(SiteIsolationPolicy::IsValidCorsHeaderSet( + frame_origin, site_origin, "http://yahoo.com")); + EXPECT_FALSE(SiteIsolationPolicy::IsValidCorsHeaderSet( + frame_origin, site_origin, "www.google.com")); +} + +TEST(SiteIsolationPolicyTest, SniffForHTML) { + const char html_data[] = " \t\r\n <HtMladfokadfkado"; + const char comment_html_data[] = " <!-- this is comment --> <html><body>"; + const char two_comments_html_data[] = + "<!-- this is comment -->\n<!-- this is comment --><html><body>"; + const char mixed_comments_html_data[] = + "<!-- this is comment <!-- --> <script></script>"; + const char non_html_data[] = " var name=window.location;\nadfadf"; + const char comment_js_data[] = " <!-- this is comment -> document.write(1); "; + + EXPECT_TRUE( + SiteIsolationPolicy::SniffForHTML(html_data, arraysize(html_data))); + EXPECT_TRUE(SiteIsolationPolicy::SniffForHTML(comment_html_data, + arraysize(comment_html_data))); + EXPECT_TRUE(SiteIsolationPolicy::SniffForHTML( + two_comments_html_data, arraysize(two_comments_html_data))); + EXPECT_TRUE(SiteIsolationPolicy::SniffForHTML( + mixed_comments_html_data, arraysize(mixed_comments_html_data))); + EXPECT_FALSE(SiteIsolationPolicy::SniffForHTML(non_html_data, + arraysize(non_html_data))); + EXPECT_FALSE(SiteIsolationPolicy::SniffForHTML(comment_js_data, + arraysize(comment_js_data))); +} + +TEST(SiteIsolationPolicyTest, SniffForXML) { + const char xml_data[] = " \t \r \n <?xml version=\"1.0\"?>\n <catalog"; + const char non_xml_data[] = " var name=window.location;\nadfadf"; + + EXPECT_TRUE(SiteIsolationPolicy::SniffForXML(xml_data, arraysize(xml_data))); + EXPECT_FALSE( + SiteIsolationPolicy::SniffForXML(non_xml_data, arraysize(non_xml_data))); +} + +TEST(SiteIsolationPolicyTest, SniffForJSON) { + const char json_data[] = "\t\t\r\n { \"name\" : \"chrome\", "; + const char non_json_data0[] = "\t\t\r\n { name : \"chrome\", "; + const char non_json_data1[] = "\t\t\r\n foo({ \"name\" : \"chrome\", "; + + EXPECT_TRUE( + SiteIsolationPolicy::SniffForJSON(json_data, arraysize(json_data))); + EXPECT_FALSE(SiteIsolationPolicy::SniffForJSON(non_json_data0, + arraysize(non_json_data0))); + EXPECT_FALSE(SiteIsolationPolicy::SniffForJSON(non_json_data1, + arraysize(non_json_data1))); +} + +TEST(SiteIsolationPolicyTest, SniffForJS) { + const char js_data[] = "\t\t\r\n var a = 4"; + const char json_data[] = "\t\t\r\n { \"name\" : \"chrome\", "; + + EXPECT_TRUE(SiteIsolationPolicy::SniffForJS(js_data, arraysize(js_data))); + EXPECT_FALSE( + SiteIsolationPolicy::SniffForJS(json_data, arraysize(json_data))); +} + +} // namespace content |