// Copyright 2015 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 "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/histogram_tester.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/resource_type.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/test/spawned_test_server/spawned_test_server.h"
#include "testing/gmock/include/gmock/gmock.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 SiteIsolationStatsGathererBrowserTest : public ContentBrowserTest {
public:
SiteIsolationStatsGathererBrowserTest() {}
~SiteIsolationStatsGathererBrowserTest() override {}
void SetUpCommandLine(base::CommandLine* command_line) override {
ASSERT_TRUE(test_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);
}
void InspectHistograms(const base::HistogramTester& histograms,
bool should_be_blocked,
const std::string& resource_name) {
std::string bucket;
int mime_type = 0; // Hardcoded because histogram enums mustn't change.
if (base::MatchPattern(resource_name, "*.html")) {
bucket = "HTML";
mime_type = 0;
} else if (base::MatchPattern(resource_name, "*.xml")) {
bucket = "XML";
mime_type = 1;
} else if (base::MatchPattern(resource_name, "*.json")) {
bucket = "JSON";
mime_type = 2;
} else if (base::MatchPattern(resource_name, "*.txt")) {
bucket = "Plain";
mime_type = 3;
if (base::MatchPattern(resource_name, "json*")) {
bucket += ".JSON";
} else if (base::MatchPattern(resource_name, "html*")) {
bucket += ".HTML";
} else if (base::MatchPattern(resource_name, "xml*")) {
bucket += ".XML";
}
} else {
FAIL();
}
FetchHistogramsFromChildProcesses();
// A few histograms are incremented unconditionally.
histograms.ExpectUniqueSample("SiteIsolation.AllResponses", 1, 1);
base::HistogramTester::CountsMap expected_metrics;
expected_metrics["SiteIsolation.XSD.DataLength"] = 1;
expected_metrics["SiteIsolation.XSD.MimeType"] = 1;
// Determine the appropriate conditionally-incremented histograms.
std::string base = "SiteIsolation.XSD." + bucket;
if (should_be_blocked) {
expected_metrics[base + ".Blocked"] = 1;
expected_metrics[base + ".Blocked.RenderableStatusCode"] = 1;
} else {
expected_metrics[base + ".NotBlocked"] = 1;
if (base::MatchPattern(resource_name, "*js.*")) {
expected_metrics[base + ".NotBlocked.MaybeJS"] = 1;
}
}
// Make sure that the expected metrics, and only those metrics, were
// incremented.
EXPECT_THAT(histograms.GetTotalCountsForPrefix("SiteIsolation.XSD."),
testing::ContainerEq(expected_metrics))
<< "For resource_name=" << resource_name
<< ", should_be_blocked=" << should_be_blocked;
EXPECT_THAT(histograms.GetAllSamples("SiteIsolation.XSD.MimeType"),
testing::ElementsAre(base::Bucket(mime_type, 1)))
<< "The wrong mime type bucket was incremented.";
if (should_be_blocked) {
static_assert(13 == RESOURCE_TYPE_XHR, "Histogram enums mustn't change.");
EXPECT_THAT(
histograms.GetAllSamples(base + ".Blocked.RenderableStatusCode"),
testing::ElementsAre(base::Bucket(RESOURCE_TYPE_XHR, 1)))
<< "The wrong RenderableStatusCode bucket was incremented.";
}
}
private:
DISALLOW_COPY_AND_ASSIGN(SiteIsolationStatsGathererBrowserTest);
};
// 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(SiteIsolationStatsGathererBrowserTest,
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. Currently, on illegal
// access, the XHR requests should succeed, but the UMA histograms should
// record that they would have been blocked. 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");
NavigateToURL(shell(), foo);
// Flush out existing histogram activity.
FetchHistogramsFromChildProcesses();
// The following are files under content/test/data/site_isolation. All
// should be disallowed for cross site XHR under the document blocking policy.
const char* blocked_resources[] = {
"comment_valid.html",
"html.txt",
"html4_dtd.html",
"html4_dtd.txt",
"html5_dtd.html",
"html5_dtd.txt",
"json.txt",
"valid.html",
"valid.json",
"valid.xml",
"xml.txt",
};
for (const char* resource : blocked_resources) {
SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource));
base::HistogramTester histograms;
bool was_blocked;
ASSERT_TRUE(ExecuteScriptAndExtractBool(
shell()->web_contents(),
base::StringPrintf("sendRequest(\"%s\");", resource), &was_blocked));
ASSERT_FALSE(was_blocked);
InspectHistograms(histograms, true, resource);
}
// These files should be allowed for XHR under the document blocking policy.
const char* allowed_resources[] = {"js.html",
"comment_js.html",
"js.xml",
"js.json",
"js.txt",
"img.html",
"img.xml",
"img.json",
"img.txt",
"comment_js.html"};
for (const char* resource : allowed_resources) {
SCOPED_TRACE(base::StringPrintf("... while testing page: %s", resource));
base::HistogramTester histograms;
bool was_blocked;
ASSERT_TRUE(ExecuteScriptAndExtractBool(
shell()->web_contents(),
base::StringPrintf("sendRequest(\"%s\");", resource), &was_blocked));
ASSERT_FALSE(was_blocked);
InspectHistograms(histograms, false, resource);
}
}
// 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(SiteIsolationStatsGathererBrowserTest,
MAYBE_CrossSiteDocumentBlockingForDifferentTargets) {
// This webpage loads a cross-site HTML page in different targets such as
// ,,