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