Throw the right exception upon memory exhaustion in Array::slice
[WebKit-https.git] / Source / WebCore / page / csp / ContentSecurityPolicySourceList.cpp
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
39 namespace WebCore {
40
41 static 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
59 static bool isSourceCharacter(UChar c)
60 {
61     return !isASCIISpace(c);
62 }
63
64 static bool isHostCharacter(UChar c)
65 {
66     return isASCIIAlphanumeric(c) || c == '-';
67 }
68
69 static bool isPathComponentCharacter(UChar c)
70 {
71     return c != '?' && c != '#';
72 }
73
74 static bool isSchemeContinuationCharacter(UChar c)
75 {
76     return isASCIIAlphanumeric(c) || c == '+' || c == '-' || c == '.';
77 }
78
79 static bool isNotColonOrSlash(UChar c)
80 {
81     return c != ':' && c != '/';
82 }
83
84 static 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
103 ContentSecurityPolicySourceList::ContentSecurityPolicySourceList(const ContentSecurityPolicy& policy, const String& directiveName)
104     : m_policy(policy)
105     , m_directiveName(directiveName)
106 {
107 }
108
109 void 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
119 bool 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
134 bool 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
150 bool ContentSecurityPolicySourceList::matches(const ContentSecurityPolicyHash& hash) const
151 {
152     return m_hashes.contains(hash);
153 }
154
155 bool 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 //
163 void 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 //
208 bool 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 //
314 bool 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 //
340 bool 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
378 bool 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 //
399 bool 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).
434 static bool isNonceCharacter(UChar c)
435 {
436     return isBase64OrBase64URLCharacter(c) || c == '=';
437 }
438
439 // nonce-source    = "'nonce-" nonce-value "'"
440 // nonce-value     = base64-value
441 bool 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( "=" )
458 bool 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