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