summaryrefslogtreecommitdiff
path: root/chromium/content/browser/loader/cross_site_resource_handler_browsertest.cc
blob: eea52307c732ebb0df374f6cad488597b4dd0588 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// 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/macros.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)
        : resumed_(false),
          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. If the request never even started, the throttle will never
      // resume it. Check this condition as well to allow for early
      // cancellation.
      tracker_->OnTrackedRequestDestroyed(!request_->is_pending() &&
                                          request_->status().is_success() &&
                                          resumed_);
    }

    // ResourceThrottle implementation:
    const char* GetNameForLogging() const override {
      return "CallbackRunningResourceThrottle";
    }

   private:
    void Resume() {
      resumed_ = true;
      controller()->Resume();
    }

    bool resumed_;
    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.
  std::unique_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