1/*
2 * Copyright (C) 2011 Google, Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "ContentSecurityPolicy.h"
28
29#include "Document.h"
30#include "NotImplemented.h"
31#include "SecurityOrigin.h"
32
33namespace WebCore {
34
35// Normally WebKit uses "static" for internal linkage, but using "static" for
36// these functions causes a compile error because these functions are used as
37// template parameters.
38namespace {
39
40bool isDirectiveNameCharacter(UChar c)
41{
42    return isASCIIAlphanumeric(c) || c == '-';
43}
44
45bool isDirectiveValueCharacter(UChar c)
46{
47    return isASCIISpace(c) || (c >= 0x21 && c <= 0x7e); // Whitespace + VCHAR
48}
49
50bool isSourceCharacter(UChar c)
51{
52    return !isASCIISpace(c);
53}
54
55bool isHostCharacter(UChar c)
56{
57    return isASCIIAlphanumeric(c) || c == '-';
58}
59
60bool isOptionValueCharacter(UChar c)
61{
62    return isASCIIAlphanumeric(c) || c == '-';
63}
64
65bool isSchemeContinuationCharacter(UChar c)
66{
67    return isASCIIAlphanumeric(c) || c == '+' || c == '-' || c == '.';
68}
69
70} // namespace
71
72static bool skipExactly(const UChar*& position, const UChar* end, UChar delimiter)
73{
74    if (position < end && *position == delimiter) {
75        ++position;
76        return true;
77    }
78    return false;
79}
80
81template<bool characterPredicate(UChar)>
82static bool skipExactly(const UChar*& position, const UChar* end)
83{
84    if (position < end && characterPredicate(*position)) {
85        ++position;
86        return true;
87    }
88    return false;
89}
90
91static void skipUtil(const UChar*& position, const UChar* end, UChar delimiter)
92{
93    while (position < end && *position != delimiter)
94        ++position;
95}
96
97template<bool characterPredicate(UChar)>
98static void skipWhile(const UChar*& position, const UChar* end)
99{
100    while (position < end && characterPredicate(*position))
101        ++position;
102}
103
104class CSPSource {
105public:
106    CSPSource(const String& scheme, const String& host, int port, bool hostHasWildcard, bool portHasWildcard)
107        : m_scheme(scheme)
108        , m_host(host)
109        , m_port(port)
110        , m_hostHasWildcard(hostHasWildcard)
111        , m_portHasWildcard(portHasWildcard)
112    {
113    }
114
115    bool matches(const KURL& url) const
116    {
117        if (!schemeMatches(url))
118            return false;
119        if (isSchemeOnly())
120            return true;
121        return hostMatches(url) && portMatches(url);
122    }
123
124private:
125    bool schemeMatches(const KURL& url) const
126    {
127        return equalIgnoringCase(url.protocol(), m_scheme);
128    }
129
130    bool hostMatches(const KURL& url) const
131    {
132        if (m_hostHasWildcard)
133            notImplemented();
134
135        return equalIgnoringCase(url.host(), m_host);
136    }
137
138    bool portMatches(const KURL& url) const
139    {
140        if (m_portHasWildcard)
141            return true;
142
143        // FIXME: Handle explicit default ports correctly.
144        return url.port() == m_port;
145    }
146
147    bool isSchemeOnly() const { return m_host.isEmpty(); }
148
149    String m_scheme;
150    String m_host;
151    int m_port;
152
153    bool m_hostHasWildcard;
154    bool m_portHasWildcard;
155};
156
157class CSPSourceList {
158public:
159    explicit CSPSourceList(SecurityOrigin*);
160
161    void parse(const String&);
162    bool matches(const KURL&);
163
164private:
165    void parse(const UChar* begin, const UChar* end);
166
167    bool parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, int& port, bool& hostHasWildcard, bool& portHasWildcard);
168    bool parseScheme(const UChar* begin, const UChar* end, String& scheme);
169    bool parseHost(const UChar* begin, const UChar* end, String& host, bool& hostHasWildcard);
170    bool parsePort(const UChar* begin, const UChar* end, int& port, bool& portHasWildcard);
171
172    void addSourceSelf();
173
174    SecurityOrigin* m_origin;
175    Vector<CSPSource> m_list;
176};
177
178CSPSourceList::CSPSourceList(SecurityOrigin* origin)
179    : m_origin(origin)
180{
181}
182
183void CSPSourceList::parse(const String& value)
184{
185    parse(value.characters(), value.characters() + value.length());
186}
187
188bool CSPSourceList::matches(const KURL& url)
189{
190    for (size_t i = 0; i < m_list.size(); ++i) {
191        if (m_list[i].matches(url))
192            return true;
193    }
194    return false;
195}
196
197// source-list       = *WSP [ source *( 1*WSP source ) *WSP ]
198//                   / *WSP "'none'" *WSP
199//
200void CSPSourceList::parse(const UChar* begin, const UChar* end)
201{
202    const UChar* position = begin;
203
204    bool isFirstSourceInList = true;
205    while (position < end) {
206        skipWhile<isASCIISpace>(position, end);
207        const UChar* beginSource = position;
208        skipWhile<isSourceCharacter>(position, end);
209
210        if (isFirstSourceInList && equalIgnoringCase("'none'", beginSource, position - beginSource))
211            return; // We represent 'none' as an empty m_list.
212        isFirstSourceInList = false;
213
214        String scheme, host;
215        int port = 0;
216        bool hostHasWildcard = false;
217        bool portHasWildcard = false;
218
219        if (parseSource(beginSource, position, scheme, host, port, hostHasWildcard, portHasWildcard)) {
220            if (scheme.isEmpty())
221                scheme = m_origin->protocol();
222            m_list.append(CSPSource(scheme, host, port, hostHasWildcard, portHasWildcard));
223        }
224
225        ASSERT(position == end || isASCIISpace(*position));
226     }
227}
228
229// source            = scheme ":"
230//                   / ( [ scheme "://" ] host [ port ] )
231//                   / "'self'"
232//
233bool CSPSourceList::parseSource(const UChar* begin, const UChar* end,
234                                String& scheme, String& host, int& port,
235                                bool& hostHasWildcard, bool& portHasWildcard)
236{
237    if (begin == end)
238        return false;
239
240    if (equalIgnoringCase("'self'", begin, end - begin)) {
241        addSourceSelf();
242        return false;
243    }
244
245    const UChar* position = begin;
246
247    const UChar* beginHost = begin;
248    skipUtil(position, end, ':');
249
250    if (position == end) {
251        // This must be a host-only source.
252        if (!parseHost(beginHost, position, host, hostHasWildcard))
253            return false;
254        return true;
255    }
256
257    if (end - position == 1) {
258        ASSERT(*position == ':');
259        // This must be a scheme-only source.
260        if (!parseScheme(begin, position, scheme))
261            return false;
262        return true;
263    }
264
265    ASSERT(end - position >= 2);
266    if (position[1] == '/') {
267        if (!parseScheme(begin, position, scheme)
268            || !skipExactly(position, end, ':')
269            || !skipExactly(position, end, '/')
270            || !skipExactly(position, end, '/'))
271            return false;
272        beginHost = position;
273        skipUtil(position, end, ':');
274    }
275
276    if (position == beginHost)
277        return false;
278
279    if (!parseHost(beginHost, position, host, hostHasWildcard))
280        return false;
281
282    if (position == end) {
283        port = 0;
284        return true;
285    }
286
287    if (!skipExactly(position, end, ':'))
288        ASSERT_NOT_REACHED();
289
290    if (!parsePort(position, end, port, portHasWildcard))
291        return false;
292
293    return true;
294}
295
296//                     ; <scheme> production from RFC 3986
297// scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
298//
299bool CSPSourceList::parseScheme(const UChar* begin, const UChar* end, String& scheme)
300{
301    ASSERT(begin <= end);
302    ASSERT(scheme.isEmpty());
303
304    if (begin == end)
305        return false;
306
307    const UChar* position = begin;
308
309    if (!skipExactly<isASCIIAlpha>(position, end))
310        return false;
311
312    skipWhile<isSchemeContinuationCharacter>(position, end);
313
314    if (position != end)
315        return false;
316
317    scheme = String(begin, end - begin);
318    return true;
319}
320
321// host              = [ "*." ] 1*host-char *( "." 1*host-char )
322//                   / "*"
323// host-char         = ALPHA / DIGIT / "-"
324//
325bool CSPSourceList::parseHost(const UChar* begin, const UChar* end, String& host, bool& hostHasWildcard)
326{
327    ASSERT(begin <= end);
328    ASSERT(host.isEmpty());
329    ASSERT(!hostHasWildcard);
330
331    if (begin == end)
332        return false;
333
334    const UChar* position = begin;
335
336    if (skipExactly(position, end, '*')) {
337        hostHasWildcard = true;
338
339        if (position == end)
340            return true;
341
342        if (!skipExactly(position, end, '.'))
343            return false;
344    }
345
346    const UChar* hostBegin = position;
347
348    while (position < end) {
349        if (!skipExactly<isHostCharacter>(position, end))
350            return false;
351
352        skipWhile<isHostCharacter>(position, end);
353
354        if (position < end && !skipExactly(position, end, '.'))
355            return false;
356    }
357
358    ASSERT(position == end);
359    host = String(hostBegin, end - hostBegin);
360    return true;
361}
362
363// port              = ":" ( 1*DIGIT / "*" )
364//
365bool CSPSourceList::parsePort(const UChar* begin, const UChar* end, int& port, bool& portHasWildcard)
366{
367    ASSERT(begin <= end);
368    ASSERT(!port);
369    ASSERT(!portHasWildcard);
370
371    if (begin == end)
372        return false;
373
374    if (end - begin == 1 && *begin == '*') {
375        port = 0;
376        portHasWildcard = true;
377        return true;
378    }
379
380    const UChar* position = begin;
381    skipWhile<isASCIIDigit>(position, end);
382
383    if (position != end)
384        return false;
385
386    bool ok;
387    port = charactersToIntStrict(begin, end - begin, &ok);
388    return ok;
389}
390
391void CSPSourceList::addSourceSelf()
392{
393    m_list.append(CSPSource(m_origin->protocol(), m_origin->host(), m_origin->port(), false, false));
394}
395
396class CSPDirective {
397public:
398    CSPDirective(const String& value, SecurityOrigin* origin)
399        : m_sourceList(origin)
400    {
401        m_sourceList.parse(value);
402    }
403
404    bool allows(const KURL& url)
405    {
406        return m_sourceList.matches(url);
407    }
408
409private:
410    CSPSourceList m_sourceList;
411};
412
413class CSPOptions {
414public:
415    explicit CSPOptions(const String& value)
416        : m_disableXSSProtection(false)
417        , m_evalScript(false)
418    {
419        parse(value);
420    }
421
422    bool disableXSSProtection() const { return m_disableXSSProtection; }
423    bool evalScript() const { return m_evalScript; }
424
425private:
426    void parse(const String&);
427
428    bool m_disableXSSProtection;
429    bool m_evalScript;
430};
431
432// options           = "options" *( 1*WSP option-value ) *WSP
433// option-value      = 1*( ALPHA / DIGIT / "-" )
434//
435void CSPOptions::parse(const String& value)
436{
437    DEFINE_STATIC_LOCAL(String, disableXSSProtection, ("disable-xss-protection"));
438    DEFINE_STATIC_LOCAL(String, evalScript, ("eval-script"));
439
440    const UChar* position = value.characters();
441    const UChar* end = position + value.length();
442
443    while (position < end) {
444        skipWhile<isASCIISpace>(position, end);
445
446        const UChar* optionsValueBegin = position;
447
448        if (!skipExactly<isOptionValueCharacter>(position, end))
449            return;
450
451        skipWhile<isOptionValueCharacter>(position, end);
452
453        String optionsValue(optionsValueBegin, position - optionsValueBegin);
454
455        if (equalIgnoringCase(optionsValue, disableXSSProtection))
456            m_disableXSSProtection = true;
457        else if (equalIgnoringCase(optionsValue, evalScript))
458            m_evalScript = true;
459    }
460}
461
462ContentSecurityPolicy::ContentSecurityPolicy(SecurityOrigin* origin)
463    : m_havePolicy(false)
464    , m_origin(origin)
465{
466}
467
468ContentSecurityPolicy::~ContentSecurityPolicy()
469{
470}
471
472void ContentSecurityPolicy::didReceiveHeader(const String& header)
473{
474    if (m_havePolicy)
475        return; // The first policy wins.
476
477    parse(header);
478    m_havePolicy = true;
479}
480
481bool ContentSecurityPolicy::protectAgainstXSS() const
482{
483    return m_scriptSrc && (!m_options || !m_options->disableXSSProtection());
484}
485
486bool ContentSecurityPolicy::allowJavaScriptURLs() const
487{
488    return !protectAgainstXSS();
489}
490
491bool ContentSecurityPolicy::allowInlineEventHandlers() const
492{
493    return !protectAgainstXSS();
494}
495
496bool ContentSecurityPolicy::allowInlineScript() const
497{
498    return !protectAgainstXSS();
499}
500
501bool ContentSecurityPolicy::allowEval() const
502{
503    return !m_scriptSrc || (m_options && m_options->evalScript());
504}
505
506bool ContentSecurityPolicy::allowScriptFromSource(const KURL& url) const
507{
508    return !m_scriptSrc || m_scriptSrc->allows(url);
509}
510
511bool ContentSecurityPolicy::allowObjectFromSource(const KURL& url) const
512{
513    return !m_objectSrc || m_objectSrc->allows(url);
514}
515
516bool ContentSecurityPolicy::allowImageFromSource(const KURL& url) const
517{
518    return !m_imgSrc || m_imgSrc->allows(url);
519}
520
521bool ContentSecurityPolicy::allowStyleFromSource(const KURL& url) const
522{
523    return !m_styleSrc || m_styleSrc->allows(url);
524}
525
526bool ContentSecurityPolicy::allowFontFromSource(const KURL& url) const
527{
528    return !m_fontSrc || m_fontSrc->allows(url);
529}
530
531bool ContentSecurityPolicy::allowMediaFromSource(const KURL& url) const
532{
533    return !m_mediaSrc || m_mediaSrc->allows(url);
534}
535
536// policy            = directive-list
537// directive-list    = [ directive *( ";" [ directive ] ) ]
538//
539void ContentSecurityPolicy::parse(const String& policy)
540{
541    ASSERT(!m_havePolicy);
542
543    if (policy.isEmpty())
544        return;
545
546    const UChar* position = policy.characters();
547    const UChar* end = position + policy.length();
548
549    while (position < end) {
550        const UChar* directiveBegin = position;
551        skipUtil(position, end, ';');
552
553        String name, value;
554        if (parseDirective(directiveBegin, position, name, value)) {
555            ASSERT(!name.isEmpty());
556            addDirective(name, value);
557        }
558
559        ASSERT(position == end || *position == ';');
560        skipExactly(position, end, ';');
561    }
562}
563
564// directive         = *WSP [ directive-name [ WSP directive-value ] ]
565// directive-name    = 1*( ALPHA / DIGIT / "-" )
566// directive-value   = *( WSP / <VCHAR except ";"> )
567//
568bool ContentSecurityPolicy::parseDirective(const UChar* begin, const UChar* end, String& name, String& value)
569{
570    ASSERT(name.isEmpty());
571    ASSERT(value.isEmpty());
572
573    const UChar* position = begin;
574    skipWhile<isASCIISpace>(position, end);
575
576    const UChar* nameBegin = position;
577    skipWhile<isDirectiveNameCharacter>(position, end);
578
579    // The directive-name must be non-empty.
580    if (nameBegin == position)
581        return false;
582
583    name = String(nameBegin, position - nameBegin);
584
585    if (position == end)
586        return true;
587
588    if (!skipExactly<isASCIISpace>(position, end))
589        return false;
590
591    skipWhile<isASCIISpace>(position, end);
592
593    const UChar* valueBegin = position;
594    skipWhile<isDirectiveValueCharacter>(position, end);
595
596    if (position != end)
597        return false;
598
599    // The directive-value may be empty.
600    if (valueBegin == position)
601        return true;
602
603    value = String(valueBegin, position - valueBegin);
604    return true;
605}
606
607void ContentSecurityPolicy::addDirective(const String& name, const String& value)
608{
609    DEFINE_STATIC_LOCAL(String, scriptSrc, ("script-src"));
610    DEFINE_STATIC_LOCAL(String, objectSrc, ("object-src"));
611    DEFINE_STATIC_LOCAL(String, imgSrc, ("img-src"));
612    DEFINE_STATIC_LOCAL(String, styleSrc, ("style-src"));
613    DEFINE_STATIC_LOCAL(String, fontSrc, ("font-src"));
614    DEFINE_STATIC_LOCAL(String, mediaSrc, ("media-src"));
615    DEFINE_STATIC_LOCAL(String, options, ("options"));
616
617    ASSERT(!name.isEmpty());
618
619    if (!m_scriptSrc && equalIgnoringCase(name, scriptSrc))
620        m_scriptSrc = adoptPtr(new CSPDirective(value, m_origin.get()));
621    else if (!m_objectSrc && equalIgnoringCase(name, objectSrc))
622        m_objectSrc = adoptPtr(new CSPDirective(value, m_origin.get()));
623    else if (!m_imgSrc && equalIgnoringCase(name, imgSrc))
624        m_imgSrc = adoptPtr(new CSPDirective(value, m_origin.get()));
625    else if (!m_styleSrc && equalIgnoringCase(name, styleSrc))
626        m_styleSrc = adoptPtr(new CSPDirective(value, m_origin.get()));
627    else if (!m_fontSrc && equalIgnoringCase(name, fontSrc))
628        m_fontSrc = adoptPtr(new CSPDirective(value, m_origin.get()));
629    else if (!m_mediaSrc && equalIgnoringCase(name, mediaSrc))
630        m_mediaSrc = adoptPtr(new CSPDirective(value, m_origin.get()));
631    else if (!m_options && equalIgnoringCase(name, options))
632        m_options = adoptPtr(new CSPOptions(value));
633}
634
635}
636