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