1/*
2 * Copyright (C) 2011 Google, Inc. All rights reserved.
3 * Copyright (C) 2016 Apple Inc. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "config.h"
28#include "ContentSecurityPolicySourceList.h"
29
30#include "ContentSecurityPolicy.h"
31#include "ContentSecurityPolicyDirectiveNames.h"
32#include "ParsingUtilities.h"
33#include "TextEncoding.h"
34#include <wtf/ASCIICType.h>
35#include <wtf/NeverDestroyed.h>
36#include <wtf/URL.h>
37#include <wtf/text/Base64.h>
38
39namespace WebCore {
40
41static bool isCSPDirectiveName(const String& name)
42{
43 return equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::baseURI)
44 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::connectSrc)
45 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::defaultSrc)
46 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::fontSrc)
47 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::formAction)
48 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::frameSrc)
49 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::imgSrc)
50 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::mediaSrc)
51 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::objectSrc)
52 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::pluginTypes)
53 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::reportURI)
54 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::sandbox)
55 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::scriptSrc)
56 || equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::styleSrc);
57}
58
59static bool isSourceCharacter(UChar c)
60{
61 return !isASCIISpace(c);
62}
63
64static bool isHostCharacter(UChar c)
65{
66 return isASCIIAlphanumeric(c) || c == '-';
67}
68
69static bool isPathComponentCharacter(UChar c)
70{
71 return c != '?' && c != '#';
72}
73
74static bool isSchemeContinuationCharacter(UChar c)
75{
76 return isASCIIAlphanumeric(c) || c == '+' || c == '-' || c == '.';
77}
78
79static bool isNotColonOrSlash(UChar c)
80{
81 return c != ':' && c != '/';
82}
83
84static bool isSourceListNone(const String& value)
85{
86 auto characters = StringView(value).upconvertedCharacters();
87 const UChar* begin = characters;
88 const UChar* end = characters + value.length();
89 skipWhile<UChar, isASCIISpace>(begin, end);
90
91 const UChar* position = begin;
92 skipWhile<UChar, isSourceCharacter>(position, end);
93 if (!equalLettersIgnoringASCIICase(begin, position - begin, "'none'"))
94 return false;
95
96 skipWhile<UChar, isASCIISpace>(position, end);
97 if (position != end)
98 return false;
99
100 return true;
101}
102
103ContentSecurityPolicySourceList::ContentSecurityPolicySourceList(const ContentSecurityPolicy& policy, const String& directiveName)
104 : m_policy(policy)
105 , m_directiveName(directiveName)
106{
107}
108
109void ContentSecurityPolicySourceList::parse(const String& value)
110{
111 if (isSourceListNone(value)) {
112 m_isNone = true;
113 return;
114 }
115 auto characters = StringView(value).upconvertedCharacters();
116 parse(characters, characters + value.length());
117}
118
119bool ContentSecurityPolicySourceList::isProtocolAllowedByStar(const URL& url) const
120{
121 if (m_policy.allowContentSecurityPolicySourceStarToMatchAnyProtocol())
122 return true;
123
124 // Although not allowed by the Content Security Policy Level 3 spec., we allow a data URL to match
125 // "img-src *" and either a data URL or blob URL to match "media-src *" for web compatibility.
126 bool isAllowed = url.protocolIsInHTTPFamily() || url.protocolIs("ws") || url.protocolIs("wss") || m_policy.protocolMatchesSelf(url);
127 if (equalIgnoringASCIICase(m_directiveName, ContentSecurityPolicyDirectiveNames::imgSrc))
128 isAllowed |= url.protocolIsData();
129 else if (equalIgnoringASCIICase(m_directiveName, ContentSecurityPolicyDirectiveNames::mediaSrc))
130 isAllowed |= url.protocolIsData() || url.protocolIsBlob();
131 return isAllowed;
132}
133
134bool ContentSecurityPolicySourceList::matches(const URL& url, bool didReceiveRedirectResponse) const
135{
136 if (m_allowStar && isProtocolAllowedByStar(url))
137 return true;
138
139 if (m_allowSelf && m_policy.urlMatchesSelf(url))
140 return true;
141
142 for (auto& entry : m_list) {
143 if (entry.matches(url, didReceiveRedirectResponse))
144 return true;
145 }
146
147 return false;
148}
149
150bool ContentSecurityPolicySourceList::matches(const ContentSecurityPolicyHash& hash) const
151{
152 return m_hashes.contains(hash);
153}
154
155bool ContentSecurityPolicySourceList::matches(const String& nonce) const
156{
157 return m_nonces.contains(nonce);
158}
159
160// source-list = *WSP [ source *( 1*WSP source ) *WSP ]
161// / *WSP "'none'" *WSP
162//
163void ContentSecurityPolicySourceList::parse(const UChar* begin, const UChar* end)
164{
165 const UChar* position = begin;
166
167 while (position < end) {
168 skipWhile<UChar, isASCIISpace>(position, end);
169 if (position == end)
170 return;
171
172 const UChar* beginSource = position;
173 skipWhile<UChar, isSourceCharacter>(position, end);
174
175 String scheme, host, path;
176 Optional<uint16_t> port;
177 bool hostHasWildcard = false;
178 bool portHasWildcard = false;
179
180 if (parseNonceSource(beginSource, position))
181 continue;
182
183 if (parseHashSource(beginSource, position))
184 continue;
185
186 if (parseSource(beginSource, position, scheme, host, port, path, hostHasWildcard, portHasWildcard)) {
187 // Wildcard hosts and keyword sources ('self', 'unsafe-inline',
188 // etc.) aren't stored in m_list, but as attributes on the source
189 // list itself.
190 if (scheme.isEmpty() && host.isEmpty())
191 continue;
192 if (isCSPDirectiveName(host))
193 m_policy.reportDirectiveAsSourceExpression(m_directiveName, host);
194 m_list.append(ContentSecurityPolicySource(m_policy, scheme, host, port, path, hostHasWildcard, portHasWildcard));
195 } else
196 m_policy.reportInvalidSourceExpression(m_directiveName, String(beginSource, position - beginSource));
197
198 ASSERT(position == end || isASCIISpace(*position));
199 }
200
201 m_list.shrinkToFit();
202}
203
204// source = scheme ":"
205// / ( [ scheme "://" ] host [ port ] [ path ] )
206// / "'self'"
207//
208bool ContentSecurityPolicySourceList::parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, Optional<uint16_t>& port, String& path, bool& hostHasWildcard, bool& portHasWildcard)
209{
210 if (begin == end)
211 return false;
212
213 if (equalLettersIgnoringASCIICase(begin, end - begin, "'none'"))
214 return false;
215
216 if (end - begin == 1 && *begin == '*') {
217 m_allowStar = true;
218 return true;
219 }
220
221 if (equalLettersIgnoringASCIICase(begin, end - begin, "'self'")) {
222 m_allowSelf = true;
223 return true;
224 }
225
226 if (equalLettersIgnoringASCIICase(begin, end - begin, "'unsafe-inline'")) {
227 m_allowInline = true;
228 return true;
229 }
230
231 if (equalLettersIgnoringASCIICase(begin, end - begin, "'unsafe-eval'")) {
232 m_allowEval = true;
233 return true;
234 }
235
236 const UChar* position = begin;
237 const UChar* beginHost = begin;
238 const UChar* beginPath = end;
239 const UChar* beginPort = nullptr;
240
241 skipWhile<UChar, isNotColonOrSlash>(position, end);
242
243 if (position == end) {
244 // host
245 // ^
246 return parseHost(beginHost, position, host, hostHasWildcard);
247 }
248
249 if (position < end && *position == '/') {
250 // host/path || host/ || /
251 // ^ ^ ^
252 return parseHost(beginHost, position, host, hostHasWildcard) && parsePath(position, end, path);
253 }
254
255 if (position < end && *position == ':') {
256 if (end - position == 1) {
257 // scheme:
258 // ^
259 return parseScheme(begin, position, scheme);
260 }
261
262 if (position[1] == '/') {
263 // scheme://host || scheme://
264 // ^ ^
265 if (!parseScheme(begin, position, scheme)
266 || !skipExactly<UChar>(position, end, ':')
267 || !skipExactly<UChar>(position, end, '/')
268 || !skipExactly<UChar>(position, end, '/'))
269 return false;
270 if (position == end)
271 return false;
272 beginHost = position;
273 skipWhile<UChar, isNotColonOrSlash>(position, end);
274 }
275
276 if (position < end && *position == ':') {
277 // host:port || scheme://host:port
278 // ^ ^
279 beginPort = position;
280 skipUntil<UChar>(position, end, '/');
281 }
282 }
283
284 if (position < end && *position == '/') {
285 // scheme://host/path || scheme://host:port/path
286 // ^ ^
287 if (position == beginHost)
288 return false;
289
290 beginPath = position;
291 }
292
293 if (!parseHost(beginHost, beginPort ? beginPort : beginPath, host, hostHasWildcard))
294 return false;
295
296 if (!beginPort)
297 port = WTF::nullopt;
298 else {
299 if (!parsePort(beginPort, beginPath, port, portHasWildcard))
300 return false;
301 }
302
303 if (beginPath != end) {
304 if (!parsePath(beginPath, end, path))
305 return false;
306 }
307
308 return true;
309}
310
311// ; <scheme> production from RFC 3986
312// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
313//
314bool ContentSecurityPolicySourceList::parseScheme(const UChar* begin, const UChar* end, String& scheme)
315{
316 ASSERT(begin <= end);
317 ASSERT(scheme.isEmpty());
318
319 if (begin == end)
320 return false;
321
322 const UChar* position = begin;
323
324 if (!skipExactly<UChar, isASCIIAlpha>(position, end))
325 return false;
326
327 skipWhile<UChar, isSchemeContinuationCharacter>(position, end);
328
329 if (position != end)
330 return false;
331
332 scheme = String(begin, end - begin);
333 return true;
334}
335
336// host = [ "*." ] 1*host-char *( "." 1*host-char )
337// / "*"
338// host-char = ALPHA / DIGIT / "-"
339//
340bool ContentSecurityPolicySourceList::parseHost(const UChar* begin, const UChar* end, String& host, bool& hostHasWildcard)
341{
342 ASSERT(begin <= end);
343 ASSERT(host.isEmpty());
344 ASSERT(!hostHasWildcard);
345
346 if (begin == end)
347 return false;
348
349 const UChar* position = begin;
350
351 if (skipExactly<UChar>(position, end, '*')) {
352 hostHasWildcard = true;
353
354 if (position == end)
355 return true;
356
357 if (!skipExactly<UChar>(position, end, '.'))
358 return false;
359 }
360
361 const UChar* hostBegin = position;
362
363 while (position < end) {
364 if (!skipExactly<UChar, isHostCharacter>(position, end))
365 return false;
366
367 skipWhile<UChar, isHostCharacter>(position, end);
368
369 if (position < end && !skipExactly<UChar>(position, end, '.'))
370 return false;
371 }
372
373 ASSERT(position == end);
374 host = String(hostBegin, end - hostBegin);
375 return true;
376}
377
378bool ContentSecurityPolicySourceList::parsePath(const UChar* begin, const UChar* end, String& path)
379{
380 ASSERT(begin <= end);
381 ASSERT(path.isEmpty());
382
383 const UChar* position = begin;
384 skipWhile<UChar, isPathComponentCharacter>(position, end);
385 // path/to/file.js?query=string || path/to/file.js#anchor
386 // ^ ^
387 if (position < end)
388 m_policy.reportInvalidPathCharacter(m_directiveName, String(begin, end - begin), *position);
389
390 path = decodeURLEscapeSequences(String(begin, position - begin));
391
392 ASSERT(position <= end);
393 ASSERT(position == end || (*position == '#' || *position == '?'));
394 return true;
395}
396
397// port = ":" ( 1*DIGIT / "*" )
398//
399bool ContentSecurityPolicySourceList::parsePort(const UChar* begin, const UChar* end, Optional<uint16_t>& port, bool& portHasWildcard)
400{
401 ASSERT(begin <= end);
402 ASSERT(!port);
403 ASSERT(!portHasWildcard);
404
405 if (!skipExactly<UChar>(begin, end, ':'))
406 ASSERT_NOT_REACHED();
407
408 if (begin == end)
409 return false;
410
411 if (end - begin == 1 && *begin == '*') {
412 port = WTF::nullopt;
413 portHasWildcard = true;
414 return true;
415 }
416
417 const UChar* position = begin;
418 skipWhile<UChar, isASCIIDigit>(position, end);
419
420 if (position != end)
421 return false;
422
423 bool ok;
424 int portInt = charactersToIntStrict(begin, end - begin, &ok);
425 if (portInt < 0 || portInt > std::numeric_limits<uint16_t>::max())
426 return false;
427 port = portInt;
428 return ok;
429}
430
431// Match Blink's behavior of allowing an equal sign to appear anywhere in the value of the nonce
432// even though this does not match the behavior of Content Security Policy Level 3 spec.,
433// <https://w3c.github.io/webappsec-csp/> (29 February 2016).
434static bool isNonceCharacter(UChar c)
435{
436 return isBase64OrBase64URLCharacter(c) || c == '=';
437}
438
439// nonce-source = "'nonce-" nonce-value "'"
440// nonce-value = base64-value
441bool ContentSecurityPolicySourceList::parseNonceSource(const UChar* begin, const UChar* end)
442{
443 const unsigned noncePrefixLength = 7;
444 if (!StringView(begin, end - begin).startsWithIgnoringASCIICase("'nonce-"))
445 return false;
446 const UChar* position = begin + noncePrefixLength;
447 const UChar* beginNonceValue = position;
448 skipWhile<UChar, isNonceCharacter>(position, end);
449 if (position >= end || position == beginNonceValue || *position != '\'')
450 return false;
451 m_nonces.add(String(beginNonceValue, position - beginNonceValue));
452 return true;
453}
454
455// hash-source = "'" hash-algorithm "-" base64-value "'"
456// hash-algorithm = "sha256" / "sha384" / "sha512"
457// base64-value = 1*( ALPHA / DIGIT / "+" / "/" / "-" / "_" )*2( "=" )
458bool ContentSecurityPolicySourceList::parseHashSource(const UChar* begin, const UChar* end)
459{
460 if (begin == end)
461 return false;
462
463 const UChar* position = begin;
464 if (!skipExactly<UChar>(position, end, '\''))
465 return false;
466
467 auto digest = parseCryptographicDigest(position, end);
468 if (!digest)
469 return false;
470
471 if (position >= end || *position != '\'')
472 return false;
473
474 if (digest->value.size() > ContentSecurityPolicyHash::maximumDigestLength)
475 return false;
476
477 m_hashAlgorithmsUsed.add(digest->algorithm);
478 m_hashes.add(WTFMove(*digest));
479 return true;
480}
481
482} // namespace WebCore
483