2 * Copyright (C) 2011 Adam Barth. All Rights Reserved.
3 * Copyright (C) 2011 Daniel Bates (dbates@intudata.com).
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
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.
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.
28 #include "core/html/parser/XSSAuditor.h"
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"
51 // SecurityOrigin::urlWithUniqueSecurityOrigin() can't be used cross-thread, or we'd use it instead.
52 const char kURLWithUniqueOrigin[] = "data:,";
58 using namespace HTMLNames;
60 static bool isNonCanonicalCharacter(UChar c)
62 // We remove all non-ASCII characters, including non-printable ASCII characters.
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,
70 // For instance: new String("http://localhost:8000") => new String("http:localhost:8").
71 return (c == '\\' || c == '0' || c == '\0' || c == '/' || c >= 127);
74 static bool isRequiredForInjection(UChar c)
76 return (c == '\'' || c == '"' || c == '<' || c == '>');
79 static bool isTerminatingCharacter(UChar c)
81 return (c == '&' || c == '/' || c == '"' || c == '\'' || c == '<' || c == '>' || c == ',');
84 static bool isHTMLQuote(UChar c)
86 return (c == '"' || c == '\'');
89 static bool isJSNewline(UChar c)
91 // Per ecma-262 section 7.3 Line Terminators.
92 return (c == '\n' || c == '\r' || c == 0x2028 || c == 0x2029);
95 static bool startsHTMLCommentAt(const String& string, size_t start)
97 return (start + 3 < string.length() && string[start] == '<' && string[start + 1] == '!' && string[start + 2] == '-' && string[start + 3] == '-');
100 static bool startsSingleLineCommentAt(const String& string, size_t start)
102 return (start + 1 < string.length() && string[start] == '/' && string[start + 1] == '/');
105 static bool startsMultiLineCommentAt(const String& string, size_t start)
107 return (start + 1 < string.length() && string[start] == '/' && string[start + 1] == '*');
110 static bool startsOpeningScriptTagAt(const String& string, size_t start)
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';
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)
125 return equalIgnoringNullity(vector, qname.localName().impl());
128 static bool hasName(const HTMLToken& token, const QualifiedName& name)
130 return threadSafeMatch(token.name(), name);
133 static bool findAttributeWithName(const HTMLToken& token, const QualifiedName& name, size_t& indexOfMatchingAttribute)
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();
138 for (size_t i = 0; i < token.attributes().size(); ++i) {
139 if (equalIgnoringNullity(token.attributes().at(i).name, attrName)) {
140 indexOfMatchingAttribute = i;
147 static bool isNameOfInlineEventHandler(const Vector<UChar, 32>& name)
149 const size_t lengthOfShortestInlineEventHandlerName = 5; // To wit: oncut.
150 if (name.size() < lengthOfShortestInlineEventHandlerName)
152 return name[0] == 'o' && name[1] == 'n';
155 static bool isDangerousHTTPEquiv(const String& value)
157 String equiv = value.stripWhiteSpace();
158 return equalIgnoringCase(equiv, "refresh") || equalIgnoringCase(equiv, "set-cookie");
161 static inline String decode16BitUnicodeEscapeSequences(const String& string)
163 // Note, the encoding is ignored since each %u-escape sequence represents a UTF-16 code unit.
164 return decodeEscapeSequences<Unicode16BitEscapeSequence>(string, UTF8Encoding());
167 static inline String decodeStandardURLEscapeSequences(const String& string, const WTF::TextEncoding& encoding)
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);
174 static String fullyDecodeString(const String& string, const WTF::TextEncoding& encoding)
176 size_t oldWorkingStringLength;
177 String workingString = string;
179 oldWorkingStringLength = workingString.length();
180 workingString = decode16BitUnicodeEscapeSequences(decodeStandardURLEscapeSequences(workingString, encoding));
181 } while (workingString.length() < oldWorkingStringLength);
182 workingString.replace('+', ' ');
183 return workingString;
186 static void truncateForSrcLikeAttribute(String& decodedSnippet)
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.
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);
206 if (currentChar == ',')
211 static void truncateForScriptLikeAttribute(String& decodedSnippet)
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.
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);
237 static ReflectedXSSDisposition combineXSSProtectionHeaderAndCSP(ReflectedXSSDisposition xssProtection, ReflectedXSSDisposition reflectedXSS)
239 ReflectedXSSDisposition result = std::max(xssProtection, reflectedXSS);
241 if (result == ReflectedXSSInvalid || result == FilterReflectedXSS || result == ReflectedXSSUnset)
242 return FilterReflectedXSS;
247 static bool isSemicolonSeparatedAttribute(const HTMLToken::Attribute& attribute)
249 return threadSafeMatch(attribute.name, SVGNames::valuesAttr);
252 static String semicolonSeparatedValueContainingJavaScriptURL(const String& value)
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))
261 return emptyString();
264 XSSAuditor::XSSAuditor()
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())
274 // Although tempting to call init() at this point, the various objects
275 // we want to reference might not all have been constructed yet.
278 void XSSAuditor::initForFragment()
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);
288 void XSSAuditor::init(Document* document, XSSAuditorDelegate* auditorDelegate)
290 ASSERT(isMainThread());
291 if (m_state != Uninitialized)
293 m_state = FilteringTokens;
295 if (Settings* settings = document->settings())
296 m_isEnabled = settings->xssAuditorEnabled();
301 m_documentURL = document->url().copy();
303 // In theory, the Document could have detached from the LocalFrame after the
304 // XSSAuditor was constructed.
305 if (!document->frame()) {
310 if (m_documentURL.isEmpty()) {
311 // The URL can be empty when opening a new browser window or calling window.open("").
316 if (m_documentURL.protocolIsData()) {
321 if (document->encoding().isValid())
322 m_encoding = document->encoding();
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);
328 unsigned errorPosition = 0;
330 KURL xssProtectionReportURL;
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();
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.");
346 ReflectedXSSDisposition cspHeader = document->contentSecurityPolicy()->reflectedXSSDisposition();
347 m_didSendValidCSPHeader = cspHeader != ReflectedXSSUnset && cspHeader != ReflectedXSSInvalid;
349 m_xssProtection = combineXSSProtectionHeaderAndCSP(xssProtectionHeader, cspHeader);
350 // FIXME: Combine the two report URLs in some reasonable way.
352 auditorDelegate->setReportURL(xssProtectionReportURL.copy());
354 FormData* httpBody = documentLoader->request().httpBody();
355 if (httpBody && !httpBody->isEmpty())
356 m_httpBodyAsString = httpBody->flattenToString();
359 setEncoding(m_encoding);
362 void XSSAuditor::setEncoding(const WTF::TextEncoding& encoding)
364 const size_t miniumLengthForSuffixTree = 512; // FIXME: Tune this parameter.
365 const int suffixTreeDepth = 5;
367 if (!encoding.isValid())
370 m_encoding = encoding;
372 m_decodedURL = canonicalize(m_documentURL.string(), NoTruncation);
373 if (m_decodedURL.find(isRequiredForInjection) == kNotFound)
374 m_decodedURL = String();
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));
385 if (m_decodedURL.isEmpty() && m_decodedHTTPBody.isEmpty())
389 PassOwnPtr<XSSInfo> XSSAuditor::filterToken(const FilterTokenRequest& request)
391 ASSERT(m_state != Uninitialized);
392 if (!m_isEnabled || m_xssProtection == AllowReflectedXSS)
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);
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();
413 bool XSSAuditor::filterStartToken(const FilterTokenRequest& request)
415 m_state = FilteringTokens;
416 bool didBlockScript = eraseDangerousAttributesIfInjected(request);
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);
443 return didBlockScript;
446 void XSSAuditor::filterEndToken(const FilterTokenRequest& request)
448 ASSERT(m_scriptTagNestingLevel);
449 m_state = FilteringTokens;
450 if (hasName(request.token, scriptTag)) {
451 m_scriptTagNestingLevel--;
452 ASSERT(request.shouldAllowCDATA || !m_scriptTagNestingLevel);
456 bool XSSAuditor::filterCharacterToken(const FilterTokenRequest& request)
458 ASSERT(m_scriptTagNestingLevel);
459 ASSERT(m_state != Uninitialized);
460 if (m_state == PermittingAdjacentCharacterTokens)
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;
471 m_state = PermittingAdjacentCharacterTokens;
475 bool XSSAuditor::filterScriptToken(const FilterTokenRequest& request)
477 ASSERT(request.token.type() == HTMLToken::StartTag);
478 ASSERT(hasName(request.token, scriptTag));
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);
486 return didBlockScript;
489 bool XSSAuditor::filterObjectToken(const FilterTokenRequest& request)
491 ASSERT(request.token.type() == HTMLToken::StartTag);
492 ASSERT(hasName(request.token, objectTag));
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);
500 return didBlockScript;
503 bool XSSAuditor::filterParamToken(const FilterTokenRequest& request)
505 ASSERT(request.token.type() == HTMLToken::StartTag);
506 ASSERT(hasName(request.token, paramTag));
508 size_t indexOfNameAttribute;
509 if (!findAttributeWithName(request.token, nameAttr, indexOfNameAttribute))
512 const HTMLToken::Attribute& nameAttribute = request.token.attributes().at(indexOfNameAttribute);
513 if (!HTMLParamElement::isURLParameter(String(nameAttribute.value)))
516 return eraseAttributeIfInjected(request, valueAttr, blankURL().string(), SrcLikeAttributeTruncation);
519 bool XSSAuditor::filterEmbedToken(const FilterTokenRequest& request)
521 ASSERT(request.token.type() == HTMLToken::StartTag);
522 ASSERT(hasName(request.token, embedTag));
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);
530 return didBlockScript;
533 bool XSSAuditor::filterAppletToken(const FilterTokenRequest& request)
535 ASSERT(request.token.type() == HTMLToken::StartTag);
536 ASSERT(hasName(request.token, appletTag));
538 bool didBlockScript = false;
539 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) {
540 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), SrcLikeAttributeTruncation);
541 didBlockScript |= eraseAttributeIfInjected(request, objectAttr);
543 return didBlockScript;
546 bool XSSAuditor::filterFrameToken(const FilterTokenRequest& request)
548 ASSERT(request.token.type() == HTMLToken::StartTag);
549 ASSERT(hasName(request.token, iframeTag) || hasName(request.token, frameTag));
551 bool didBlockScript = eraseAttributeIfInjected(request, srcdocAttr, String(), ScriptLikeAttributeTruncation);
552 if (isContainedInRequest(canonicalizedSnippetForTagName(request)))
553 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, String(), SrcLikeAttributeTruncation);
555 return didBlockScript;
558 bool XSSAuditor::filterMetaToken(const FilterTokenRequest& request)
560 ASSERT(request.token.type() == HTMLToken::StartTag);
561 ASSERT(hasName(request.token, metaTag));
563 return eraseAttributeIfInjected(request, http_equivAttr);
566 bool XSSAuditor::filterBaseToken(const FilterTokenRequest& request)
568 ASSERT(request.token.type() == HTMLToken::StartTag);
569 ASSERT(hasName(request.token, baseTag));
571 return eraseAttributeIfInjected(request, hrefAttr);
574 bool XSSAuditor::filterFormToken(const FilterTokenRequest& request)
576 ASSERT(request.token.type() == HTMLToken::StartTag);
577 ASSERT(hasName(request.token, formTag));
579 return eraseAttributeIfInjected(request, actionAttr, kURLWithUniqueOrigin);
582 bool XSSAuditor::filterInputToken(const FilterTokenRequest& request)
584 ASSERT(request.token.type() == HTMLToken::StartTag);
585 ASSERT(hasName(request.token, inputTag));
587 return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigin, SrcLikeAttributeTruncation);
590 bool XSSAuditor::filterButtonToken(const FilterTokenRequest& request)
592 ASSERT(request.token.type() == HTMLToken::StartTag);
593 ASSERT(hasName(request.token, buttonTag));
595 return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigin, SrcLikeAttributeTruncation);
598 bool XSSAuditor::eraseDangerousAttributesIfInjected(const FilterTokenRequest& request)
600 DEFINE_STATIC_LOCAL(String, safeJavaScriptURL, ("javascript:void(0)"));
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));
617 } else if (protocolIsJavaScript(stripLeadingAndTrailingHTMLSpaces(String(attribute.value)))) {
618 valueContainsJavaScriptURL = true;
619 eraseAttribute = isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), ScriptLikeAttributeTruncation));
623 request.token.eraseValueOfAttribute(i);
624 if (valueContainsJavaScriptURL)
625 request.token.appendToAttributeValue(i, safeJavaScriptURL);
626 didBlockScript = true;
628 return didBlockScript;
631 bool XSSAuditor::eraseAttributeIfInjected(const FilterTokenRequest& request, const QualifiedName& attributeName, const String& replacementValue, TruncationKind treatment)
633 size_t indexOfAttribute = 0;
634 if (!findAttributeWithName(request.token, attributeName, indexOfAttribute))
637 const HTMLToken::Attribute& attribute = request.token.attributes().at(indexOfAttribute);
638 if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), treatment)))
641 if (threadSafeMatch(attributeName, srcAttr)) {
642 if (isLikelySafeResource(String(attribute.value)))
644 } else if (threadSafeMatch(attributeName, http_equivAttr)) {
645 if (!isDangerousHTTPEquiv(String(attribute.value)))
649 request.token.eraseValueOfAttribute(indexOfAttribute);
650 if (!replacementValue.isEmpty())
651 request.token.appendToAttributeValue(indexOfAttribute, replacementValue);
656 String XSSAuditor::canonicalizedSnippetForTagName(const FilterTokenRequest& request)
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);
662 String XSSAuditor::nameFromAttribute(const FilterTokenRequest& request, const HTMLToken::Attribute& attribute)
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);
671 String XSSAuditor::snippetFromAttribute(const FilterTokenRequest& request, const HTMLToken::Attribute& attribute)
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);
682 String XSSAuditor::canonicalize(String snippet, TruncationKind treatment)
684 String decodedSnippet = fullyDecodeString(snippet, m_encoding);
686 if (treatment != NoTruncation) {
687 decodedSnippet.truncate(kMaximumFragmentLengthTarget);
688 if (treatment == SrcLikeAttributeTruncation)
689 truncateForSrcLikeAttribute(decodedSnippet);
690 else if (treatment == ScriptLikeAttributeTruncation)
691 truncateForScriptLikeAttribute(decodedSnippet);
694 return decodedSnippet.removeCharacters(&isNonCanonicalCharacter);
697 String XSSAuditor::canonicalizedSnippetForJavaScript(const FilterTokenRequest& request)
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;
705 // Skip over initial comments to find start of code.
706 while (startPosition < endPosition) {
707 while (startPosition < endPosition && isHTMLSpace<UChar>(string[startPosition]))
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)
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]))
721 } else if (startsMultiLineCommentAt(string, startPosition)) {
722 if (startPosition + 2 < endPosition && (foundPosition = string.find("*/", startPosition + 2)) != kNotFound)
723 startPosition = foundPosition + 2;
725 startPosition = endPosition;
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)) {
744 if (string[foundPosition] == ',')
747 if (lastNonSpacePosition != kNotFound && startsOpeningScriptTagAt(string, foundPosition)) {
748 foundPosition = lastNonSpacePosition;
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]))
759 if (!isHTMLSpace<UChar>(string[foundPosition]))
760 lastNonSpacePosition = foundPosition;
762 result = canonicalize(string.substring(startPosition, foundPosition - startPosition), NoTruncation);
763 startPosition = foundPosition + 1;
769 bool XSSAuditor::isContainedInRequest(const String& decodedSnippet)
771 if (decodedSnippet.isEmpty())
773 if (m_decodedURL.find(decodedSnippet, 0, false) != kNotFound)
775 if (m_decodedHTTPBodySuffixTree && !m_decodedHTTPBodySuffixTree->mightContain(decodedSnippet))
777 return m_decodedHTTPBody.find(decodedSnippet, 0, false) != kNotFound;
780 bool XSSAuditor::isLikelySafeResource(const String& url)
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())
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())
797 KURL resourceURL(m_documentURL, url);
798 return (m_documentURL.host() == resourceURL.host() && resourceURL.query().isEmpty());
801 bool XSSAuditor::isSafeToSendToAnotherThread() const
803 return m_documentURL.isSafeToSendToAnotherThread()
804 && m_decodedURL.isSafeToSendToAnotherThread()
805 && m_decodedHTTPBody.isSafeToSendToAnotherThread()
806 && m_httpBodyAsString.isSafeToSendToAnotherThread();
809 } // namespace WebCore