// Copyright (c) 2010 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 "webkit/glue/site_isolation_metrics.h"
#include <set>
#include "base/hash_tables.h"
#include "base/metrics/histogram.h"
#include "net/base/mime_sniffer.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebSecurityOrigin.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebString.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebURL.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebURLRequest.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebURLResponse.h"
using WebKit::WebFrame;
using WebKit::WebSecurityOrigin;
using WebKit::WebString;
using WebKit::WebURL;
using WebKit::WebURLRequest;
using WebKit::WebURLResponse;
namespace webkit_glue {
typedef base::hash_map<unsigned, WebURLRequest::TargetType> TargetTypeMap;
typedef base::hash_map<std::string, int> MimeTypeMap;
typedef std::set<std::string> CrossOriginTextHtmlResponseSet;
static TargetTypeMap* GetTargetTypeMap() {
static TargetTypeMap target_type_map_;
return &target_type_map_;
}
// Copied from net/base/mime_util.cc, supported_non_image_types[]
static const char* const kCrossOriginMimeTypesToLog[] = {
"text/cache-manifest",
"text/html",
"text/xml",
"text/xsl",
"text/plain",
"text/vnd.chromium.ftp-dir",
"text/",
"text/css",
"image/svg+xml",
"application/xml",
"application/xhtml+xml",
"application/rss+xml",
"application/atom+xml",
"application/json",
"application/x-x509-user-cert",
"multipart/x-mixed-replace",
"(NONE)" // Keep track of missing MIME types as well
};
static MimeTypeMap* GetMimeTypeMap() {
static MimeTypeMap mime_type_map_;
if (!mime_type_map_.size()) {
for (size_t i = 0; i < arraysize(kCrossOriginMimeTypesToLog); ++i)
mime_type_map_[kCrossOriginMimeTypesToLog[i]] = i;
}
return &mime_type_map_;
}
// This is set is used to keep track of the response urls that we want to
// sniff, since we will have to wait for the payload to arrive.
static CrossOriginTextHtmlResponseSet* GetCrossOriginTextHtmlResponseSet() {
static CrossOriginTextHtmlResponseSet cross_origin_text_html_response_set_;
return &cross_origin_text_html_response_set_;
}
static void LogVerifiedTextHtmlResponse() {
UMA_HISTOGRAM_COUNTS(
"SiteIsolation.CrossSiteNonFrameResponse_verified_texthtml_BLOCK", 1);
}
static void LogMislabeledTextHtmlResponse() {
UMA_HISTOGRAM_COUNTS(
"SiteIsolation.CrossSiteNonFrameResponse_mislabeled_texthtml", 1);
}
void SiteIsolationMetrics::AddRequest(unsigned identifier,
WebURLRequest::TargetType target_type) {
TargetTypeMap& target_type_map = *GetTargetTypeMap();
target_type_map[identifier] = target_type;
}
// Check whether the given response is allowed due to access control headers.
// This is basically a copy of the logic of passesAccessControlCheck() in
// WebCore/loader/CrossOriginAccessControl.cpp.
bool SiteIsolationMetrics::AllowedByAccessControlHeader(
WebFrame* frame, const WebURLResponse& response) {
WebString access_control_origin = response.httpHeaderField(
WebString::fromUTF8("Access-Control-Allow-Origin"));
WebSecurityOrigin security_origin =
WebSecurityOrigin::createFromString(access_control_origin);
return access_control_origin == WebString::fromUTF8("*") ||
frame->securityOrigin().canAccess(security_origin);
}
// We want to log any cross-site request that we don't think a renderer should
// be allowed to make. We can safely ignore frame requests (since we'd like
// those to be in a separate renderer) and plugin requests, even if they are
// cross-origin.
//
// For comparison, we keep counts of:
// - All requests made by a renderer
// - All cross-site requests
//
// Then, for cross-site non-frame/plugin requests, we keep track of:
// - Counts for MIME types of interest
// - Counts of those MIME types that carry CORS headers
// - Counts of mislabeled text/html responses (without CORS)
// As well as those we would block:
// - Counts of verified text/html responses (without CORS)
// - Counts of XML/JSON responses (without CORS)
//
// This will let us say what percentage of requests we would end up blocking.
void SiteIsolationMetrics::LogMimeTypeForCrossOriginRequest(
WebFrame* frame, unsigned identifier, const WebURLResponse& response) {
UMA_HISTOGRAM_COUNTS("SiteIsolation.Requests", 1);
TargetTypeMap& target_type_map = *GetTargetTypeMap();
TargetTypeMap::iterator iter = target_type_map.find(identifier);
if (iter != target_type_map.end()) {
WebURLRequest::TargetType target_type = iter->second;
target_type_map.erase(iter);
// Focus on cross-site requests.
if (!frame->securityOrigin().canAccess(
WebSecurityOrigin::create(response.url()))) {
UMA_HISTOGRAM_COUNTS("SiteIsolation.CrossSiteRequests", 1);
// Now focus on non-frame, non-plugin requests.
if (target_type != WebURLRequest::TargetIsMainFrame &&
target_type != WebURLRequest::TargetIsSubframe &&
target_type != WebURLRequest::TargetIsObject) {
// If it is part of a MIME type we might block, log the MIME type.
std::string mime_type = response.mimeType().utf8();
MimeTypeMap mime_type_map = *GetMimeTypeMap();
// Also track it if it lacks a MIME type.
// TODO(creis): 304 responses have no MIME type, so we don't handle
// them correctly. Can we look up their MIME type from the cache?
if (mime_type == "")
mime_type = "(NONE)";
MimeTypeMap::iterator mime_type_iter = mime_type_map.find(mime_type);
if (mime_type_iter != mime_type_map.end()) {
UMA_HISTOGRAM_ENUMERATION(
"SiteIsolation.CrossSiteNonFrameResponse_MIME_Type",
mime_type_iter->second,
arraysize(kCrossOriginMimeTypesToLog));
// We also check access control headers, in case this
// cross-origin request has been explicitly permitted.
if (AllowedByAccessControlHeader(frame, response)) {
UMA_HISTOGRAM_ENUMERATION(
"SiteIsolation.CrossSiteNonFrameResponse_With_CORS_MIME_Type",
mime_type_iter->second,
arraysize(kCrossOriginMimeTypesToLog));
} else {
// Without access control headers, we might block this request.
// Sometimes resources are mislabled as text/html, though, and we
// should only block them if we can verify that. To do so, we sniff
// the content once we have some of the payload.
if (mime_type == "text/html") {
// Remember the response until we can sniff its contents.
GetCrossOriginTextHtmlResponseSet()->insert(
response.url().spec());
} else if (mime_type == "text/xml" ||
mime_type == "text/xsl" ||
mime_type == "application/xml" ||
mime_type == "application/xhtml+xml" ||
mime_type == "application/rss+xml" ||
mime_type == "application/atom+xml" ||
mime_type == "application/json") {
// We will also block XML and JSON MIME types for cross-site
// non-frame requests without CORS headers.
UMA_HISTOGRAM_COUNTS(
"SiteIsolation.CrossSiteNonFrameResponse_xml_or_json_BLOCK",
1);
}
}
}
}
}
}
}
void SiteIsolationMetrics::SniffCrossOriginHTML(const WebURL& response_url,
const char* data,
int len) {
if (!response_url.isValid())
return;
// Look up the URL to see if it is a text/html request we are tracking.
CrossOriginTextHtmlResponseSet& cross_origin_text_html_response_set =
*GetCrossOriginTextHtmlResponseSet();
CrossOriginTextHtmlResponseSet::iterator request_iter =
cross_origin_text_html_response_set.find(response_url.spec());
if (request_iter != cross_origin_text_html_response_set.end()) {
// Log whether it actually looks like HTML.
std::string sniffed_mime_type;
bool successful = net::SniffMimeType(data, len, response_url,
"", &sniffed_mime_type);
if (successful && sniffed_mime_type == "text/html")
LogVerifiedTextHtmlResponse();
else
LogMislabeledTextHtmlResponse();
cross_origin_text_html_response_set.erase(request_iter);
}
}
void SiteIsolationMetrics::RemoveCompletedResponse(
const WebURL& response_url) {
if (!response_url.isValid())
return;
// Ensure we don't leave responses in the set after they've completed.
CrossOriginTextHtmlResponseSet& cross_origin_text_html_response_set =
*GetCrossOriginTextHtmlResponseSet();
CrossOriginTextHtmlResponseSet::iterator request_iter =
cross_origin_text_html_response_set.find(response_url.spec());
if (request_iter != cross_origin_text_html_response_set.end()) {
LogMislabeledTextHtmlResponse();
cross_origin_text_html_response_set.erase(request_iter);
}
}
} // namespace webkit_glue