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