| Index: content/browser/loader/cross_site_resource_handler_browsertest.cc
|
| diff --git a/content/browser/loader/cross_site_resource_handler_browsertest.cc b/content/browser/loader/cross_site_resource_handler_browsertest.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..4d66c1a9812275603c130aec7420c5b0d091de3a
|
| --- /dev/null
|
| +++ b/content/browser/loader/cross_site_resource_handler_browsertest.cc
|
| @@ -0,0 +1,277 @@
|
| +// 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/callback.h"
|
| +#include "base/command_line.h"
|
| +#include "base/memory/weak_ptr.h"
|
| +#include "base/single_thread_task_runner.h"
|
| +#include "base/thread_task_runner_handle.h"
|
| +#include "content/browser/frame_host/render_frame_host_impl.h"
|
| +#include "content/browser/loader/resource_dispatcher_host_impl.h"
|
| +#include "content/browser/web_contents/web_contents_impl.h"
|
| +#include "content/common/frame_messages.h"
|
| +#include "content/public/browser/resource_dispatcher_host.h"
|
| +#include "content/public/browser/resource_dispatcher_host_delegate.h"
|
| +#include "content/public/browser/resource_throttle.h"
|
| +#include "content/public/browser/web_contents.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/public/test/test_utils.h"
|
| +#include "content/shell/browser/shell.h"
|
| +#include "content/shell/browser/shell_resource_dispatcher_host_delegate.h"
|
| +#include "ipc/ipc_security_test_util.h"
|
| +#include "net/dns/mock_host_resolver.h"
|
| +#include "net/test/embedded_test_server/embedded_test_server.h"
|
| +#include "net/url_request/url_request.h"
|
| +
|
| +namespace content {
|
| +
|
| +namespace {
|
| +
|
| +// A ResourceDispatchHostDelegate that uses ResourceThrottles to pause a
|
| +// targeted request temporarily, to run a chunk of test code.
|
| +class TestResourceDispatcherHostDelegate
|
| + : public ShellResourceDispatcherHostDelegate {
|
| + public:
|
| + using RequestDeferredHook = base::Callback<void(const base::Closure& resume)>;
|
| + TestResourceDispatcherHostDelegate() : throttle_created_(false) {}
|
| +
|
| + void RequestBeginning(net::URLRequest* request,
|
| + ResourceContext* resource_context,
|
| + AppCacheService* appcache_service,
|
| + ResourceType resource_type,
|
| + ScopedVector<ResourceThrottle>* throttles) override {
|
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
|
| + ShellResourceDispatcherHostDelegate::RequestBeginning(
|
| + request, resource_context, appcache_service, resource_type, throttles);
|
| +
|
| + // If this is a request for the tracked URL, add a throttle to track it.
|
| + if (request->url() == tracked_url_) {
|
| + // Expect only a single request for the tracked url.
|
| + ASSERT_FALSE(throttle_created_);
|
| + throttle_created_ = true;
|
| +
|
| + throttles->push_back(
|
| + new CallbackRunningResourceThrottle(request, this, run_on_start_));
|
| + }
|
| + }
|
| +
|
| + // Starts tracking a URL. The request for previously tracked URL, if any,
|
| + // must have been made and deleted before calling this function.
|
| + void SetTrackedURL(const GURL& tracked_url,
|
| + const RequestDeferredHook& run_on_start) {
|
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
| + // Should not currently be tracking any URL.
|
| + ASSERT_FALSE(run_loop_);
|
| +
|
| + // Create a RunLoop that will be stopped once the request for the tracked
|
| + // URL has been destroyed, to allow tracking the URL while also waiting for
|
| + // other events.
|
| + run_loop_.reset(new base::RunLoop());
|
| +
|
| + BrowserThread::PostTask(
|
| + BrowserThread::IO, FROM_HERE,
|
| + base::Bind(&TestResourceDispatcherHostDelegate::SetTrackedURLOnIOThread,
|
| + base::Unretained(this), tracked_url, run_on_start,
|
| + run_loop_->QuitClosure()));
|
| + }
|
| +
|
| + // Waits until the tracked URL has been requested, and the request for it has
|
| + // been destroyed.
|
| + bool WaitForTrackedURLAndGetCompleted() {
|
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
| + run_loop_->Run();
|
| + run_loop_.reset();
|
| + return tracked_request_completed_;
|
| + }
|
| +
|
| + private:
|
| + // A ResourceThrottle which defers the request at WillStartRequest time until
|
| + // a test-supplied callback completes. Notifies |tracker| when the request is
|
| + // destroyed.
|
| + class CallbackRunningResourceThrottle : public ResourceThrottle {
|
| + public:
|
| + CallbackRunningResourceThrottle(net::URLRequest* request,
|
| + TestResourceDispatcherHostDelegate* tracker,
|
| + const RequestDeferredHook& run_on_start)
|
| + : request_(request),
|
| + tracker_(tracker),
|
| + run_on_start_(run_on_start),
|
| + weak_factory_(this) {}
|
| +
|
| + void WillStartRequest(bool* defer) override {
|
| + *defer = true;
|
| + base::Closure resume_request_on_io_thread = base::Bind(
|
| + base::IgnoreResult(&BrowserThread::PostTask), BrowserThread::IO,
|
| + FROM_HERE, base::Bind(&CallbackRunningResourceThrottle::Resume,
|
| + weak_factory_.GetWeakPtr()));
|
| + BrowserThread::PostTask(
|
| + BrowserThread::UI, FROM_HERE,
|
| + base::Bind(run_on_start_, resume_request_on_io_thread));
|
| + }
|
| +
|
| + ~CallbackRunningResourceThrottle() override {
|
| + // If the request is deleted without being cancelled, its status will
|
| + // indicate it succeeded, so have to check if the request is still pending
|
| + // as well.
|
| + tracker_->OnTrackedRequestDestroyed(!request_->is_pending() &&
|
| + request_->status().is_success());
|
| + }
|
| +
|
| + // ResourceThrottle implementation:
|
| + const char* GetNameForLogging() const override {
|
| + return "CallbackRunningResourceThrottle";
|
| + }
|
| +
|
| + private:
|
| + void Resume() { controller()->Resume(); }
|
| + net::URLRequest* request_;
|
| + TestResourceDispatcherHostDelegate* tracker_;
|
| + RequestDeferredHook run_on_start_;
|
| + base::WeakPtrFactory<CallbackRunningResourceThrottle> weak_factory_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(CallbackRunningResourceThrottle);
|
| + };
|
| +
|
| + void SetTrackedURLOnIOThread(const GURL& tracked_url,
|
| + const RequestDeferredHook& run_on_start,
|
| + const base::Closure& run_loop_quit_closure) {
|
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
|
| + throttle_created_ = false;
|
| + tracked_url_ = tracked_url;
|
| + run_on_start_ = run_on_start;
|
| + run_loop_quit_closure_ = run_loop_quit_closure;
|
| + }
|
| +
|
| + void OnTrackedRequestDestroyed(bool completed) {
|
| + CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
|
| + tracked_request_completed_ = completed;
|
| + tracked_url_ = GURL();
|
| + run_on_start_ = RequestDeferredHook();
|
| +
|
| + BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
|
| + run_loop_quit_closure_);
|
| + }
|
| +
|
| + // These live on the IO thread.
|
| + GURL tracked_url_;
|
| + bool throttle_created_;
|
| + base::Closure run_loop_quit_closure_;
|
| + RequestDeferredHook run_on_start_;
|
| +
|
| + // This lives on the UI thread.
|
| + scoped_ptr<base::RunLoop> run_loop_;
|
| +
|
| + // Set on the IO thread while |run_loop_| is non-nullptr, read on the UI
|
| + // thread after deleting run_loop_.
|
| + bool tracked_request_completed_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(TestResourceDispatcherHostDelegate);
|
| +};
|
| +
|
| +class CrossSiteResourceHandlerTest : public ContentBrowserTest {
|
| + public:
|
| + CrossSiteResourceHandlerTest() : old_delegate_(nullptr) {}
|
| +
|
| + // ContentBrowserTest implementation:
|
| + void SetUpOnMainThread() override {
|
| + BrowserThread::PostTask(
|
| + BrowserThread::IO, FROM_HERE,
|
| + base::Bind(
|
| + &CrossSiteResourceHandlerTest::InjectResourceDispatcherHostDelegate,
|
| + base::Unretained(this)));
|
| + host_resolver()->AddRule("*", "127.0.0.1");
|
| + ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
|
| + content::SetupCrossSiteRedirector(embedded_test_server());
|
| + }
|
| +
|
| + void TearDownOnMainThread() override {
|
| + BrowserThread::PostTask(
|
| + BrowserThread::IO, FROM_HERE,
|
| + base::Bind(&CrossSiteResourceHandlerTest::
|
| + RestoreResourceDispatcherHostDelegate,
|
| + base::Unretained(this)));
|
| + }
|
| +
|
| + protected:
|
| + void SetUpCommandLine(base::CommandLine* command_line) override {
|
| + IsolateAllSitesForTesting(command_line);
|
| + }
|
| +
|
| + void InjectResourceDispatcherHostDelegate() {
|
| + DCHECK_CURRENTLY_ON(BrowserThread::IO);
|
| + old_delegate_ = ResourceDispatcherHostImpl::Get()->delegate();
|
| + ResourceDispatcherHostImpl::Get()->SetDelegate(&tracking_delegate_);
|
| + }
|
| +
|
| + void RestoreResourceDispatcherHostDelegate() {
|
| + DCHECK_CURRENTLY_ON(BrowserThread::IO);
|
| + ResourceDispatcherHostImpl::Get()->SetDelegate(old_delegate_);
|
| + old_delegate_ = nullptr;
|
| + }
|
| +
|
| + TestResourceDispatcherHostDelegate& tracking_delegate() {
|
| + return tracking_delegate_;
|
| + }
|
| +
|
| + private:
|
| + TestResourceDispatcherHostDelegate tracking_delegate_;
|
| + ResourceDispatcherHostDelegate* old_delegate_;
|
| +};
|
| +
|
| +void SimulateMaliciousFrameDetachOnUIThread(int render_process_id,
|
| + int frame_routing_id,
|
| + const base::Closure& done_cb) {
|
| + RenderFrameHostImpl* rfh =
|
| + RenderFrameHostImpl::FromID(render_process_id, frame_routing_id);
|
| + CHECK(rfh);
|
| +
|
| + // Inject a frame detach message. An attacker-controlled renderer could do
|
| + // this without also cancelling the pending navigation (as blink would, if you
|
| + // removed the iframe from the document via js).
|
| + rfh->OnMessageReceived(FrameHostMsg_Detach(frame_routing_id));
|
| + done_cb.Run();
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +// Regression test for https://crbug.com/538784 -- ensures that one can't
|
| +// sidestep CrossSiteResourceHandler by detaching a frame mid-request.
|
| +IN_PROC_BROWSER_TEST_F(CrossSiteResourceHandlerTest,
|
| + NoDeliveryToDetachedFrame) {
|
| + GURL attacker_page = embedded_test_server()->GetURL(
|
| + "evil.com", "/cross_site_iframe_factory.html?evil(evil)");
|
| + EXPECT_TRUE(NavigateToURL(shell(), attacker_page));
|
| +
|
| + FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
|
| + ->GetFrameTree()
|
| + ->root();
|
| +
|
| + RenderFrameHost* child_frame = root->child_at(0)->current_frame_host();
|
| +
|
| + // Attacker initiates a navigation to a cross-site document. Under --site-per-
|
| + // process, these bytes must not be sent to the attacker process.
|
| + GURL target_resource =
|
| + embedded_test_server()->GetURL("a.com", "/title1.html");
|
| +
|
| + // We add a testing hook to simulate the attacker-controlled process sending
|
| + // FrameHostMsg_Detach before the http response arrives. At the time this test
|
| + // was written, the resource request had a lifetime separate from the RFH,
|
| + tracking_delegate().SetTrackedURL(
|
| + target_resource, base::Bind(&SimulateMaliciousFrameDetachOnUIThread,
|
| + child_frame->GetProcess()->GetID(),
|
| + child_frame->GetRoutingID()));
|
| + EXPECT_TRUE(ExecuteScript(
|
| + shell()->web_contents()->GetMainFrame(),
|
| + base::StringPrintf("document.getElementById('child-0').src='%s'",
|
| + target_resource.spec().c_str())));
|
| +
|
| + // Wait for the scenario to play out. If this returns false, it means the
|
| + // request did not succeed, which is good in this case.
|
| + EXPECT_FALSE(tracking_delegate().WaitForTrackedURLAndGetCompleted())
|
| + << "Request should have been cancelled before reaching the renderer.";
|
| +}
|
| +
|
| +} // namespace content
|
|
|