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