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