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