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 |
483 | |