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