/* * Copyright (C) 2011 Google, Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "ContentSecurityPolicy.h" #include "Document.h" #include "NotImplemented.h" #include "SecurityOrigin.h" namespace WebCore { // Normally WebKit uses "static" for internal linkage, but using "static" for // these functions causes a compile error because these functions are used as // template parameters. namespace { bool isDirectiveNameCharacter(UChar c) { return isASCIIAlphanumeric(c) || c == '-'; } bool isDirectiveValueCharacter(UChar c) { return isASCIISpace(c) || (c >= 0x21 && c <= 0x7e); // Whitespace + VCHAR } bool isSourceCharacter(UChar c) { return !isASCIISpace(c); } bool isHostCharacter(UChar c) { return isASCIIAlphanumeric(c) || c == '-'; } bool isOptionValueCharacter(UChar c) { return isASCIIAlphanumeric(c) || c == '-'; } bool isSchemeContinuationCharacter(UChar c) { return isASCIIAlphanumeric(c) || c == '+' || c == '-' || c == '.'; } } // namespace static bool skipExactly(const UChar*& position, const UChar* end, UChar delimiter) { if (position < end && *position == delimiter) { ++position; return true; } return false; } template static bool skipExactly(const UChar*& position, const UChar* end) { if (position < end && characterPredicate(*position)) { ++position; return true; } return false; } static void skipUtil(const UChar*& position, const UChar* end, UChar delimiter) { while (position < end && *position != delimiter) ++position; } template static void skipWhile(const UChar*& position, const UChar* end) { while (position < end && characterPredicate(*position)) ++position; } class CSPSource { public: CSPSource(const String& scheme, const String& host, int port, bool hostHasWildcard, bool portHasWildcard) : m_scheme(scheme) , m_host(host) , m_port(port) , m_hostHasWildcard(hostHasWildcard) , m_portHasWildcard(portHasWildcard) { } bool matches(const KURL& url) const { if (!schemeMatches(url)) return false; if (isSchemeOnly()) return true; return hostMatches(url) && portMatches(url); } private: bool schemeMatches(const KURL& url) const { return equalIgnoringCase(url.protocol(), m_scheme); } bool hostMatches(const KURL& url) const { if (m_hostHasWildcard) notImplemented(); return equalIgnoringCase(url.host(), m_host); } bool portMatches(const KURL& url) const { if (m_portHasWildcard) return true; // FIXME: Handle explicit default ports correctly. return url.port() == m_port; } bool isSchemeOnly() const { return m_host.isEmpty(); } String m_scheme; String m_host; int m_port; bool m_hostHasWildcard; bool m_portHasWildcard; }; class CSPSourceList { public: explicit CSPSourceList(SecurityOrigin*); void parse(const String&); bool matches(const KURL&); private: void parse(const UChar* begin, const UChar* end); bool parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, int& port, bool& hostHasWildcard, bool& portHasWildcard); bool parseScheme(const UChar* begin, const UChar* end, String& scheme); bool parseHost(const UChar* begin, const UChar* end, String& host, bool& hostHasWildcard); bool parsePort(const UChar* begin, const UChar* end, int& port, bool& portHasWildcard); void addSourceSelf(); SecurityOrigin* m_origin; Vector m_list; }; CSPSourceList::CSPSourceList(SecurityOrigin* origin) : m_origin(origin) { } void CSPSourceList::parse(const String& value) { parse(value.characters(), value.characters() + value.length()); } bool CSPSourceList::matches(const KURL& url) { for (size_t i = 0; i < m_list.size(); ++i) { if (m_list[i].matches(url)) return true; } return false; } // source-list = *WSP [ source *( 1*WSP source ) *WSP ] // / *WSP "'none'" *WSP // void CSPSourceList::parse(const UChar* begin, const UChar* end) { const UChar* position = begin; bool isFirstSourceInList = true; while (position < end) { skipWhile(position, end); const UChar* beginSource = position; skipWhile(position, end); if (isFirstSourceInList && equalIgnoringCase("'none'", beginSource, position - beginSource)) return; // We represent 'none' as an empty m_list. isFirstSourceInList = false; String scheme, host; int port = 0; bool hostHasWildcard = false; bool portHasWildcard = false; if (parseSource(beginSource, position, scheme, host, port, hostHasWildcard, portHasWildcard)) { if (scheme.isEmpty()) scheme = m_origin->protocol(); m_list.append(CSPSource(scheme, host, port, hostHasWildcard, portHasWildcard)); } ASSERT(position == end || isASCIISpace(*position)); } } // source = scheme ":" // / ( [ scheme "://" ] host [ port ] ) // / "'self'" // bool CSPSourceList::parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, int& port, bool& hostHasWildcard, bool& portHasWildcard) { if (begin == end) return false; if (equalIgnoringCase("'self'", begin, end - begin)) { addSourceSelf(); return false; } const UChar* position = begin; const UChar* beginHost = begin; skipUtil(position, end, ':'); if (position == end) { // This must be a host-only source. if (!parseHost(beginHost, position, host, hostHasWildcard)) return false; return true; } if (end - position == 1) { ASSERT(*position == ':'); // This must be a scheme-only source. if (!parseScheme(begin, position, scheme)) return false; return true; } ASSERT(end - position >= 2); if (position[1] == '/') { if (!parseScheme(begin, position, scheme) || !skipExactly(position, end, ':') || !skipExactly(position, end, '/') || !skipExactly(position, end, '/')) return false; beginHost = position; skipUtil(position, end, ':'); } if (position == beginHost) return false; if (!parseHost(beginHost, position, host, hostHasWildcard)) return false; if (position == end) { port = 0; return true; } if (!skipExactly(position, end, ':')) ASSERT_NOT_REACHED(); if (!parsePort(position, end, port, portHasWildcard)) return false; return true; } // ; production from RFC 3986 // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) // bool CSPSourceList::parseScheme(const UChar* begin, const UChar* end, String& scheme) { ASSERT(begin <= end); ASSERT(scheme.isEmpty()); if (begin == end) return false; const UChar* position = begin; if (!skipExactly(position, end)) return false; skipWhile(position, end); if (position != end) return false; scheme = String(begin, end - begin); return true; } // host = [ "*." ] 1*host-char *( "." 1*host-char ) // / "*" // host-char = ALPHA / DIGIT / "-" // bool CSPSourceList::parseHost(const UChar* begin, const UChar* end, String& host, bool& hostHasWildcard) { ASSERT(begin <= end); ASSERT(host.isEmpty()); ASSERT(!hostHasWildcard); if (begin == end) return false; const UChar* position = begin; if (skipExactly(position, end, '*')) { hostHasWildcard = true; if (position == end) return true; if (!skipExactly(position, end, '.')) return false; } const UChar* hostBegin = position; while (position < end) { if (!skipExactly(position, end)) return false; skipWhile(position, end); if (position < end && !skipExactly(position, end, '.')) return false; } ASSERT(position == end); host = String(hostBegin, end - hostBegin); return true; } // port = ":" ( 1*DIGIT / "*" ) // bool CSPSourceList::parsePort(const UChar* begin, const UChar* end, int& port, bool& portHasWildcard) { ASSERT(begin <= end); ASSERT(!port); ASSERT(!portHasWildcard); if (begin == end) return false; if (end - begin == 1 && *begin == '*') { port = 0; portHasWildcard = true; return true; } const UChar* position = begin; skipWhile(position, end); if (position != end) return false; bool ok; port = charactersToIntStrict(begin, end - begin, &ok); return ok; } void CSPSourceList::addSourceSelf() { m_list.append(CSPSource(m_origin->protocol(), m_origin->host(), m_origin->port(), false, false)); } class CSPDirective { public: CSPDirective(const String& value, SecurityOrigin* origin) : m_sourceList(origin) { m_sourceList.parse(value); } bool allows(const KURL& url) { return m_sourceList.matches(url); } private: CSPSourceList m_sourceList; }; class CSPOptions { public: explicit CSPOptions(const String& value) : m_disableXSSProtection(false) , m_evalScript(false) { parse(value); } bool disableXSSProtection() const { return m_disableXSSProtection; } bool evalScript() const { return m_evalScript; } private: void parse(const String&); bool m_disableXSSProtection; bool m_evalScript; }; // options = "options" *( 1*WSP option-value ) *WSP // option-value = 1*( ALPHA / DIGIT / "-" ) // void CSPOptions::parse(const String& value) { DEFINE_STATIC_LOCAL(String, disableXSSProtection, ("disable-xss-protection")); DEFINE_STATIC_LOCAL(String, evalScript, ("eval-script")); const UChar* position = value.characters(); const UChar* end = position + value.length(); while (position < end) { skipWhile(position, end); const UChar* optionsValueBegin = position; if (!skipExactly(position, end)) return; skipWhile(position, end); String optionsValue(optionsValueBegin, position - optionsValueBegin); if (equalIgnoringCase(optionsValue, disableXSSProtection)) m_disableXSSProtection = true; else if (equalIgnoringCase(optionsValue, evalScript)) m_evalScript = true; } } ContentSecurityPolicy::ContentSecurityPolicy(SecurityOrigin* origin) : m_havePolicy(false) , m_origin(origin) { } ContentSecurityPolicy::~ContentSecurityPolicy() { } void ContentSecurityPolicy::didReceiveHeader(const String& header) { if (m_havePolicy) return; // The first policy wins. parse(header); m_havePolicy = true; } bool ContentSecurityPolicy::protectAgainstXSS() const { return m_scriptSrc && (!m_options || !m_options->disableXSSProtection()); } bool ContentSecurityPolicy::allowJavaScriptURLs() const { return !protectAgainstXSS(); } bool ContentSecurityPolicy::allowInlineEventHandlers() const { return !protectAgainstXSS(); } bool ContentSecurityPolicy::allowInlineScript() const { return !protectAgainstXSS(); } bool ContentSecurityPolicy::allowEval() const { return !m_scriptSrc || (m_options && m_options->evalScript()); } bool ContentSecurityPolicy::allowScriptFromSource(const KURL& url) const { return !m_scriptSrc || m_scriptSrc->allows(url); } bool ContentSecurityPolicy::allowObjectFromSource(const KURL& url) const { return !m_objectSrc || m_objectSrc->allows(url); } bool ContentSecurityPolicy::allowImageFromSource(const KURL& url) const { return !m_imgSrc || m_imgSrc->allows(url); } bool ContentSecurityPolicy::allowStyleFromSource(const KURL& url) const { return !m_styleSrc || m_styleSrc->allows(url); } bool ContentSecurityPolicy::allowFontFromSource(const KURL& url) const { return !m_fontSrc || m_fontSrc->allows(url); } bool ContentSecurityPolicy::allowMediaFromSource(const KURL& url) const { return !m_mediaSrc || m_mediaSrc->allows(url); } // policy = directive-list // directive-list = [ directive *( ";" [ directive ] ) ] // void ContentSecurityPolicy::parse(const String& policy) { ASSERT(!m_havePolicy); if (policy.isEmpty()) return; const UChar* position = policy.characters(); const UChar* end = position + policy.length(); while (position < end) { const UChar* directiveBegin = position; skipUtil(position, end, ';'); String name, value; if (parseDirective(directiveBegin, position, name, value)) { ASSERT(!name.isEmpty()); addDirective(name, value); } ASSERT(position == end || *position == ';'); skipExactly(position, end, ';'); } } // directive = *WSP [ directive-name [ WSP directive-value ] ] // directive-name = 1*( ALPHA / DIGIT / "-" ) // directive-value = *( WSP / ) // bool ContentSecurityPolicy::parseDirective(const UChar* begin, const UChar* end, String& name, String& value) { ASSERT(name.isEmpty()); ASSERT(value.isEmpty()); const UChar* position = begin; skipWhile(position, end); const UChar* nameBegin = position; skipWhile(position, end); // The directive-name must be non-empty. if (nameBegin == position) return false; name = String(nameBegin, position - nameBegin); if (position == end) return true; if (!skipExactly(position, end)) return false; skipWhile(position, end); const UChar* valueBegin = position; skipWhile(position, end); if (position != end) return false; // The directive-value may be empty. if (valueBegin == position) return true; value = String(valueBegin, position - valueBegin); return true; } void ContentSecurityPolicy::addDirective(const String& name, const String& value) { DEFINE_STATIC_LOCAL(String, scriptSrc, ("script-src")); DEFINE_STATIC_LOCAL(String, objectSrc, ("object-src")); DEFINE_STATIC_LOCAL(String, imgSrc, ("img-src")); DEFINE_STATIC_LOCAL(String, styleSrc, ("style-src")); DEFINE_STATIC_LOCAL(String, fontSrc, ("font-src")); DEFINE_STATIC_LOCAL(String, mediaSrc, ("media-src")); DEFINE_STATIC_LOCAL(String, options, ("options")); ASSERT(!name.isEmpty()); if (!m_scriptSrc && equalIgnoringCase(name, scriptSrc)) m_scriptSrc = adoptPtr(new CSPDirective(value, m_origin.get())); else if (!m_objectSrc && equalIgnoringCase(name, objectSrc)) m_objectSrc = adoptPtr(new CSPDirective(value, m_origin.get())); else if (!m_imgSrc && equalIgnoringCase(name, imgSrc)) m_imgSrc = adoptPtr(new CSPDirective(value, m_origin.get())); else if (!m_styleSrc && equalIgnoringCase(name, styleSrc)) m_styleSrc = adoptPtr(new CSPDirective(value, m_origin.get())); else if (!m_fontSrc && equalIgnoringCase(name, fontSrc)) m_fontSrc = adoptPtr(new CSPDirective(value, m_origin.get())); else if (!m_mediaSrc && equalIgnoringCase(name, mediaSrc)) m_mediaSrc = adoptPtr(new CSPDirective(value, m_origin.get())); else if (!m_options && equalIgnoringCase(name, options)) m_options = adoptPtr(new CSPOptions(value)); } }