1 /* 2 * Copyright (C) 2011 Adam Barth. All Rights Reserved. 3 * Copyright (C) 2011 Daniel Bates (dbates (at) 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 810