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