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