blob: 55d7bc010560e992f7f6a41b69b1b58057a09849 [file] [log] [blame]
// 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()->Start());
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