Changeset 195074 in webkit
- Timestamp:
- Jan 14, 2016 1:40:13 PM (8 years ago)
- Location:
- trunk/Source/WebCore
- Files:
-
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Source/WebCore/ChangeLog
r195073 r195074 1 2016-01-14 Daniel Bates <dabates@apple.com> 2 3 [XSS Auditor] Extract attribute truncation logic and formalize string canonicalization 4 https://bugs.webkit.org/show_bug.cgi?id=152874 5 6 Reviewed by Brent Fulgham. 7 8 Derived from Blink patch (by Tom Sepez <tsepez@chromium.org>): 9 <https://src.chromium.org/viewvc/blink?revision=176339&view=revision> 10 11 Extract the src-like and script-like attribute truncation logic into independent functions 12 towards making it more straightforward to re-purpose this logic. Additionally, formalize the 13 concept of string canonicalization as a member function that consolidates the process of 14 decoding URL escape sequences, truncating the decoded string (if applicable), and removing 15 characters that are considered noise. 16 17 * html/parser/XSSAuditor.cpp: 18 (WebCore::truncateForSrcLikeAttribute): Extracted from XSSAuditor::decodedSnippetForAttribute(). 19 (WebCore::truncateForScriptLikeAttribute): Ditto. 20 (WebCore::XSSAuditor::init): Write in terms of XSSAuditor::canonicalize(). 21 (WebCore::XSSAuditor::filterCharacterToken): Updated to make use of formalized canonicalization methods. 22 (WebCore::XSSAuditor::filterScriptToken): Ditto. 23 (WebCore::XSSAuditor::filterObjectToken): Ditto. 24 (WebCore::XSSAuditor::filterParamToken): Ditto. 25 (WebCore::XSSAuditor::filterEmbedToken): Ditto. 26 (WebCore::XSSAuditor::filterAppletToken): Ditto. 27 (WebCore::XSSAuditor::filterFrameToken): Ditto. 28 (WebCore::XSSAuditor::filterInputToken): Ditto. 29 (WebCore::XSSAuditor::filterButtonToken): Ditto. 30 (WebCore::XSSAuditor::eraseDangerousAttributesIfInjected): Ditto. 31 (WebCore::XSSAuditor::eraseAttributeIfInjected): Updated code to use early return style and avoid an unnecessary string 32 comparison when we know that a src attribute was injected. 33 (WebCore::XSSAuditor::canonicalizedSnippetForTagName): Renamed; formerly known as XSSAuditor::decodedSnippetForName(). Updated 34 to make use of XSSAuditor::canonicalize(). 35 (WebCore::XSSAuditor::snippetFromAttribute): Renamed; formerly known as XSSAuditor::decodedSnippetForAttribute(). Moved 36 truncation logic from here to WebCore::truncateFor{Script, Src}LikeAttribute. 37 (WebCore::XSSAuditor::canonicalize): Added. 38 (WebCore::XSSAuditor::canonicalizedSnippetForJavaScript): Added. 39 (WebCore::canonicalize): Deleted. 40 (WebCore::XSSAuditor::decodedSnippetForName): Deleted. 41 (WebCore::XSSAuditor::decodedSnippetForAttribute): Deleted. 42 (WebCore::XSSAuditor::decodedSnippetForJavaScript): Deleted. 43 * html/parser/XSSAuditor.h: Define enum class for the various attribute truncation styles. 44 1 45 2016-01-14 Daniel Bates <dabates@apple.com> 2 46 -
trunk/Source/WebCore/html/parser/XSSAuditor.cpp
r195073 r195074 64 64 } 65 65 66 static String canonicalize(const String& string)67 {68 return string.removeCharacters(&isNonCanonicalCharacter);69 }70 71 66 static bool isRequiredForInjection(UChar c) 72 67 { … … 181 176 } 182 177 178 static void truncateForSrcLikeAttribute(String& decodedSnippet) 179 { 180 // In HTTP URLs, characters following the first ?, #, or third slash may come from 181 // the page itself and can be merely ignored by an attacker's server when a remote 182 // script or script-like resource is requested. In DATA URLS, the payload starts at 183 // the first comma, and the the first /*, //, or <!-- may introduce a comment. Characters 184 // following this may come from the page itself and may be ignored when the script is 185 // executed. For simplicity, we don't differentiate based on URL scheme, and stop at 186 // the first # or ?, the third slash, or the first slash or < once a comma is seen. 187 int slashCount = 0; 188 bool commaSeen = false; 189 for (size_t currentLength = 0; currentLength < decodedSnippet.length(); ++currentLength) { 190 UChar currentChar = decodedSnippet[currentLength]; 191 if (currentChar == '?' 192 || currentChar == '#' 193 || ((currentChar == '/' || currentChar == '\\') && (commaSeen || ++slashCount > 2)) 194 || (currentChar == '<' && commaSeen)) { 195 decodedSnippet.truncate(currentLength); 196 return; 197 } 198 if (currentChar == ',') 199 commaSeen = true; 200 } 201 } 202 203 static void truncateForScriptLikeAttribute(String& decodedSnippet) 204 { 205 // Beware of trailing characters which came from the page itself, not the 206 // injected vector. Excluding the terminating character covers common cases 207 // where the page immediately ends the attribute, but doesn't cover more 208 // complex cases where there is other page data following the injection. 209 // Generally, these won't parse as JavaScript, so the injected vector 210 // typically excludes them from consideration via a single-line comment or 211 // by enclosing them in a string literal terminated later by the page's own 212 // closing punctuation. Since the snippet has not been parsed, the vector 213 // may also try to introduce these via entities. As a result, we'd like to 214 // stop before the first "//", the first <!--, the first entity, or the first 215 // quote not immediately following the first equals sign (taking whitespace 216 // into consideration). To keep things simpler, we don't try to distinguish 217 // between entity-introducing ampersands vs. other uses, nor do we bother to 218 // check for a second slash for a comment, nor do we bother to check for 219 // !-- following a less-than sign. We stop instead on any ampersand 220 // slash, or less-than sign. 221 size_t position = 0; 222 if ((position = decodedSnippet.find('=')) != notFound 223 && (position = decodedSnippet.find(isNotHTMLSpace, position + 1)) != notFound 224 && (position = decodedSnippet.find(isTerminatingCharacter, isHTMLQuote(decodedSnippet[position]) ? position + 1 : position)) != notFound) { 225 decodedSnippet.truncate(position); 226 } 227 } 228 183 229 static ContentSecurityPolicy::ReflectedXSSDisposition combineXSSProtectionHeaderAndCSP(ContentSecurityPolicy::ReflectedXSSDisposition xssProtection, ContentSecurityPolicy::ReflectedXSSDisposition reflectedXSS) 184 230 { … … 270 316 m_encoding = document->decoder()->encoding(); 271 317 272 m_decodedURL = canonicalize( fullyDecodeString(m_documentURL.string(), m_encoding));318 m_decodedURL = canonicalize(m_documentURL.string(), TruncationStyle::None); 273 319 if (m_decodedURL.find(isRequiredForInjection) == notFound) 274 320 m_decodedURL = String(); … … 308 354 httpBodyAsString = httpBody->flattenToString(); 309 355 if (!httpBodyAsString.isEmpty()) { 310 m_decodedHTTPBody = canonicalize( fullyDecodeString(httpBodyAsString, m_encoding));356 m_decodedHTTPBody = canonicalize(httpBodyAsString, TruncationStyle::None); 311 357 if (m_decodedHTTPBody.find(isRequiredForInjection) == notFound) 312 358 m_decodedHTTPBody = String(); … … 390 436 { 391 437 ASSERT(m_scriptTagNestingLevel); 392 if (m_wasScriptTagFoundInRequest && isContainedInRequest( decodedSnippetForJavaScript(request))) {438 if (m_wasScriptTagFoundInRequest && isContainedInRequest(canonicalizedSnippetForJavaScript(request))) { 393 439 request.token.clear(); 394 440 LChar space = ' '; … … 404 450 ASSERT(hasName(request.token, scriptTag)); 405 451 406 m_wasScriptTagFoundInRequest = isContainedInRequest( decodedSnippetForName(request));452 m_wasScriptTagFoundInRequest = isContainedInRequest(canonicalizedSnippetForTagName(request)); 407 453 408 454 bool didBlockScript = false; 409 455 if (m_wasScriptTagFoundInRequest) { 410 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().string(), SrcLikeAttribute);411 didBlockScript |= eraseAttributeIfInjected(request, XLinkNames::hrefAttr, blankURL().string(), SrcLikeAttribute);456 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().string(), TruncationStyle::SrcLikeAttribute); 457 didBlockScript |= eraseAttributeIfInjected(request, XLinkNames::hrefAttr, blankURL().string(), TruncationStyle::SrcLikeAttribute); 412 458 } 413 459 … … 421 467 422 468 bool didBlockScript = false; 423 if (isContainedInRequest( decodedSnippetForName(request))) {424 didBlockScript |= eraseAttributeIfInjected(request, dataAttr, blankURL().string(), SrcLikeAttribute);469 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) { 470 didBlockScript |= eraseAttributeIfInjected(request, dataAttr, blankURL().string(), TruncationStyle::SrcLikeAttribute); 425 471 didBlockScript |= eraseAttributeIfInjected(request, typeAttr); 426 472 didBlockScript |= eraseAttributeIfInjected(request, classidAttr); … … 442 488 return false; 443 489 444 return eraseAttributeIfInjected(request, valueAttr, blankURL().string(), SrcLikeAttribute);490 return eraseAttributeIfInjected(request, valueAttr, blankURL().string(), TruncationStyle::SrcLikeAttribute); 445 491 } 446 492 … … 451 497 452 498 bool didBlockScript = false; 453 if (isContainedInRequest( decodedSnippetForName(request))) {454 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), SrcLikeAttribute);455 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().string(), SrcLikeAttribute);499 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) { 500 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), TruncationStyle::SrcLikeAttribute); 501 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().string(), TruncationStyle::SrcLikeAttribute); 456 502 didBlockScript |= eraseAttributeIfInjected(request, typeAttr); 457 503 } … … 465 511 466 512 bool didBlockScript = false; 467 if (isContainedInRequest( decodedSnippetForName(request))) {468 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), SrcLikeAttribute);513 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) { 514 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(), TruncationStyle::SrcLikeAttribute); 469 515 didBlockScript |= eraseAttributeIfInjected(request, objectAttr); 470 516 } … … 477 523 ASSERT(hasName(request.token, iframeTag) || hasName(request.token, frameTag)); 478 524 479 bool didBlockScript = eraseAttributeIfInjected(request, srcdocAttr, String(), ScriptLikeAttribute);480 if (isContainedInRequest( decodedSnippetForName(request)))481 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, String(), SrcLikeAttribute);525 bool didBlockScript = eraseAttributeIfInjected(request, srcdocAttr, String(), TruncationStyle::ScriptLikeAttribute); 526 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) 527 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, String(), TruncationStyle::SrcLikeAttribute); 482 528 483 529 return didBlockScript; … … 513 559 ASSERT(hasName(request.token, inputTag)); 514 560 515 return eraseAttributeIfInjected(request, formactionAttr, blankURL().string(), SrcLikeAttribute);561 return eraseAttributeIfInjected(request, formactionAttr, blankURL().string(), TruncationStyle::SrcLikeAttribute); 516 562 } 517 563 … … 521 567 ASSERT(hasName(request.token, buttonTag)); 522 568 523 return eraseAttributeIfInjected(request, formactionAttr, blankURL().string(), SrcLikeAttribute);569 return eraseAttributeIfInjected(request, formactionAttr, blankURL().string(), TruncationStyle::SrcLikeAttribute); 524 570 } 525 571 … … 537 583 if (!isInlineEventHandler && !valueContainsJavaScriptURL) 538 584 continue; 539 if (!isContainedInRequest( decodedSnippetForAttribute(request, attribute,ScriptLikeAttribute)))585 if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), TruncationStyle::ScriptLikeAttribute))) 540 586 continue; 541 587 request.token.eraseValueOfAttribute(i); … … 547 593 } 548 594 549 bool XSSAuditor::eraseAttributeIfInjected(const FilterTokenRequest& request, const QualifiedName& attributeName, const String& replacementValue, AttributeKind treatment)595 bool XSSAuditor::eraseAttributeIfInjected(const FilterTokenRequest& request, const QualifiedName& attributeName, const String& replacementValue, TruncationStyle truncationStyle) 550 596 { 551 597 size_t indexOfAttribute = 0; 552 if (findAttributeWithName(request.token, attributeName, indexOfAttribute)) { 553 const HTMLToken::Attribute& attribute = request.token.attributes().at(indexOfAttribute); 554 if (isContainedInRequest(decodedSnippetForAttribute(request, attribute, treatment))) { 555 if (threadSafeMatch(attributeName, srcAttr) && isLikelySafeResource(String(attribute.value))) 556 return false; 557 if (threadSafeMatch(attributeName, http_equivAttr) && !isDangerousHTTPEquiv(String(attribute.value))) 558 return false; 559 request.token.eraseValueOfAttribute(indexOfAttribute); 560 if (!replacementValue.isEmpty()) 561 request.token.appendToAttributeValue(indexOfAttribute, replacementValue); 562 return true; 563 } 564 } 565 return false; 566 } 567 568 String XSSAuditor::decodedSnippetForName(const FilterTokenRequest& request) 598 if (!findAttributeWithName(request.token, attributeName, indexOfAttribute)) 599 return false; 600 601 const HTMLToken::Attribute& attribute = request.token.attributes().at(indexOfAttribute); 602 if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, attribute), truncationStyle))) 603 return false; 604 605 if (threadSafeMatch(attributeName, srcAttr)) { 606 if (isLikelySafeResource(String(attribute.value))) 607 return false; 608 } else if (threadSafeMatch(attributeName, http_equivAttr)) { 609 if (!isDangerousHTTPEquiv(String(attribute.value))) 610 return false; 611 } 612 613 request.token.eraseValueOfAttribute(indexOfAttribute); 614 if (!replacementValue.isEmpty()) 615 request.token.appendToAttributeValue(indexOfAttribute, replacementValue); 616 return true; 617 } 618 619 String XSSAuditor::canonicalizedSnippetForTagName(const FilterTokenRequest& request) 569 620 { 570 621 // Grab a fixed number of characters equal to the length of the token's name plus one (to account for the "<"). 571 return canonicalize( fullyDecodeString(request.sourceTracker.source(request.token), m_encoding).substring(0, request.token.name().size() + 1));572 } 573 574 String XSSAuditor:: decodedSnippetForAttribute(const FilterTokenRequest& request, const HTMLToken::Attribute& attribute, AttributeKind treatment)622 return canonicalize(request.sourceTracker.source(request.token).substring(0, request.token.name().size() + 1), TruncationStyle::None); 623 } 624 625 String XSSAuditor::snippetFromAttribute(const FilterTokenRequest& request, const HTMLToken::Attribute& attribute) 575 626 { 576 627 // The range doesn't include the character which terminates the value. So, … … 578 629 // unquoted input of |name=value |, the snippet is |name=value|. 579 630 // FIXME: We should grab one character before the name also. 580 unsigned start = attribute.startOffset; 581 unsigned end = attribute.endOffset; 582 583 // We defer canonicalizing the decoded string here to preserve embedded slashes (if any) that 584 // may lead us to truncate the string. 585 String decodedSnippet = fullyDecodeString(request.sourceTracker.source(request.token, start, end), m_encoding); 586 decodedSnippet.truncate(kMaximumFragmentLengthTarget); 587 if (treatment == SrcLikeAttribute) { 588 int slashCount = 0; 589 bool commaSeen = false; 590 // In HTTP URLs, characters following the first ?, #, or third slash may come from 591 // the page itself and can be merely ignored by an attacker's server when a remote 592 // script or script-like resource is requested. In DATA URLS, the payload starts at 593 // the first comma, and the the first /*, //, or <!-- may introduce a comment. Characters 594 // following this may come from the page itself and may be ignored when the script is 595 // executed. For simplicity, we don't differentiate based on URL scheme, and stop at 596 // the first # or ?, the third slash, or the first slash or < once a comma is seen. 597 for (size_t currentLength = 0; currentLength < decodedSnippet.length(); ++currentLength) { 598 UChar currentChar = decodedSnippet[currentLength]; 599 if (currentChar == '?' 600 || currentChar == '#' 601 || ((currentChar == '/' || currentChar == '\\') && (commaSeen || ++slashCount > 2)) 602 || (currentChar == '<' && commaSeen)) { 603 decodedSnippet.truncate(currentLength); 604 break; 605 } 606 if (currentChar == ',') 607 commaSeen = true; 608 } 609 } else if (treatment == ScriptLikeAttribute) { 610 // Beware of trailing characters which came from the page itself, not the 611 // injected vector. Excluding the terminating character covers common cases 612 // where the page immediately ends the attribute, but doesn't cover more 613 // complex cases where there is other page data following the injection. 614 // Generally, these won't parse as javascript, so the injected vector 615 // typically excludes them from consideration via a single-line comment or 616 // by enclosing them in a string literal terminated later by the page's own 617 // closing punctuation. Since the snippet has not been parsed, the vector 618 // may also try to introduce these via entities. As a result, we'd like to 619 // stop before the first "//", the first <!--, the first entity, or the first 620 // quote not immediately following the first equals sign (taking whitespace 621 // into consideration). To keep things simpler, we don't try to distinguish 622 // between entity-introducing amperands vs. other uses, nor do we bother to 623 // check for a second slash for a comment, nor do we bother to check for 624 // !-- following a less-than sign. We stop instead on any ampersand 625 // slash, or less-than sign. 626 size_t position = 0; 627 if ((position = decodedSnippet.find('=')) != notFound 628 && (position = decodedSnippet.find(isNotHTMLSpace, position + 1)) != notFound 629 && (position = decodedSnippet.find(isTerminatingCharacter, isHTMLQuote(decodedSnippet[position]) ? position + 1 : position)) != notFound) { 630 decodedSnippet.truncate(position); 631 } 632 } 633 return canonicalize(decodedSnippet); 634 } 635 636 String XSSAuditor::decodedSnippetForJavaScript(const FilterTokenRequest& request) 631 return request.sourceTracker.source(request.token, attribute.startOffset, attribute.endOffset); 632 } 633 634 String XSSAuditor::canonicalize(const String& snippet, TruncationStyle truncationStyle) 635 { 636 String decodedSnippet = fullyDecodeString(snippet, m_encoding); 637 if (truncationStyle != TruncationStyle::None) { 638 decodedSnippet.truncate(kMaximumFragmentLengthTarget); 639 if (truncationStyle == TruncationStyle::SrcLikeAttribute) 640 truncateForSrcLikeAttribute(decodedSnippet); 641 else if (truncationStyle == TruncationStyle::ScriptLikeAttribute) 642 truncateForScriptLikeAttribute(decodedSnippet); 643 } 644 return decodedSnippet.removeCharacters(&isNonCanonicalCharacter); 645 } 646 647 String XSSAuditor::canonicalizedSnippetForJavaScript(const FilterTokenRequest& request) 637 648 { 638 649 String string = request.sourceTracker.source(request.token); … … 688 699 break; 689 700 } 690 691 701 if (foundPosition > startPosition + kMaximumFragmentLengthTarget) { 692 702 // After hitting the length target, we can only stop at a point where we know we are … … 702 712 } 703 713 704 result = canonicalize( fullyDecodeString(string.substring(startPosition, foundPosition - startPosition), m_encoding));714 result = canonicalize(string.substring(startPosition, foundPosition - startPosition), TruncationStyle::None); 705 715 startPosition = foundPosition + 1; 706 716 } -
trunk/Source/WebCore/html/parser/XSSAuditor.h
r194982 r195074 71 71 }; 72 72 73 enum AttributeKind { 73 enum class TruncationStyle { 74 None, 74 75 NormalAttribute, 75 76 SrcLikeAttribute, … … 93 94 94 95 bool eraseDangerousAttributesIfInjected(const FilterTokenRequest&); 95 bool eraseAttributeIfInjected(const FilterTokenRequest&, const QualifiedName&, const String& replacementValue = String(), AttributeKind treatment =NormalAttribute);96 bool eraseAttributeIfInjected(const FilterTokenRequest&, const QualifiedName&, const String& replacementValue = String(), TruncationStyle = TruncationStyle::NormalAttribute); 96 97 97 String decodedSnippetForToken(const HTMLToken&);98 String decodedSnippetForName(const FilterTokenRequest&);99 String decodedSnippetForAttribute(const FilterTokenRequest&, const HTMLToken::Attribute&, AttributeKind treatment = NormalAttribute);100 String decodedSnippetForJavaScript(const FilterTokenRequest&);98 String canonicalizedSnippetForTagName(const FilterTokenRequest&); 99 String canonicalizedSnippetForJavaScript(const FilterTokenRequest&); 100 String snippetFromAttribute(const FilterTokenRequest&, const HTMLToken::Attribute&); 101 String canonicalize(const String&, TruncationStyle); 101 102 102 103 bool isContainedInRequest(const String&);
Note: See TracChangeset
for help on using the changeset viewer.