1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/browser/renderer_host/ancestor_throttle.h"
6
7 #include "base/bind.h"
8 #include "base/callback_helpers.h"
9 #include "base/memory/ref_counted.h"
10 #include "base/test/scoped_feature_list.h"
11 #include "content/browser/renderer_host/render_frame_host_impl.h"
12 #include "content/public/browser/navigation_handle.h"
13 #include "content/public/browser/navigation_throttle.h"
14 #include "content/public/browser/web_contents.h"
15 #include "content/public/test/test_renderer_host.h"
16 #include "content/test/navigation_simulator_impl.h"
17 #include "content/test/test_navigation_url_loader.h"
18 #include "net/http/http_response_headers.h"
19 #include "services/network/public/cpp/content_security_policy/content_security_policy.h"
20 #include "services/network/public/cpp/features.h"
21 #include "testing/gmock/include/gmock/gmock.h"
22 #include "testing/gtest/include/gtest/gtest.h"
23
24 namespace content {
25
26 namespace {
27
28 using HeaderDisposition = AncestorThrottle::HeaderDisposition;
29
GetAncestorHeaders(const char * xfo,const char * csp)30 net::HttpResponseHeaders* GetAncestorHeaders(const char* xfo, const char* csp) {
31 std::string header_string("HTTP/1.1 200 OK\nX-Frame-Options: ");
32 header_string += xfo;
33 if (csp != nullptr) {
34 header_string += "\nContent-Security-Policy: ";
35 header_string += csp;
36 }
37 header_string += "\n\n";
38 std::replace(header_string.begin(), header_string.end(), '\n', '\0');
39 net::HttpResponseHeaders* headers =
40 new net::HttpResponseHeaders(header_string);
41 EXPECT_TRUE(headers->HasHeader("X-Frame-Options"));
42 if (csp != nullptr)
43 EXPECT_TRUE(headers->HasHeader("Content-Security-Policy"));
44 return headers;
45 }
46
ParsePolicy(const std::string & policy)47 network::mojom::ContentSecurityPolicyPtr ParsePolicy(
48 const std::string& policy) {
49 scoped_refptr<net::HttpResponseHeaders> headers(
50 new net::HttpResponseHeaders("HTTP/1.1 200 OK"));
51 headers->SetHeader("Content-Security-Policy", policy);
52 std::vector<network::mojom::ContentSecurityPolicyPtr> policies;
53 network::AddContentSecurityPolicyFromHeaders(
54 *headers, GURL("https://example.com/"), &policies);
55 return std::move(policies[0]);
56 }
57
58 } // namespace
59
60 // AncestorThrottleTest
61 // -------------------------------------------------------------
62
63 class AncestorThrottleTest : public testing::Test {};
64
TEST_F(AncestorThrottleTest,ParsingXFrameOptions)65 TEST_F(AncestorThrottleTest, ParsingXFrameOptions) {
66 struct TestCase {
67 const char* header;
68 AncestorThrottle::HeaderDisposition expected;
69 const char* value;
70 } cases[] = {
71 // Basic keywords
72 {"DENY", HeaderDisposition::DENY, "DENY"},
73 {"SAMEORIGIN", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
74 {"ALLOWALL", HeaderDisposition::ALLOWALL, "ALLOWALL"},
75
76 // Repeated keywords
77 {"DENY,DENY", HeaderDisposition::DENY, "DENY, DENY"},
78 {"SAMEORIGIN,SAMEORIGIN", HeaderDisposition::SAMEORIGIN,
79 "SAMEORIGIN, SAMEORIGIN"},
80 {"ALLOWALL,ALLOWALL", HeaderDisposition::ALLOWALL, "ALLOWALL, ALLOWALL"},
81
82 // Case-insensitive
83 {"deNy", HeaderDisposition::DENY, "deNy"},
84 {"sAmEorIgIn", HeaderDisposition::SAMEORIGIN, "sAmEorIgIn"},
85 {"AlLOWaLL", HeaderDisposition::ALLOWALL, "AlLOWaLL"},
86
87 // Trim whitespace
88 {" DENY", HeaderDisposition::DENY, "DENY"},
89 {"SAMEORIGIN ", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
90 {" ALLOWALL ", HeaderDisposition::ALLOWALL, "ALLOWALL"},
91 {" DENY", HeaderDisposition::DENY, "DENY"},
92 {"SAMEORIGIN ", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
93 {" ALLOWALL ", HeaderDisposition::ALLOWALL, "ALLOWALL"},
94 {" DENY , DENY ", HeaderDisposition::DENY, "DENY, DENY"},
95 {"SAMEORIGIN, SAMEORIGIN", HeaderDisposition::SAMEORIGIN,
96 "SAMEORIGIN, SAMEORIGIN"},
97 {"ALLOWALL ,ALLOWALL", HeaderDisposition::ALLOWALL,
98 "ALLOWALL, ALLOWALL"},
99 };
100
101 AncestorThrottle throttle(nullptr);
102 for (const auto& test : cases) {
103 SCOPED_TRACE(test.header);
104 scoped_refptr<net::HttpResponseHeaders> headers =
105 GetAncestorHeaders(test.header, nullptr);
106 std::string header_value;
107 EXPECT_EQ(test.expected,
108 throttle.ParseXFrameOptionsHeader(headers.get(), &header_value));
109 EXPECT_EQ(test.value, header_value);
110 }
111 }
112
TEST_F(AncestorThrottleTest,ErrorsParsingXFrameOptions)113 TEST_F(AncestorThrottleTest, ErrorsParsingXFrameOptions) {
114 struct TestCase {
115 const char* header;
116 AncestorThrottle::HeaderDisposition expected;
117 const char* failure;
118 } cases[] = {
119 // Empty == Invalid.
120 {"", HeaderDisposition::INVALID, ""},
121
122 // Invalid
123 {"INVALID", HeaderDisposition::INVALID, "INVALID"},
124 {"INVALID DENY", HeaderDisposition::INVALID, "INVALID DENY"},
125 {"DENY DENY", HeaderDisposition::INVALID, "DENY DENY"},
126 {"DE NY", HeaderDisposition::INVALID, "DE NY"},
127
128 // Conflicts
129 {"INVALID,DENY", HeaderDisposition::CONFLICT, "INVALID, DENY"},
130 {"DENY,ALLOWALL", HeaderDisposition::CONFLICT, "DENY, ALLOWALL"},
131 {"SAMEORIGIN,DENY", HeaderDisposition::CONFLICT, "SAMEORIGIN, DENY"},
132 {"ALLOWALL,SAMEORIGIN", HeaderDisposition::CONFLICT,
133 "ALLOWALL, SAMEORIGIN"},
134 {"DENY, SAMEORIGIN", HeaderDisposition::CONFLICT, "DENY, SAMEORIGIN"}};
135
136 AncestorThrottle throttle(nullptr);
137 for (const auto& test : cases) {
138 SCOPED_TRACE(test.header);
139 scoped_refptr<net::HttpResponseHeaders> headers =
140 GetAncestorHeaders(test.header, nullptr);
141 std::string header_value;
142 EXPECT_EQ(test.expected,
143 throttle.ParseXFrameOptionsHeader(headers.get(), &header_value));
144 EXPECT_EQ(test.failure, header_value);
145 }
146 }
147
TEST_F(AncestorThrottleTest,AllowsBlanketEnforcementOfRequiredCSP)148 TEST_F(AncestorThrottleTest, AllowsBlanketEnforcementOfRequiredCSP) {
149 base::test::ScopedFeatureList feature_list;
150 feature_list.InitAndEnableFeature(network::features::kOutOfBlinkCSPEE);
151
152 struct TestCase {
153 const char* name;
154 const char* request_origin;
155 const char* response_origin;
156 const char* allow_csp_from;
157 bool expected_result;
158 } cases[] = {
159 {
160 "About scheme allows",
161 "http://example.com",
162 "about://me",
163 nullptr,
164 true,
165 },
166 {
167 "File scheme allows",
168 "http://example.com",
169 "file://me",
170 nullptr,
171 true,
172 },
173 {
174 "Data scheme allows",
175 "http://example.com",
176 "data://me",
177 nullptr,
178 true,
179 },
180 {
181 "Filesystem scheme allows",
182 "http://example.com",
183 "filesystem://me",
184 nullptr,
185 true,
186 },
187 {
188 "Blob scheme allows",
189 "http://example.com",
190 "blob://me",
191 nullptr,
192 true,
193 },
194 {
195 "Same origin allows",
196 "http://example.com",
197 "http://example.com",
198 nullptr,
199 true,
200 },
201 {
202 "Same origin allows independently of header",
203 "http://example.com",
204 "http://example.com",
205 "http://not-example.com",
206 true,
207 },
208 {
209 "Different origin does not allow",
210 "http://example.com",
211 "http://not.example.com",
212 nullptr,
213 false,
214 },
215 {
216 "Different origin with right header allows",
217 "http://example.com",
218 "http://not-example.com",
219 "http://example.com",
220 true,
221 },
222 {
223 "Different origin with right header 2 allows",
224 "http://example.com",
225 "http://not-example.com",
226 "http://example.com/",
227 true,
228 },
229 {
230 "Different origin with wrong header does not allow",
231 "http://example.com",
232 "http://not-example.com",
233 "http://not-example.com",
234 false,
235 },
236 {
237 "Wildcard header allows",
238 "http://example.com",
239 "http://not-example.com",
240 "*",
241 true,
242 },
243 {
244 "Malformed header does not allow",
245 "http://example.com",
246 "http://not-example.com",
247 "*; http://example.com",
248 false,
249 },
250 };
251
252 for (const auto& test : cases) {
253 SCOPED_TRACE(test.name);
254 auto headers =
255 base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
256 if (test.allow_csp_from)
257 headers->AddHeader("allow-csp-from", test.allow_csp_from);
258 auto allow_csp_from = network::ParseAllowCSPFromHeader(*headers);
259
260 bool actual = AncestorThrottle::AllowsBlanketEnforcementOfRequiredCSP(
261 url::Origin::Create(GURL(test.request_origin)),
262 GURL(test.response_origin), allow_csp_from);
263 EXPECT_EQ(test.expected_result, actual);
264 }
265 }
266
267 using AncestorThrottleNavigationTest = RenderViewHostTestHarness;
268
TEST_F(AncestorThrottleNavigationTest,WillStartRequestAddsSecRequiredCSPHeader)269 TEST_F(AncestorThrottleNavigationTest,
270 WillStartRequestAddsSecRequiredCSPHeader) {
271 base::test::ScopedFeatureList feature_list;
272 feature_list.InitAndEnableFeature(network::features::kOutOfBlinkCSPEE);
273
274 // Create a frame tree with different 'csp' attributes according to the
275 // following graph:
276 //
277 // FRAME NAME | 'csp' attribute
278 // ------------------------------|-------------------------------------
279 // main_frame | (none)
280 // ├─child_with_csp | script-src 'none'
281 // │ ├─grandchild_same_csp | script-src 'none'
282 // │ ├─grandchild_no_csp | (none)
283 // │ │ └─grandgrandchild | (none)
284 // │ ├─grandchild_invalid_csp | report-to group
285 // │ └─grandchild_invalid_csp2 | script-src 'none'; invalid-directive
286 // └─sibling | (none)
287 //
288 // Test that the required CSP of every frame is computed/inherited correctly
289 // and that the Sec-Required-CSP header is set.
290
291 auto test = [](TestRenderFrameHost* frame, std::string csp_attr,
292 std::string expect_csp) {
293 SCOPED_TRACE(frame->GetFrameName());
294
295 if (!csp_attr.empty())
296 frame->frame_tree_node()->set_csp_attribute(ParsePolicy(csp_attr));
297
298 std::unique_ptr<NavigationSimulator> simulator =
299 content::NavigationSimulator::CreateRendererInitiated(
300 // Chrome blocks a frame navigating to a URL if more than one of its
301 // ancestors have the same URL. Use a different URL every time, to
302 // avoid blocking navigation of the grandchild frame.
303 GURL("https://www.example.com/" + frame->GetFrameName()), frame);
304 simulator->Start();
305 NavigationRequest* request =
306 NavigationRequest::From(simulator->GetNavigationHandle());
307 std::string header_value;
308 bool found = request->GetRequestHeaders().GetHeader("sec-required-csp",
309 &header_value);
310 if (!expect_csp.empty()) {
311 EXPECT_TRUE(found);
312 EXPECT_EQ(expect_csp, header_value);
313 } else {
314 EXPECT_FALSE(found);
315 }
316
317 // Complete the navigation so that the required csp is stored in the
318 // RenderFrameHost, so that when we will add children to this frame they
319 // will be able to get the parent's required csp (and hence also test that
320 // the whole logic works).
321 auto response_headers =
322 base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
323 response_headers->SetHeader("Allow-CSP-From", "*");
324 simulator->SetResponseHeaders(response_headers);
325 simulator->Commit();
326 };
327
328 auto* main_frame = static_cast<TestRenderFrameHost*>(main_rfh());
329 test(main_frame, "", "");
330
331 auto* child_with_csp = static_cast<TestRenderFrameHost*>(
332 content::RenderFrameHostTester::For(main_frame)
333 ->AppendChild("child_with_csp"));
334 test(child_with_csp, "script-src 'none'", "script-src 'none'");
335
336 auto* grandchild_same_csp = static_cast<TestRenderFrameHost*>(
337 content::RenderFrameHostTester::For(child_with_csp)
338 ->AppendChild("grandchild_same_csp"));
339 test(grandchild_same_csp, "script-src 'none'", "script-src 'none'");
340
341 auto* grandchild_no_csp = static_cast<TestRenderFrameHost*>(
342 content::RenderFrameHostTester::For(child_with_csp)
343 ->AppendChild("grandchild_no_csp"));
344 test(grandchild_no_csp, "", "script-src 'none'");
345
346 auto* grandgrandchild = static_cast<TestRenderFrameHost*>(
347 content::RenderFrameHostTester::For(grandchild_no_csp)
348 ->AppendChild("grandgrandchild"));
349 test(grandgrandchild, "", "script-src 'none'");
350
351 auto* grandchild_invalid_csp = static_cast<TestRenderFrameHost*>(
352 content::RenderFrameHostTester::For(child_with_csp)
353 ->AppendChild("grandchild_invalid_csp"));
354 test(grandchild_invalid_csp, "report-to group", "script-src 'none'");
355
356 auto* grandchild_invalid_csp2 = static_cast<TestRenderFrameHost*>(
357 content::RenderFrameHostTester::For(child_with_csp)
358 ->AppendChild("grandchild_invalid_csp2"));
359 test(grandchild_invalid_csp2, "script-src 'none'; invalid-directive",
360 "script-src 'none'");
361
362 auto* sibling = static_cast<TestRenderFrameHost*>(
363 content::RenderFrameHostTester::For(main_frame)->AppendChild("sibling"));
364 test(sibling, "", "");
365 }
366
TEST_F(AncestorThrottleNavigationTest,EvaluateCSPEmbeddedEnforcement)367 TEST_F(AncestorThrottleNavigationTest, EvaluateCSPEmbeddedEnforcement) {
368 base::test::ScopedFeatureList feature_list;
369 feature_list.InitAndEnableFeature(network::features::kOutOfBlinkCSPEE);
370
371 // We need one initial navigation to set up everything.
372 NavigateAndCommit(GURL("https://www.example.org"));
373
374 auto* main_frame = static_cast<TestRenderFrameHost*>(main_rfh());
375
376 struct TestCase {
377 const char* name;
378 const char* required_csp;
379 const char* frame_url;
380 const char* allow_csp_from;
381 const char* returned_csp;
382 bool expect_allow;
383 } cases[] = {
384 {
385 "No required csp",
386 nullptr,
387 "https://www.not-example.org",
388 nullptr,
389 nullptr,
390 true,
391 },
392 {
393 "Required csp - Same origin",
394 "script-src 'none'",
395 "https://www.example.org",
396 nullptr,
397 nullptr,
398 true,
399 },
400 {
401 "Required csp - Cross origin",
402 "script-src 'none'",
403 "https://www.not-example.org",
404 nullptr,
405 nullptr,
406 false,
407 },
408 {
409 "Required csp - Cross origin with Allow-CSP-From",
410 "script-src 'none'",
411 "https://www.not-example.org",
412 "*",
413 nullptr,
414 true,
415 },
416 {
417 "Required csp - Cross origin with wrong Allow-CSP-From",
418 "script-src 'none'",
419 "https://www.not-example.org",
420 "https://www.another-example.org",
421 nullptr,
422 false,
423 },
424 {
425 "Required csp - Cross origin with non-subsuming CSPs",
426 "script-src 'none'",
427 "https://www.not-example.org",
428 nullptr,
429 "style-src 'none'",
430 false,
431 },
432 {
433 "Required csp - Cross origin with subsuming CSPs",
434 "script-src 'none'",
435 "https://www.not-example.org",
436 nullptr,
437 "script-src 'none'",
438 true,
439 },
440 {
441 "Required csp - Cross origin with wrong Allow-CSP-From but subsuming "
442 "CSPs",
443 "script-src 'none'",
444 "https://www.not-example.org",
445 "https://www.another-example.org",
446 "script-src 'none'",
447 true,
448 },
449 };
450
451 for (auto test : cases) {
452 SCOPED_TRACE(test.name);
453 auto* frame = static_cast<TestRenderFrameHost*>(
454 content::RenderFrameHostTester::For(main_frame)
455 ->AppendChild(test.name));
456
457 if (test.required_csp) {
458 frame->frame_tree_node()->set_csp_attribute(
459 ParsePolicy(test.required_csp));
460 }
461
462 std::unique_ptr<NavigationSimulator> simulator =
463 content::NavigationSimulator::CreateRendererInitiated(
464 GURL(test.frame_url), frame);
465
466 auto response_headers =
467 base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
468 if (test.allow_csp_from)
469 response_headers->SetHeader("Allow-CSP-From", test.allow_csp_from);
470 if (test.returned_csp)
471 response_headers->SetHeader("Content-Security-Policy", test.returned_csp);
472
473 simulator->SetResponseHeaders(response_headers);
474 simulator->ReadyToCommit();
475
476 if (test.expect_allow) {
477 EXPECT_EQ(NavigationThrottle::PROCEED,
478 simulator->GetLastThrottleCheckResult());
479 } else {
480 EXPECT_EQ(NavigationThrottle::BLOCK_RESPONSE,
481 simulator->GetLastThrottleCheckResult());
482 }
483 }
484 }
485
486 } // namespace content
487