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