1 // Copyright 2017 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 "third_party/blink/renderer/platform/loader/allowed_by_nosniff.h"
6 
7 #include "testing/gmock/include/gmock/gmock.h"
8 #include "testing/gtest/include/gtest/gtest.h"
9 #include "third_party/blink/public/mojom/web_feature/web_feature.mojom-blink.h"
10 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
11 #include "third_party/blink/renderer/platform/loader/fetch/console_logger.h"
12 #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
13 #include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
14 #include "third_party/blink/renderer/platform/loader/testing/test_loader_factory.h"
15 #include "third_party/blink/renderer/platform/loader/testing/test_resource_fetcher_properties.h"
16 
17 namespace blink {
18 
19 namespace {
20 
21 using MimeTypeCheck = AllowedByNosniff::MimeTypeCheck;
22 using WebFeature = mojom::WebFeature;
23 using ::testing::_;
24 
25 class MockUseCounter : public GarbageCollected<MockUseCounter>,
26                        public UseCounter {
27   USING_GARBAGE_COLLECTED_MIXIN(MockUseCounter);
28 
29  public:
Create()30   static MockUseCounter* Create() {
31     return MakeGarbageCollected<testing::StrictMock<MockUseCounter>>();
32   }
33 
34   MOCK_METHOD1(CountUse, void(mojom::WebFeature));
35   MOCK_METHOD1(CountDeprecation, void(mojom::WebFeature));
36 };
37 
38 class MockConsoleLogger : public GarbageCollected<MockConsoleLogger>,
39                           public ConsoleLogger {
40   USING_GARBAGE_COLLECTED_MIXIN(MockConsoleLogger);
41 
42  public:
43   MOCK_METHOD4(AddConsoleMessageImpl,
44                void(mojom::ConsoleMessageSource,
45                     mojom::ConsoleMessageLevel,
46                     const String&,
47                     bool));
48 };
49 
50 }  // namespace
51 
52 class AllowedByNosniffTest : public testing::Test {
53  public:
54 };
55 
TEST_F(AllowedByNosniffTest,AllowedOrNot)56 TEST_F(AllowedByNosniffTest, AllowedOrNot) {
57   struct {
58     const char* mimetype;
59     bool allowed;
60     bool strict_allowed;
61   } data[] = {
62       // Supported mimetypes:
63       {"text/javascript", true, true},
64       {"application/javascript", true, true},
65       {"text/ecmascript", true, true},
66 
67       // Blocked mimetpyes:
68       {"image/png", false, false},
69       {"text/csv", false, false},
70       {"video/mpeg", false, false},
71 
72       // Legacy mimetypes:
73       {"text/html", true, false},
74       {"text/plain", true, false},
75       {"application/xml", true, false},
76       {"application/octet-stream", true, false},
77 
78       // Potato mimetypes:
79       {"text/potato", true, false},
80       {"potato/text", true, false},
81       {"aaa/aaa", true, false},
82       {"zzz/zzz", true, false},
83 
84       // Parameterized mime types:
85       {"text/javascript; charset=utf-8", true, true},
86       {"text/javascript;charset=utf-8", true, true},
87       {"text/javascript;bla;bla", true, true},
88       {"text/csv; charset=utf-8", false, false},
89       {"text/csv;charset=utf-8", false, false},
90       {"text/csv;bla;bla", false, false},
91 
92       // Funky capitalization:
93       {"text/html", true, false},
94       {"Text/html", true, false},
95       {"text/Html", true, false},
96       {"TeXt/HtMl", true, false},
97       {"TEXT/HTML", true, false},
98   };
99 
100   for (auto& testcase : data) {
101     SCOPED_TRACE(testing::Message()
102                  << "\n  mime type: " << testcase.mimetype
103                  << "\n  allowed: " << (testcase.allowed ? "true" : "false")
104                  << "\n  strict_allowed: "
105                  << (testcase.strict_allowed ? "true" : "false"));
106 
107     const KURL url("https://bla.com/");
108     Persistent<MockUseCounter> use_counter = MockUseCounter::Create();
109     Persistent<MockConsoleLogger> logger =
110         MakeGarbageCollected<MockConsoleLogger>();
111     ResourceResponse response(url);
112     response.SetHttpHeaderField("Content-Type", testcase.mimetype);
113 
114     EXPECT_CALL(*use_counter, CountUse(_)).Times(::testing::AnyNumber());
115     if (!testcase.allowed)
116       EXPECT_CALL(*logger, AddConsoleMessageImpl(_, _, _, _));
117     EXPECT_EQ(testcase.allowed, AllowedByNosniff::MimeTypeAsScript(
118                                     *use_counter, logger, response,
119                                     MimeTypeCheck::kLaxForElement));
120     ::testing::Mock::VerifyAndClear(use_counter);
121 
122     EXPECT_CALL(*use_counter, CountUse(_)).Times(::testing::AnyNumber());
123     if (!testcase.allowed)
124       EXPECT_CALL(*logger, AddConsoleMessageImpl(_, _, _, _));
125     EXPECT_EQ(testcase.allowed,
126               AllowedByNosniff::MimeTypeAsScript(*use_counter, logger, response,
127                                                  MimeTypeCheck::kLaxForWorker));
128     ::testing::Mock::VerifyAndClear(use_counter);
129 
130     EXPECT_CALL(*use_counter, CountUse(_)).Times(::testing::AnyNumber());
131     if (!testcase.strict_allowed)
132       EXPECT_CALL(*logger, AddConsoleMessageImpl(_, _, _, _));
133     EXPECT_EQ(testcase.strict_allowed,
134               AllowedByNosniff::MimeTypeAsScript(*use_counter, logger, response,
135                                                  MimeTypeCheck::kStrict));
136     ::testing::Mock::VerifyAndClear(use_counter);
137   }
138 }
139 
TEST_F(AllowedByNosniffTest,Counters)140 TEST_F(AllowedByNosniffTest, Counters) {
141   constexpr auto kBasic = network::mojom::FetchResponseType::kBasic;
142   constexpr auto kOpaque = network::mojom::FetchResponseType::kOpaque;
143   constexpr auto kCors = network::mojom::FetchResponseType::kCors;
144   const char* bla = "https://bla.com";
145   const char* blubb = "https://blubb.com";
146   struct {
147     const char* url;
148     const char* origin;
149     const char* mimetype;
150     network::mojom::FetchResponseType response_type;
151     WebFeature expected;
152   } data[] = {
153       // Test same- vs cross-origin cases.
154       {bla, "", "text/plain", kOpaque, WebFeature::kCrossOriginTextScript},
155       {bla, "", "text/plain", kCors, WebFeature::kCrossOriginTextPlain},
156       {bla, blubb, "text/plain", kCors, WebFeature::kCrossOriginTextScript},
157       {bla, blubb, "text/plain", kOpaque, WebFeature::kCrossOriginTextPlain},
158       {bla, bla, "text/plain", kBasic, WebFeature::kSameOriginTextScript},
159       {bla, bla, "text/plain", kBasic, WebFeature::kSameOriginTextPlain},
160 
161       // Test mime type and subtype handling.
162       {bla, bla, "text/xml", kBasic, WebFeature::kSameOriginTextScript},
163       {bla, bla, "text/xml", kBasic, WebFeature::kSameOriginTextXml},
164 
165       // Test mime types from crbug.com/765544, with random cross/same site
166       // origins.
167       {bla, bla, "text/plain", kBasic, WebFeature::kSameOriginTextPlain},
168       {bla, bla, "text/xml", kOpaque, WebFeature::kCrossOriginTextXml},
169       {blubb, blubb, "application/octet-stream", kBasic,
170        WebFeature::kSameOriginApplicationOctetStream},
171       {blubb, blubb, "application/xml", kCors,
172        WebFeature::kCrossOriginApplicationXml},
173       {bla, bla, "text/html", kBasic, WebFeature::kSameOriginTextHtml},
174   };
175 
176   for (auto& testcase : data) {
177     SCOPED_TRACE(testing::Message()
178                  << "\n  url: " << testcase.url << "\n  origin: "
179                  << testcase.origin << "\n  mime type: " << testcase.mimetype
180                  << "\n response type: " << testcase.response_type
181                  << "\n  webfeature: " << testcase.expected);
182     Persistent<MockUseCounter> use_counter = MockUseCounter::Create();
183     Persistent<MockConsoleLogger> logger =
184         MakeGarbageCollected<MockConsoleLogger>();
185     ResourceResponse response(KURL(testcase.url));
186     response.SetType(testcase.response_type);
187     response.SetHttpHeaderField("Content-Type", testcase.mimetype);
188 
189     EXPECT_CALL(*use_counter, CountUse(testcase.expected));
190     EXPECT_CALL(*use_counter, CountUse(::testing::Ne(testcase.expected)))
191         .Times(::testing::AnyNumber());
192     AllowedByNosniff::MimeTypeAsScript(*use_counter, logger, response,
193                                        MimeTypeCheck::kLaxForElement);
194     ::testing::Mock::VerifyAndClear(use_counter);
195 
196     EXPECT_CALL(*use_counter, CountUse(testcase.expected));
197     EXPECT_CALL(*use_counter, CountUse(::testing::Ne(testcase.expected)))
198         .Times(::testing::AnyNumber());
199     AllowedByNosniff::MimeTypeAsScript(*use_counter, logger, response,
200                                        MimeTypeCheck::kLaxForWorker);
201     ::testing::Mock::VerifyAndClear(use_counter);
202 
203     EXPECT_CALL(*use_counter,
204                 CountUse(WebFeature::kStrictMimeTypeChecksWouldBlockWorker));
205     EXPECT_CALL(*use_counter,
206                 CountUse(::testing::Ne(
207                     WebFeature::kStrictMimeTypeChecksWouldBlockWorker)))
208         .Times(::testing::AnyNumber());
209     AllowedByNosniff::MimeTypeAsScript(*use_counter, logger, response,
210                                        MimeTypeCheck::kLaxForWorker);
211     ::testing::Mock::VerifyAndClear(use_counter);
212   }
213 }
214 
TEST_F(AllowedByNosniffTest,AllTheSchemes)215 TEST_F(AllowedByNosniffTest, AllTheSchemes) {
216   // We test various URL schemes.
217   // To force a decision based on the scheme, we give all responses an
218   // invalid Content-Type plus a "nosniff" header. That way, all Content-Type
219   // based checks are always denied and we can test for whether this is decided
220   // based on the URL or not.
221   struct {
222     const char* url;
223     bool allowed;
224   } data[] = {
225       {"http://example.com/bla.js", false},
226       {"https://example.com/bla.js", false},
227       {"file://etc/passwd.js", true},
228       {"file://etc/passwd", false},
229       {"chrome://dino/dino.js", true},
230       {"chrome://dino/dino.css", false},
231       {"ftp://example.com/bla.js", true},
232       {"ftp://example.com/bla.txt", false},
233 
234       {"file://home/potato.txt", false},
235       {"file://home/potato.js", true},
236       {"file://home/potato.mjs", true},
237       {"chrome://dino/dino.mjs", true},
238 
239       // `blob:` and `filesystem:` are excluded:
240       {"blob:https://example.com/bla.js", true},
241       {"blob:https://example.com/bla.txt", true},
242       {"filesystem:https://example.com/temporary/bla.js", true},
243       {"filesystem:https://example.com/temporary/bla.txt", true},
244   };
245 
246   for (auto& testcase : data) {
247     auto* use_counter = MockUseCounter::Create();
248     Persistent<MockConsoleLogger> logger =
249         MakeGarbageCollected<MockConsoleLogger>();
250     EXPECT_CALL(*logger, AddConsoleMessageImpl(_, _, _, _))
251         .Times(::testing::AnyNumber());
252     SCOPED_TRACE(testing::Message() << "\n  url: " << testcase.url
253                                     << "\n  allowed: " << testcase.allowed);
254     ResourceResponse response(KURL(testcase.url));
255     response.SetHttpHeaderField("Content-Type", "invalid");
256     response.SetHttpHeaderField("X-Content-Type-Options", "nosniff");
257     EXPECT_EQ(testcase.allowed,
258               AllowedByNosniff::MimeTypeAsScript(*use_counter, logger, response,
259                                                  MimeTypeCheck::kStrict));
260     EXPECT_EQ(testcase.allowed, AllowedByNosniff::MimeTypeAsScript(
261                                     *use_counter, logger, response,
262                                     MimeTypeCheck::kLaxForElement));
263     EXPECT_EQ(testcase.allowed,
264               AllowedByNosniff::MimeTypeAsScript(*use_counter, logger, response,
265                                                  MimeTypeCheck::kLaxForWorker));
266   }
267 }
268 
269 }  // namespace blink
270