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