CSP: Fix parsing of 'host/path' source expressions
[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 "ContentSecurityPolicyDirectiveList.h"
32 #include "ParsingUtilities.h"
33 #include "SecurityOrigin.h"
34 #include "URL.h"
35 #include <wtf/ASCIICType.h>
36
37 namespace WebCore {
38
39 static bool isSourceCharacter(UChar c)
40 {
41     return !isASCIISpace(c);
42 }
43
44 static bool isHostCharacter(UChar c)
45 {
46     return isASCIIAlphanumeric(c) || c == '-';
47 }
48
49 static bool isPathComponentCharacter(UChar c)
50 {
51     return c != '?' && c != '#';
52 }
53
54 static bool isSchemeContinuationCharacter(UChar c)
55 {
56     return isASCIIAlphanumeric(c) || c == '+' || c == '-' || c == '.';
57 }
58
59 static bool isNotColonOrSlash(UChar c)
60 {
61     return c != ':' && c != '/';
62 }
63
64 static bool isSourceListNone(const String& value)
65 {
66     auto characters = StringView(value).upconvertedCharacters();
67     const UChar* begin = characters;
68     const UChar* end = characters + value.length();
69     skipWhile<UChar, isASCIISpace>(begin, end);
70
71     const UChar* position = begin;
72     skipWhile<UChar, isSourceCharacter>(position, end);
73     if (!equalLettersIgnoringASCIICase(begin, position - begin, "'none'"))
74         return false;
75
76     skipWhile<UChar, isASCIISpace>(position, end);
77     if (position != end)
78         return false;
79     
80     return true;
81 }
82
83 ContentSecurityPolicySourceList::ContentSecurityPolicySourceList(const ContentSecurityPolicy& policy, const String& directiveName)
84     : m_policy(policy)
85     , m_directiveName(directiveName)
86 {
87 }
88
89 void ContentSecurityPolicySourceList::parse(const String& value)
90 {
91     // We represent 'none' as an empty m_list.
92     if (isSourceListNone(value))
93         return;
94     auto characters = StringView(value).upconvertedCharacters();
95     parse(characters, characters + value.length());
96 }
97
98 bool ContentSecurityPolicySourceList::matches(const URL& url)
99 {
100     if (m_allowStar) {
101         // FIXME: Should only match for URLs whose scheme is not blob, data or filesystem.
102         // See <https://bugs.webkit.org/show_bug.cgi?id=154122> for more details.
103         return true;
104     }
105
106     if (m_allowSelf && m_policy.urlMatchesSelf(url))
107         return true;
108
109     for (auto& entry : m_list) {
110         if (entry.matches(url))
111             return true;
112     }
113
114     return false;
115 }
116
117 // source-list       = *WSP [ source *( 1*WSP source ) *WSP ]
118 //                   / *WSP "'none'" *WSP
119 //
120 void ContentSecurityPolicySourceList::parse(const UChar* begin, const UChar* end)
121 {
122     const UChar* position = begin;
123
124     while (position < end) {
125         skipWhile<UChar, isASCIISpace>(position, end);
126         if (position == end)
127             return;
128
129         const UChar* beginSource = position;
130         skipWhile<UChar, isSourceCharacter>(position, end);
131
132         String scheme, host, path;
133         int port = 0;
134         bool hostHasWildcard = false;
135         bool portHasWildcard = false;
136
137         if (parseSource(beginSource, position, scheme, host, port, path, hostHasWildcard, portHasWildcard)) {
138             // Wildcard hosts and keyword sources ('self', 'unsafe-inline',
139             // etc.) aren't stored in m_list, but as attributes on the source
140             // list itself.
141             if (scheme.isEmpty() && host.isEmpty())
142                 continue;
143             if (isCSPDirectiveName(host))
144                 m_policy.reportDirectiveAsSourceExpression(m_directiveName, host);
145             m_list.append(ContentSecurityPolicySource(m_policy, scheme, host, port, path, hostHasWildcard, portHasWildcard));
146         } else
147             m_policy.reportInvalidSourceExpression(m_directiveName, String(beginSource, position - beginSource));
148
149         ASSERT(position == end || isASCIISpace(*position));
150     }
151 }
152
153 // source            = scheme ":"
154 //                   / ( [ scheme "://" ] host [ port ] [ path ] )
155 //                   / "'self'"
156 //
157 bool ContentSecurityPolicySourceList::parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, int& port, String& path, bool& hostHasWildcard, bool& portHasWildcard)
158 {
159     if (begin == end)
160         return false;
161
162     if (equalLettersIgnoringASCIICase(begin, end - begin, "'none'"))
163         return false;
164
165     if (end - begin == 1 && *begin == '*') {
166         m_allowStar = true;
167         return true;
168     }
169
170     if (equalLettersIgnoringASCIICase(begin, end - begin, "'self'")) {
171         m_allowSelf = true;
172         return true;
173     }
174
175     if (equalLettersIgnoringASCIICase(begin, end - begin, "'unsafe-inline'")) {
176         m_allowInline = true;
177         return true;
178     }
179
180     if (equalLettersIgnoringASCIICase(begin, end - begin, "'unsafe-eval'")) {
181         m_allowEval = true;
182         return true;
183     }
184
185     const UChar* position = begin;
186     const UChar* beginHost = begin;
187     const UChar* beginPath = end;
188     const UChar* beginPort = nullptr;
189
190     skipWhile<UChar, isNotColonOrSlash>(position, end);
191
192     if (position == end) {
193         // host
194         //     ^
195         return parseHost(beginHost, position, host, hostHasWildcard);
196     }
197
198     if (position < end && *position == '/') {
199         // host/path || host/ || /
200         //     ^            ^    ^
201         return parseHost(beginHost, position, host, hostHasWildcard) && parsePath(position, end, path);
202     }
203
204     if (position < end && *position == ':') {
205         if (end - position == 1) {
206             // scheme:
207             //       ^
208             return parseScheme(begin, position, scheme);
209         }
210
211         if (position[1] == '/') {
212             // scheme://host || scheme://
213             //       ^                ^
214             if (!parseScheme(begin, position, scheme)
215                 || !skipExactly<UChar>(position, end, ':')
216                 || !skipExactly<UChar>(position, end, '/')
217                 || !skipExactly<UChar>(position, end, '/'))
218                 return false;
219             if (position == end)
220                 return false;
221             beginHost = position;
222             skipWhile<UChar, isNotColonOrSlash>(position, end);
223         }
224
225         if (position < end && *position == ':') {
226             // host:port || scheme://host:port
227             //     ^                     ^
228             beginPort = position;
229             skipUntil<UChar>(position, end, '/');
230         }
231     }
232
233     if (position < end && *position == '/') {
234         // scheme://host/path || scheme://host:port/path
235         //              ^                          ^
236         if (position == beginHost)
237             return false;
238
239         beginPath = position;
240     }
241
242     if (!parseHost(beginHost, beginPort ? beginPort : beginPath, host, hostHasWildcard))
243         return false;
244
245     if (!beginPort)
246         port = 0;
247     else {
248         if (!parsePort(beginPort, beginPath, port, portHasWildcard))
249             return false;
250     }
251
252     if (beginPath != end) {
253         if (!parsePath(beginPath, end, path))
254             return false;
255     }
256
257     return true;
258 }
259
260 //                     ; <scheme> production from RFC 3986
261 // scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
262 //
263 bool ContentSecurityPolicySourceList::parseScheme(const UChar* begin, const UChar* end, String& scheme)
264 {
265     ASSERT(begin <= end);
266     ASSERT(scheme.isEmpty());
267
268     if (begin == end)
269         return false;
270
271     const UChar* position = begin;
272
273     if (!skipExactly<UChar, isASCIIAlpha>(position, end))
274         return false;
275
276     skipWhile<UChar, isSchemeContinuationCharacter>(position, end);
277
278     if (position != end)
279         return false;
280
281     scheme = String(begin, end - begin);
282     return true;
283 }
284
285 // host              = [ "*." ] 1*host-char *( "." 1*host-char )
286 //                   / "*"
287 // host-char         = ALPHA / DIGIT / "-"
288 //
289 bool ContentSecurityPolicySourceList::parseHost(const UChar* begin, const UChar* end, String& host, bool& hostHasWildcard)
290 {
291     ASSERT(begin <= end);
292     ASSERT(host.isEmpty());
293     ASSERT(!hostHasWildcard);
294
295     if (begin == end)
296         return false;
297
298     const UChar* position = begin;
299
300     if (skipExactly<UChar>(position, end, '*')) {
301         hostHasWildcard = true;
302
303         if (position == end)
304             return true;
305
306         if (!skipExactly<UChar>(position, end, '.'))
307             return false;
308     }
309
310     const UChar* hostBegin = position;
311
312     while (position < end) {
313         if (!skipExactly<UChar, isHostCharacter>(position, end))
314             return false;
315
316         skipWhile<UChar, isHostCharacter>(position, end);
317
318         if (position < end && !skipExactly<UChar>(position, end, '.'))
319             return false;
320     }
321
322     ASSERT(position == end);
323     host = String(hostBegin, end - hostBegin);
324     return true;
325 }
326
327 bool ContentSecurityPolicySourceList::parsePath(const UChar* begin, const UChar* end, String& path)
328 {
329     ASSERT(begin <= end);
330     ASSERT(path.isEmpty());
331     
332     const UChar* position = begin;
333     skipWhile<UChar, isPathComponentCharacter>(position, end);
334     // path/to/file.js?query=string || path/to/file.js#anchor
335     //                ^                               ^
336     if (position < end)
337         m_policy.reportInvalidPathCharacter(m_directiveName, String(begin, end - begin), *position);
338     
339     path = decodeURLEscapeSequences(String(begin, position - begin));
340     
341     ASSERT(position <= end);
342     ASSERT(position == end || (*position == '#' || *position == '?'));
343     return true;
344 }
345
346 // port              = ":" ( 1*DIGIT / "*" )
347 //
348 bool ContentSecurityPolicySourceList::parsePort(const UChar* begin, const UChar* end, int& port, bool& portHasWildcard)
349 {
350     ASSERT(begin <= end);
351     ASSERT(!port);
352     ASSERT(!portHasWildcard);
353     
354     if (!skipExactly<UChar>(begin, end, ':'))
355         ASSERT_NOT_REACHED();
356     
357     if (begin == end)
358         return false;
359     
360     if (end - begin == 1 && *begin == '*') {
361         port = 0;
362         portHasWildcard = true;
363         return true;
364     }
365     
366     const UChar* position = begin;
367     skipWhile<UChar, isASCIIDigit>(position, end);
368     
369     if (position != end)
370         return false;
371     
372     bool ok;
373     port = charactersToIntStrict(begin, end - begin, &ok);
374     return ok;
375 }
376
377 } // namespace WebCore