Home | History | Annotate | Download | only in parser
      1 /*
      2  * Copyright (C) 2011 Adam Barth. All Rights Reserved.
      3  * Copyright (C) 2011 Daniel Bates (dbates (at) intudata.com).
      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 APPLE 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 "core/html/parser/XSSAuditor.h"
     29 
     30 #include "core/HTMLNames.h"
     31 #include "core/SVGNames.h"
     32 #include "core/XLinkNames.h"
     33 #include "core/dom/Document.h"
     34 #include "core/frame/LocalFrame.h"
     35 #include "core/frame/csp/ContentSecurityPolicy.h"
     36 #include "core/html/HTMLParamElement.h"
     37 #include "core/html/parser/HTMLDocumentParser.h"
     38 #include "core/html/parser/HTMLParserIdioms.h"
     39 #include "core/html/parser/TextResourceDecoder.h"
     40 #include "core/html/parser/XSSAuditorDelegate.h"
     41 #include "core/loader/DocumentLoader.h"
     42 #include "core/frame/Settings.h"
     43 #include "platform/JSONValues.h"
     44 #include "platform/network/FormData.h"
     45 #include "platform/text/DecodeEscapeSequences.h"
     46 #include "wtf/ASCIICType.h"
     47 #include "wtf/MainThread.h"
     48 
     49 namespace {
     50 
     51 // SecurityOrigin::urlWithUniqueSecurityOrigin() can't be used cross-thread, or we'd use it instead.
     52 const char kURLWithUniqueOrigin[] = "data:,";
     53 
     54 } // namespace
     55 
     56 namespace WebCore {
     57 
     58 using namespace HTMLNames;
     59 
     60 static bool isNonCanonicalCharacter(UChar c)
     61 {
     62     // We remove all non-ASCII characters, including non-printable ASCII characters.
     63     //
     64     // Note, we don't remove backslashes like PHP stripslashes(), which among other things converts "\\0" to the \0 character.
     65     // Instead, we remove backslashes and zeros (since the string "\\0" =(remove backslashes)=> "0"). However, this has the
     66     // adverse effect that we remove any legitimate zeros from a string.
     67     // We also remove forward-slash, because it is common for some servers to collapse successive path components, eg,
     68     // a//b becomes a/b.
     69     //
     70     // For instance: new String("http://localhost:8000") => new String("http:localhost:8").
     71     return (c == '\\' || c == '0' || c == '\0' || c == '/' || c >= 127);
     72 }
     73 
     74 static bool isRequiredForInjection(UChar c)
     75 {
     76     return (c == '\'' || c == '"' || c == '<' || c == '>');
     77 }
     78 
     79 static bool isTerminatingCharacter(UChar c)
     80 {
     81     return (c == '&' || c == '/' || c == '"' || c == '\'' || c == '<' || c == '>' || c == ',');
     82 }
     83 
     84 static bool isHTMLQuote(UChar c)
     85 {
     86     return (c == '"' || c == '\'');
     87 }
     88 
     89 static bool isJSNewline(UChar c)
     90 {
     91     // Per ecma-262 section 7.3 Line Terminators.
     92     return (c == '\n' || c == '\r' || c == 0x2028 || c == 0x2029);
     93 }
     94 
     95 static bool startsHTMLCommentAt(const String& string, size_t start)
     96 {
     97     return (start + 3 < string.length() && string[start] == '<' && string[start + 1] == '!' && string[start + 2] == '-' && string[start + 3] == '-');
     98 }
     99 
    100 static bool startsSingleLineCommentAt(const String& string, size_t start)
    101 {
    102     return (start + 1 < string.length() && string[start] == '/' && string[start + 1] == '/');
    103 }
    104 
    105 static bool startsMultiLineCommentAt(const String& string, size_t start)
    106 {
    107     return (start + 1 < string.length() && string[start] == '/' && string[start + 1] == '*');
    108 }
    109 
    110 static bool startsOpeningScriptTagAt(const String& string, size_t start)
    111 {
    112     return start + 6 < string.length() && string[start] == '<'
    113         && WTF::toASCIILowerUnchecked(string[start + 1]) == 's'
    114         && WTF::toASCIILowerUnchecked(string[start + 2]) == 'c'
    115         && WTF::toASCIILowerUnchecked(string[start + 3]) == 'r'
    116         && WTF::toASCIILowerUnchecked(string[start + 4]) == 'i'
    117         && WTF::toASCIILowerUnchecked(string[start + 5]) == 'p'
    118         && WTF::toASCIILowerUnchecked(string[start + 6]) == 't';
    119 }
    120 
    121 // If other files need this, we should move this to core/html/parser/HTMLParserIdioms.h
    122 template<size_t inlineCapacity>
    123 bool threadSafeMatch(const Vector<UChar, inlineCapacity>& vector, const QualifiedName& qname)
    124 {
    125     return equalIgnoringNullity(vector, qname.localName().impl());
    126 }
    127 
    128 static bool hasName(const HTMLToken& token, const QualifiedName& name)
    129 {
    130     return threadSafeMatch(token.name(), name);
    131 }
    132 
    133 static bool findAttributeWithName(const HTMLToken& token, const QualifiedName& name, size_t& indexOfMatchingAttribute)
    134 {
    135     // Notice that we're careful not to ref the StringImpl here because we might be on a background thread.
    136     const String& attrName = name.namespaceURI() == XLinkNames::xlinkNamespaceURI ? "xlink:" + name.localName().string() : name.localName().string();
    137 
    138     for (size_t i = 0; i < token.attributes().size(); ++i) {
    139         if (equalIgnoringNullity(token.attributes().at(i).name, attrName)) {
    140             indexOfMatchingAttribute = i;
    141             return true;
    142         }
    143     }
    144     return false;
    145 }
    146 
    147 static bool isNameOfInlineEventHandler(const Vector<UChar, 32>& name)
    148 {
    149     const size_t lengthOfShortestInlineEventHandlerName = 5; // To wit: oncut.
    150     if (name.size() < lengthOfShortestInlineEventHandlerName)
    151         return false;
    152     return name[0] == 'o' && name[1] == 'n';
    153 }
    154 
    155 static bool isDangerousHTTPEquiv(const String& value)
    156 {
    157     String equiv = value.stripWhiteSpace();
    158     return equalIgnoringCase(equiv, "refresh") || equalIgnoringCase(equiv, "set-cookie");
    159 }
    160 
    161 static inline String decode16BitUnicodeEscapeSequences(const String& string)
    162 {
    163     // Note, the encoding is ignored since each %u-escape sequence represents a UTF-16 code unit.
    164     return decodeEscapeSequences<Unicode16BitEscapeSequence>(string, UTF8Encoding());
    165 }
    166 
    167 static inline String decodeStandardURLEscapeSequences(const String& string, const WTF::TextEncoding& encoding)
    168 {
    169     // We use decodeEscapeSequences() instead of decodeURLEscapeSequences() (declared in weborigin/KURL.h) to
    170     // avoid platform-specific URL decoding differences (e.g. KURLGoogle).
    171     return decodeEscapeSequences<URLEscapeSequence>(string, encoding);
    172 }
    173 
    174 static String fullyDecodeString(const String& string, const WTF::TextEncoding& encoding)
    175 {
    176     size_t oldWorkingStringLength;
    177     String workingString = string;
    178     do {
    179         oldWorkingStringLength = workingString.length();
    180         workingString = decode16BitUnicodeEscapeSequences(decodeStandardURLEscapeSequences(workingString, encoding));
    181     } while (workingString.length() < oldWorkingStringLength);
    182     workingString.replace('+', ' ');
    183     return workingString;
    184 }
    185 
    186 static void truncateForSrcLikeAttribute(String& decodedSnippet)
    187 {
    188     // In HTTP URLs, characters following the first ?, #, or third slash may come from
    189     // the page itself and can be merely ignored by an attacker's server when a remote
    190     // script or script-like resource is requested. In DATA URLS, the payload starts at
    191     // the first comma, and the the first /*, //, or <!-- may introduce a comment. Characters
    192     // following this may come from the page itself and may be ignored when the script is
    193     // executed. For simplicity, we don't differentiate based on URL scheme, and stop at
    194     // the first # or ?, the third slash, or the first slash or < once a comma is seen.
    195     int slashCount = 0;
    196     bool commaSeen = false;
    197     for (size_t currentLength = 0; currentLength < decodedSnippet.length(); ++currentLength) {
    198         UChar currentChar = decodedSnippet[currentLength];
    199         if (currentChar == '?'
    200             || currentChar == '#'
    201             || ((currentChar == '/' || currentChar == '\\') && (commaSeen || ++slashCount > 2))
    202             || (currentChar == '<' && commaSeen)) {
    203             decodedSnippet.truncate(currentLength);
    204             return;
    205         }
    206         if (currentChar == ',')
    207             commaSeen = true;
    208     }
    209 }
    210 
    211 static void truncateForScriptLikeAttribute(String& decodedSnippet)
    212 {
    213     // Beware of trailing characters which came from the page itself, not the
    214     // injected vector. Excluding the terminating character covers common cases
    215     // where the page immediately ends the attribute, but doesn't cover more
    216     // complex cases where there is other page data following the injection.
    217     // Generally, these won't parse as javascript, so the injected vector
    218     // typically excludes them from consideration via a single-line comment or
    219     // by enclosing them in a string literal terminated later by the page's own
    220     // closing punctuation. Since the snippet has not been parsed, the vector
    221     // may also try to introduce these via entities. As a result, we'd like to
    222     // stop before the first "//", the first <!--, the first entity, or the first
    223     // quote not immediately following the first equals sign (taking whitespace
    224     // into consideration). To keep things simpler, we don't try to distinguish
    225     // between entity-introducing amperands vs. other uses, nor do we bother to
    226     // check for a second slash for a comment, nor do we bother to check for
    227     // !-- following a less-than sign. We stop instead on any ampersand
    228     // slash, or less-than sign.
    229     size_t position = 0;
    230     if ((position = decodedSnippet.find("=")) != kNotFound
    231         && (position = decodedSnippet.find(isNotHTMLSpace<UChar>, position + 1)) != kNotFound
    232         && (position = decodedSnippet.find(isTerminatingCharacter, isHTMLQuote(decodedSnippet[position]) ? position + 1 : position)) != kNotFound) {
    233         decodedSnippet.truncate(position);
    234     }
    235 }
    236 
    237 static ReflectedXSSDisposition combineXSSProtectionHeaderAndCSP(ReflectedXSSDisposition xssProtection, ReflectedXSSDisposition reflectedXSS)
    238 {
    239     ReflectedXSSDisposition result = std::max(xssProtection, reflectedXSS);
    240 
    241     if (result == ReflectedXSSInvalid || result == FilterReflectedXSS || result == ReflectedXSSUnset)
    242         return FilterReflectedXSS;
    243 
    244     return result;
    245 }
    246 
    247 static bool isSemicolonSeparatedAttribute(const HTMLToken::Attribute& attribute)
    248 {
    249     return threadSafeMatch(attribute.name, SVGNames::valuesAttr);
    250 }
    251 
    252 static String semicolonSeparatedValueContainingJavaScriptURL(const String& value)
    253 {
    254     Vector<String> valueList;
    255     value.split(';', valueList);
    256     for (size_t i = 0; i < valueList.size(); ++i) {
    257         String stripped = stripLeadingAndTrailingHTMLSpaces(valueList[i]);
    258         if (protocolIsJavaScript(stripped))
    259             return stripped;
    260     }
    261     return emptyString();
    262 }
    263 
    264 XSSAuditor::XSSAuditor()
    265     : m_isEnabled(false)
    266     , m_xssProtection(FilterReflectedXSS)
    267     , m_didSendValidCSPHeader(false)
    268     , m_didSendValidXSSProtectionHeader(false)
    269     , m_state(Uninitialized)
    270     , m_scriptTagFoundInRequest(false)
    271     , m_scriptTagNestingLevel(0)
    272     , m_encoding(UTF8Encoding())
    273 {
    274     // Although tempting to call init() at this point, the various objects
    275     // we want to reference might not all have been constructed yet.
    276 }
    277 
    278 void XSSAuditor::initForFragment()
    279 {
    280     ASSERT(isMainThread());
    281     ASSERT(m_state == Uninitialized);
    282     m_state = FilteringTokens;
    283     // When parsing a fragment, we don't enable the XSS auditor because it's
    284     // too much overhead.
    285     ASSERT(!m_isEnabled);
    286 }
    287 
    288 void XSSAuditor::init(Document* document, XSSAuditorDelegate* auditorDelegate)
    289 {
    290     ASSERT(isMainThread());
    291     if (m_state != Uninitialized)
    292         return;
    293     m_state = FilteringTokens;
    294 
    295     if (Settings* settings = document->settings())
    296         m_isEnabled = settings->xssAuditorEnabled();
    297 
    298     if (!m_isEnabled)
    299         return;
    300 
    301     m_documentURL = document->url().copy();
    302 
    303     // In theory, the Document could have detached from the LocalFrame after the
    304     // XSSAuditor was constructed.
    305     if (!document->frame()) {
    306         m_isEnabled = false;
    307         return;
    308     }
    309 
    310     if (m_documentURL.isEmpty()) {
    311         // The URL can be empty when opening a new browser window or calling window.open("").
    312         m_isEnabled = false;
    313         return;
    314     }
    315 
    316     if (m_documentURL.protocolIsData()) {
    317         m_isEnabled = false;
    318         return;
    319     }
    320 
    321     if (document->encoding().isValid())
    322         m_encoding = document->encoding();
    323 
    324     if (DocumentLoader* documentLoader = document->frame()->loader().documentLoader()) {
    325         DEFINE_STATIC_LOCAL(const AtomicString, XSSProtectionHeader, ("X-XSS-Protection", AtomicString::ConstructFromLiteral));
    326         const AtomicString& headerValue = documentLoader->response().httpHeaderField(XSSProtectionHeader);
    327         String errorDetails;
    328         unsigned errorPosition = 0;
    329         String reportURL;
    330         KURL xssProtectionReportURL;
    331 
    332         // Process the X-XSS-Protection header, then mix in the CSP header's value.
    333         ReflectedXSSDisposition xssProtectionHeader = parseXSSProtectionHeader(headerValue, errorDetails, errorPosition, reportURL);
    334         m_didSendValidXSSProtectionHeader = xssProtectionHeader != ReflectedXSSUnset && xssProtectionHeader != ReflectedXSSInvalid;
    335         if ((xssProtectionHeader == FilterReflectedXSS || xssProtectionHeader == BlockReflectedXSS) && !reportURL.isEmpty()) {
    336             xssProtectionReportURL = document->completeURL(reportURL);
    337             if (MixedContentChecker::isMixedContent(document->securityOrigin(), xssProtectionReportURL)) {
    338                 errorDetails = "insecure reporting URL for secure page";
    339                 xssProtectionHeader = ReflectedXSSInvalid;
    340                 xssProtectionReportURL = KURL();
    341             }
    342         }
    343         if (xssProtectionHeader == ReflectedXSSInvalid)
    344             document->addConsoleMessage(SecurityMessageSource, ErrorMessageLevel, "Error parsing header X-XSS-Protection: " + headerValue + ": "  + errorDetails + " at character position " + String::format("%u", errorPosition) + ". The default protections will be applied.");
    345 
    346         ReflectedXSSDisposition cspHeader = document->contentSecurityPolicy()->reflectedXSSDisposition();
    347         m_didSendValidCSPHeader = cspHeader != ReflectedXSSUnset && cspHeader != ReflectedXSSInvalid;
    348 
    349         m_xssProtection = combineXSSProtectionHeaderAndCSP(xssProtectionHeader, cspHeader);
    350         // FIXME: Combine the two report URLs in some reasonable way.
    351         if (auditorDelegate)
    352             auditorDelegate->setReportURL(xssProtectionReportURL.copy());
    353 
    354         FormData* httpBody = documentLoader->request().httpBody();
    355         if (httpBody && !httpBody->isEmpty())
    356             m_httpBodyAsString = httpBody->flattenToString();
    357     }
    358 
    359     setEncoding(m_encoding);
    360 }
    361 
    362 void XSSAuditor::setEncoding(const WTF::TextEncoding& encoding)
    363 {
    364     const size_t miniumLengthForSuffixTree = 512; // FIXME: Tune this parameter.
    365     const int suffixTreeDepth = 5;
    366 
    367     if (!encoding.isValid())
    368         return;
    369 
    370     m_encoding = encoding;
    371 
    372     m_decodedURL = canonicalize(m_documentURL.string(), NoTruncation);
    373     if (m_decodedURL.find(isRequiredForInjection) == kNotFound)
    374         m_decodedURL = String();
    375 
    376     if (!m_httpBodyAsString.isEmpty()) {
    377         m_decodedHTTPBody = canonicalize(m_httpBodyAsString, NoTruncation);
    378         m_httpBodyAsString = String();
    379         if (m_decodedHTTPBody.find(isRequiredForInjection) == kNotFound)
    380             m_decodedHTTPBody = String();
    381             if (m_decodedHTTPBody.length() >= miniumLengthForSuffixTree)
    382                 m_decodedHTTPBodySuffixTree = adoptPtr(new SuffixTree<ASCIICodebook>(m_decodedHTTPBody, suffixTreeDepth));
    383     }
    384 
    385     if (m_decodedURL.isEmpty() && m_decodedHTTPBody.isEmpty())
    386         m_isEnabled = false;
    387 }
    388 
    389 PassOwnPtr<XSSInfo> XSSAuditor::filterToken(const FilterTokenRequest& request)
    390 {
    391     ASSERT(m_state != Uninitialized);
    392     if (!m_isEnabled || m_xssProtection == AllowReflectedXSS)
    393         return nullptr;
    394 
    395     bool didBlockScript = false;
    396     if (request.token.type() == HTMLToken::StartTag)
    397         didBlockScript = filterStartToken(request);
    398     else if (m_scriptTagNestingLevel) {
    399         if (request.token.type() == HTMLToken::Character)
    400             didBlockScript = filterCharacterToken(request);
    401         else if (request.token.type() == HTMLToken::EndTag)
    402             filterEndToken(request);
    403     }
    404 
    405     if (didBlockScript) {
    406         bool didBlockEntirePage = (m_xssProtection == BlockReflectedXSS);
    407         OwnPtr<XSSInfo> xssInfo = XSSInfo::create(m_documentURL, didBlockEntirePage, m_didSendValidXSSProtectionHeader, m_didSendValidCSPHeader);
    408         return xssInfo.release();
    409     }
    410     return nullptr;
    411 }
    412 
    413 bool XSSAuditor::filterStartToken(const FilterTokenRequest& request)
    414 {
    415     m_state = FilteringTokens;
    416     bool didBlockScript = eraseDangerousAttributesIfInjected(request);
    417 
    418     if (hasName(request.token, scriptTag)) {
    419         didBlockScript |= filterScriptToken(request);
    420         ASSERT(request.shouldAllowCDATA || !m_scriptTagNestingLevel);
    421         m_scriptTagNestingLevel++;
    422     } else if (hasName(request.token, objectTag))
    423         didBlockScript |= filterObjectToken(request);
    424     else if (hasName(request.token, paramTag))
    425         didBlockScript |= filterParamToken(request);
    426     else if (hasName(request.token, embedTag))
    427         didBlockScript |= filterEmbedToken(request);
    428     else if (hasName(request.token, appletTag))
    429         didBlockScript |= filterAppletToken(request);
    430     else if (hasName(request.token, iframeTag) || hasName(request.token, frameTag))
    431         didBlockScript |= filterFrameToken(request);
    432     else if (hasName(request.token, metaTag))
    433         didBlockScript |= filterMetaToken(request);
    434     else if (hasName(request.token, baseTag))
    435         didBlockScript |= filterBaseToken(request);
    436     else if (hasName(request.token, formTag))
    437         didBlockScript |= filterFormToken(request);
    438     else if (hasName(request.token, inputTag))
    439         didBlockScript |= filterInputToken(request);
    440     else if (hasName(request.token, buttonTag))
    441         didBlockScript |= filterButtonToken(request);
    442 
    443     return didBlockScript;
    444 }
    445 
    446 void XSSAuditor::filterEndToken(const FilterTokenRequest& request)
    447 {
    448     ASSERT(m_scriptTagNestingLevel);
    449     m_state = FilteringTokens;
    450     if (hasName(request.token, scriptTag)) {
    451         m_scriptTagNestingLevel--;
    452         ASSERT(request.shouldAllowCDATA || !m_scriptTagNestingLevel);
    453     }
    454 }
    455 
    456 bool XSSAuditor::filterCharacterToken(const FilterTokenRequest& request)
    457 {
    458     ASSERT(m_scriptTagNestingLevel);
    459     ASSERT(m_state != Uninitialized);
    460     if (m_state == PermittingAdjacentCharacterTokens)
    461         return false;
    462 
    463     if ((m_state == SuppressingAdjacentCharacterTokens)
    464         || (m_scriptTagFoundInRequest && isContainedInRequest(canonicalizedSnippetForJavaScript(request)))) {
    465         request.token.eraseCharacters();
    466         request.token.appendToCharacter(' '); // Technically, character tokens can't be empty.
    467         m_state = SuppressingAdjacentCharacterTokens;
    468         return true;
    469     }
    470 
    471     m_state = PermittingAdjacentCharacterTokens;
    472     return false;
    473 }
    474 
    475 bool XSSAuditor::filterScriptToken(const FilterTokenRequest& request)
    476 {
    477     ASSERT(request.token.type() == HTMLToken::StartTag);
    478     ASSERT(hasName(request.token, scriptTag));
    479 
    480     bool didBlockScript = false;
    481     m_scriptTagFoundInRequest = isContainedInRequest(canonicalizedSnippetForTagName(request));
    482     if (m_scriptTagFoundInRequest) {
    483         didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().string(), SrcLikeAttributeTruncation);
    484         didBlockScript |= eraseAttributeIfInjected(request, XLinkNames::hrefAttr, blankURL().string(), SrcLikeAttributeTruncation);
    485     }
    486     return didBlockScript;
    487 }
    488 
    489 bool XSSAuditor::filterObjectToken(const FilterTokenRequest& request)
    490 {
    491     ASSERT(request.token.type() == HTMLToken::StartTag);
    492     ASSERT(hasName(request.token, objectTag));
    493 
    494     bool didBlockScript = false;
    495     if (isContainedInRequest(canonicalizedSnippetForTagName(request))) {
    496         didBlockScript |= eraseAttributeIfInjected(request, dataAttr, blankURL().string(), SrcLikeAttributeTruncation);
    497         didBlockScript |= eraseAttributeIfInjected(request, typeAttr);
    498         didBlockScript |= eraseAttributeIfInjected(request, classidAttr);
    499     }
    500     return didBlockScript;
    501 }
    502 
    503 bool XSSAuditor::filterParamToken(const FilterTokenRequest& request)
    504 {
    505     ASSERT(request.token.type() == HTMLToken::StartTag);
    506     ASSERT(hasName(request.token, paramTag));
    507 
    508     size_t indexOfNameAttribute;
    509     if (!findAttributeWithName(request.token, nameAttr, indexOfNameAttribute))
    510         return false;
    511 
    512     const HTMLToken::Attribute& nameAttribute = request.token.attributes().at(indexOfNameAttribute);
    513     if (!HTMLParamElement::isURLParameter(String(nameAttribute.value)))
    514         return false;
    515 
    516     return eraseAttributeIfInjected(request, valueAttr, blankURL().string(), SrcLikeAttributeTruncation);
    517 }
    518 
    519 bool XSSAuditor::filterEmbedToken(const FilterTokenRequest& request)
    520 {
    521     ASSERT(request.token.type() == HTMLToken::StartTag);
    522     ASSERT(hasName(request.token, embedTag));
    523 
    524     bool didBlockScript = false;
    525     if (isContainedInRequest(canonicalizedSnippetForTagName(request))) {
    526         didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), SrcLikeAttributeTruncation);
    527         didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().string(), SrcLikeAttributeTruncation);
    528         didBlockScript |= eraseAttributeIfInjected(request, typeAttr);
    529     }
    530     return didBlockScript;
    531 }
    532 
    533 bool XSSAuditor::filterAppletToken(const FilterTokenRequest& request)
    534 {
    535     ASSERT(request.token.type() == HTMLToken::StartTag);
    536     ASSERT(hasName(request.token, appletTag));
    537 
    538     bool didBlockScript = false;
    539     if (isContainedInRequest(canonicalizedSnippetForTagName(request))) {
    540         didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), SrcLikeAttributeTruncation);
    541         didBlockScript |= eraseAttributeIfInjected(request, objectAttr);
    542     }
    543     return didBlockScript;
    544 }
    545 
    546 bool XSSAuditor::filterFrameToken(const FilterTokenRequest& request)
    547 {
    548     ASSERT(request.token.type() == HTMLToken::StartTag);
    549     ASSERT(hasName(request.token, iframeTag) || hasName(request.token, frameTag));
    550 
    551     bool didBlockScript = eraseAttributeIfInjected(request, srcdocAttr, String(), ScriptLikeAttributeTruncation);
    552     if (isContainedInRequest(canonicalizedSnippetForTagName(request)))
    553         didBlockScript |= eraseAttributeIfInjected(request, srcAttr, String(), SrcLikeAttributeTruncation);
    554 
    555     return didBlockScript;
    556 }
    557 
    558 bool XSSAuditor::filterMetaToken(const FilterTokenRequest& request)
    559 {
    560     ASSERT(request.token.type() == HTMLToken::StartTag);
    561     ASSERT(hasName(request.token, metaTag));
    562 
    563     return eraseAttributeIfInjected(request, http_equivAttr);
    564 }
    565 
    566 bool XSSAuditor::filterBaseToken(const FilterTokenRequest& request)
    567 {
    568     ASSERT(request.token.type() == HTMLToken::StartTag);
    569     ASSERT(hasName(request.token, baseTag));
    570 
    571     return eraseAttributeIfInjected(request, hrefAttr);
    572 }
    573 
    574 bool XSSAuditor::filterFormToken(const FilterTokenRequest& request)
    575 {
    576     ASSERT(request.token.type() == HTMLToken::StartTag);
    577     ASSERT(hasName(request.token, formTag));
    578 
    579     return eraseAttributeIfInjected(request, actionAttr, kURLWithUniqueOrigin);
    580 }
    581 
    582 bool XSSAuditor::filterInputToken(const FilterTokenRequest& request)
    583 {
    584     ASSERT(request.token.type() == HTMLToken::StartTag);
    585     ASSERT(hasName(request.token, inputTag));
    586 
    587     return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigin, SrcLikeAttributeTruncation);
    588 }
    589 
    590 bool XSSAuditor::filterButtonToken(const FilterTokenRequest& request)
    591 {
    592     ASSERT(request.token.type() == HTMLToken::StartTag);
    593     ASSERT(hasName(request.token, buttonTag));
    594 
    595     return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigin, SrcLikeAttributeTruncation);
    596 }
    597 
    598 bool XSSAuditor::eraseDangerousAttributesIfInjected(const FilterTokenRequest& request)
    599 {
    600     DEFINE_STATIC_LOCAL(String, safeJavaScriptURL, ("javascript:void(0)"));
    601 
    602     bool didBlockScript = false;
    603     for (size_t i = 0; i < request.token.attributes().size(); ++i) {
    604         bool eraseAttribute = false;
    605         bool valueContainsJavaScriptURL = false;
    606         const HTMLToken::Attribute& attribute = request.token.attributes().at(i);
    607         // FIXME: Don't create a new String for every attribute.value in the document.
    608         if (isNameOfInlineEventHandler(attribute.name)) {
    609             eraseAttribute = isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), ScriptLikeAttributeTruncation));
    610         } else if (isSemicolonSeparatedAttribute(attribute)) {
    611             String subValue = semicolonSeparatedValueContainingJavaScriptURL(String(attribute.value));
    612             if (!subValue.isEmpty()) {
    613                 valueContainsJavaScriptURL = true;
    614                 eraseAttribute = isContainedInRequest(canonicalize(nameFromAttribute(request, attribute), NoTruncation))
    615                     && isContainedInRequest(canonicalize(subValue, ScriptLikeAttributeTruncation));
    616             }
    617         } else if (protocolIsJavaScript(stripLeadingAndTrailingHTMLSpaces(String(attribute.value)))) {
    618             valueContainsJavaScriptURL = true;
    619             eraseAttribute = isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), ScriptLikeAttributeTruncation));
    620         }
    621         if (!eraseAttribute)
    622             continue;
    623         request.token.eraseValueOfAttribute(i);
    624         if (valueContainsJavaScriptURL)
    625             request.token.appendToAttributeValue(i, safeJavaScriptURL);
    626         didBlockScript = true;
    627     }
    628     return didBlockScript;
    629 }
    630 
    631 bool XSSAuditor::eraseAttributeIfInjected(const FilterTokenRequest& request, const QualifiedName& attributeName, const String& replacementValue, TruncationKind treatment)
    632 {
    633     size_t indexOfAttribute = 0;
    634     if (!findAttributeWithName(request.token, attributeName, indexOfAttribute))
    635         return false;
    636 
    637     const HTMLToken::Attribute& attribute = request.token.attributes().at(indexOfAttribute);
    638     if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), treatment)))
    639         return false;
    640 
    641     if (threadSafeMatch(attributeName, srcAttr)) {
    642         if (isLikelySafeResource(String(attribute.value)))
    643             return false;
    644     } else if (threadSafeMatch(attributeName, http_equivAttr)) {
    645         if (!isDangerousHTTPEquiv(String(attribute.value)))
    646             return false;
    647     }
    648 
    649     request.token.eraseValueOfAttribute(indexOfAttribute);
    650     if (!replacementValue.isEmpty())
    651         request.token.appendToAttributeValue(indexOfAttribute, replacementValue);
    652 
    653     return true;
    654 }
    655 
    656 String XSSAuditor::canonicalizedSnippetForTagName(const FilterTokenRequest& request)
    657 {
    658     // Grab a fixed number of characters equal to the length of the token's name plus one (to account for the "<").
    659     return canonicalize(request.sourceTracker.sourceForToken(request.token).substring(0, request.token.name().size() + 1), NoTruncation);
    660 }
    661 
    662 String XSSAuditor::nameFromAttribute(const FilterTokenRequest& request, const HTMLToken::Attribute& attribute)
    663 {
    664     // The range inlcudes the character which terminates the name. So,
    665     // for an input of |name="value"|, the snippet is |name=|.
    666     int start = attribute.nameRange.start - request.token.startIndex();
    667     int end = attribute.valueRange.start - request.token.startIndex();
    668     return request.sourceTracker.sourceForToken(request.token).substring(start, end - start);
    669 }
    670 
    671 String XSSAuditor::snippetFromAttribute(const FilterTokenRequest& request, const HTMLToken::Attribute& attribute)
    672 {
    673     // The range doesn't include the character which terminates the value. So,
    674     // for an input of |name="value"|, the snippet is |name="value|. For an
    675     // unquoted input of |name=value |, the snippet is |name=value|.
    676     // FIXME: We should grab one character before the name also.
    677     int start = attribute.nameRange.start - request.token.startIndex();
    678     int end = attribute.valueRange.end - request.token.startIndex();
    679     return request.sourceTracker.sourceForToken(request.token).substring(start, end - start);
    680 }
    681 
    682 String XSSAuditor::canonicalize(String snippet, TruncationKind treatment)
    683 {
    684     String decodedSnippet = fullyDecodeString(snippet, m_encoding);
    685 
    686     if (treatment != NoTruncation) {
    687         decodedSnippet.truncate(kMaximumFragmentLengthTarget);
    688         if (treatment == SrcLikeAttributeTruncation)
    689             truncateForSrcLikeAttribute(decodedSnippet);
    690         else if (treatment == ScriptLikeAttributeTruncation)
    691             truncateForScriptLikeAttribute(decodedSnippet);
    692     }
    693 
    694     return decodedSnippet.removeCharacters(&isNonCanonicalCharacter);
    695 }
    696 
    697 String XSSAuditor::canonicalizedSnippetForJavaScript(const FilterTokenRequest& request)
    698 {
    699     String string = request.sourceTracker.sourceForToken(request.token);
    700     size_t startPosition = 0;
    701     size_t endPosition = string.length();
    702     size_t foundPosition = kNotFound;
    703     size_t lastNonSpacePosition = kNotFound;
    704 
    705     // Skip over initial comments to find start of code.
    706     while (startPosition < endPosition) {
    707         while (startPosition < endPosition && isHTMLSpace<UChar>(string[startPosition]))
    708             startPosition++;
    709 
    710         // Under SVG/XML rules, only HTML comment syntax matters and the parser returns
    711         // these as a separate comment tokens. Having consumed whitespace, we need not look
    712         // further for these.
    713         if (request.shouldAllowCDATA)
    714             break;
    715 
    716         // Under HTML rules, both the HTML and JS comment synatx matters, and the HTML
    717         // comment ends at the end of the line, not with -->.
    718         if (startsHTMLCommentAt(string, startPosition) || startsSingleLineCommentAt(string, startPosition)) {
    719             while (startPosition < endPosition && !isJSNewline(string[startPosition]))
    720                 startPosition++;
    721         } else if (startsMultiLineCommentAt(string, startPosition)) {
    722             if (startPosition + 2 < endPosition && (foundPosition = string.find("*/", startPosition + 2)) != kNotFound)
    723                 startPosition = foundPosition + 2;
    724             else
    725                 startPosition = endPosition;
    726         } else
    727             break;
    728     }
    729 
    730     String result;
    731     while (startPosition < endPosition && !result.length()) {
    732         // Stop at next comment (using the same rules as above for SVG/XML vs HTML), when we encounter a comma,
    733         // when we hit an opening <script> tag, or when we exceed the maximum length target. The comma rule
    734         // covers a common parameter concatenation case performed by some web servers.
    735         lastNonSpacePosition = kNotFound;
    736         for (foundPosition = startPosition; foundPosition < endPosition; foundPosition++) {
    737             if (!request.shouldAllowCDATA) {
    738                 if (startsSingleLineCommentAt(string, foundPosition)
    739                     || startsMultiLineCommentAt(string, foundPosition)
    740                     || startsHTMLCommentAt(string, foundPosition)) {
    741                     break;
    742                 }
    743             }
    744             if (string[foundPosition] == ',')
    745                 break;
    746 
    747             if (lastNonSpacePosition != kNotFound && startsOpeningScriptTagAt(string, foundPosition)) {
    748                 foundPosition = lastNonSpacePosition;
    749                 break;
    750             }
    751             if (foundPosition > startPosition + kMaximumFragmentLengthTarget) {
    752                 // After hitting the length target, we can only stop at a point where we know we are
    753                 // not in the middle of a %-escape sequence. For the sake of simplicity, approximate
    754                 // not stopping inside a (possibly multiply encoded) %-escape sequence by breaking on
    755                 // whitespace only. We should have enough text in these cases to avoid false positives.
    756                 if (isHTMLSpace<UChar>(string[foundPosition]))
    757                     break;
    758             }
    759             if (!isHTMLSpace<UChar>(string[foundPosition]))
    760                 lastNonSpacePosition = foundPosition;
    761         }
    762         result = canonicalize(string.substring(startPosition, foundPosition - startPosition), NoTruncation);
    763         startPosition = foundPosition + 1;
    764     }
    765 
    766     return result;
    767 }
    768 
    769 bool XSSAuditor::isContainedInRequest(const String& decodedSnippet)
    770 {
    771     if (decodedSnippet.isEmpty())
    772         return false;
    773     if (m_decodedURL.find(decodedSnippet, 0, false) != kNotFound)
    774         return true;
    775     if (m_decodedHTTPBodySuffixTree && !m_decodedHTTPBodySuffixTree->mightContain(decodedSnippet))
    776         return false;
    777     return m_decodedHTTPBody.find(decodedSnippet, 0, false) != kNotFound;
    778 }
    779 
    780 bool XSSAuditor::isLikelySafeResource(const String& url)
    781 {
    782     // Give empty URLs and about:blank a pass. Making a resourceURL from an
    783     // empty string below will likely later fail the "no query args test" as
    784     // it inherits the document's query args.
    785     if (url.isEmpty() || url == blankURL().string())
    786         return true;
    787 
    788     // If the resource is loaded from the same host as the enclosing page, it's
    789     // probably not an XSS attack, so we reduce false positives by allowing the
    790     // request, ignoring scheme and port considerations. If the resource has a
    791     // query string, we're more suspicious, however, because that's pretty rare
    792     // and the attacker might be able to trick a server-side script into doing
    793     // something dangerous with the query string.
    794     if (m_documentURL.host().isEmpty())
    795         return false;
    796 
    797     KURL resourceURL(m_documentURL, url);
    798     return (m_documentURL.host() == resourceURL.host() && resourceURL.query().isEmpty());
    799 }
    800 
    801 bool XSSAuditor::isSafeToSendToAnotherThread() const
    802 {
    803     return m_documentURL.isSafeToSendToAnotherThread()
    804         && m_decodedURL.isSafeToSendToAnotherThread()
    805         && m_decodedHTTPBody.isSafeToSendToAnotherThread()
    806         && m_httpBodyAsString.isSafeToSendToAnotherThread();
    807 }
    808 
    809 } // namespace WebCore
    810