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