| OLD | NEW |
| 1 /* | 1 /* |
| 2 * Copyright (C) 2011 Adam Barth. All Rights Reserved. | 2 * Copyright (C) 2011 Adam Barth. All Rights Reserved. |
| 3 * Copyright (C) 2011 Daniel Bates (dbates@intudata.com). | 3 * Copyright (C) 2011 Daniel Bates (dbates@intudata.com). |
| 4 * | 4 * |
| 5 * Redistribution and use in source and binary forms, with or without | 5 * Redistribution and use in source and binary forms, with or without |
| 6 * modification, are permitted provided that the following conditions | 6 * modification, are permitted provided that the following conditions |
| 7 * are met: | 7 * are met: |
| 8 * 1. Redistributions of source code must retain the above copyright | 8 * 1. Redistributions of source code must retain the above copyright |
| 9 * notice, this list of conditions and the following disclaimer. | 9 * notice, this list of conditions and the following disclaimer. |
| 10 * 2. Redistributions in binary form must reproduce the above copyright | 10 * 2. Redistributions in binary form must reproduce the above copyright |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 64 // Note, we don't remove backslashes like PHP stripslashes(), which among ot
her things converts "\\0" to the \0 character. | 64 // Note, we don't remove backslashes like PHP stripslashes(), which among ot
her 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 | 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. | 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 co
llapse successive path components, eg, | 67 // We also remove forward-slash, because it is common for some servers to co
llapse successive path components, eg, |
| 68 // a//b becomes a/b. | 68 // a//b becomes a/b. |
| 69 // | 69 // |
| 70 // For instance: new String("http://localhost:8000") => new String("http:loc
alhost:8"). | 70 // For instance: new String("http://localhost:8000") => new String("http:loc
alhost:8"). |
| 71 return (c == '\\' || c == '0' || c == '\0' || c == '/' || c >= 127); | 71 return (c == '\\' || c == '0' || c == '\0' || c == '/' || c >= 127); |
| 72 } | 72 } |
| 73 | 73 |
| 74 static String canonicalize(const String& string) | |
| 75 { | |
| 76 return string.removeCharacters(&isNonCanonicalCharacter); | |
| 77 } | |
| 78 | |
| 79 static bool isRequiredForInjection(UChar c) | 74 static bool isRequiredForInjection(UChar c) |
| 80 { | 75 { |
| 81 return (c == '\'' || c == '"' || c == '<' || c == '>'); | 76 return (c == '\'' || c == '"' || c == '<' || c == '>'); |
| 82 } | 77 } |
| 83 | 78 |
| 84 static bool isTerminatingCharacter(UChar c) | 79 static bool isTerminatingCharacter(UChar c) |
| 85 { | 80 { |
| 86 return (c == '&' || c == '/' || c == '"' || c == '\'' || c == '<' || c == '>
' || c == ','); | 81 return (c == '&' || c == '/' || c == '"' || c == '\'' || c == '<' || c == '>
' || c == ','); |
| 87 } | 82 } |
| 88 | 83 |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 181 size_t oldWorkingStringLength; | 176 size_t oldWorkingStringLength; |
| 182 String workingString = string; | 177 String workingString = string; |
| 183 do { | 178 do { |
| 184 oldWorkingStringLength = workingString.length(); | 179 oldWorkingStringLength = workingString.length(); |
| 185 workingString = decode16BitUnicodeEscapeSequences(decodeStandardURLEscap
eSequences(workingString, encoding)); | 180 workingString = decode16BitUnicodeEscapeSequences(decodeStandardURLEscap
eSequences(workingString, encoding)); |
| 186 } while (workingString.length() < oldWorkingStringLength); | 181 } while (workingString.length() < oldWorkingStringLength); |
| 187 workingString.replace('+', ' '); | 182 workingString.replace('+', ' '); |
| 188 return workingString; | 183 return workingString; |
| 189 } | 184 } |
| 190 | 185 |
| 186 static void truncateForSrcLikeAttribute(String& decodedSnippet) |
| 187 { |
| 188 // In HTTP URLs, characters following the first ?, #, or third slash may com
e 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 st
arts at |
| 191 // the first comma, and the the first /*, //, or <!-- may introduce a commen
t. 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 i
s seen. |
| 195 int slashCount = 0; |
| 196 bool commaSeen = false; |
| 197 for (size_t currentLength = 0; currentLength < decodedSnippet.length(); ++cu
rrentLength) { |
| 198 UChar currentChar = decodedSnippet[currentLength]; |
| 199 if (currentChar == '?' |
| 200 || currentChar == '#' |
| 201 || ((currentChar == '/' || currentChar == '\\') && (commaSeen || ++s
lashCount > 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 firs
t |
| 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(d
ecodedSnippet[position]) ? position + 1 : position)) != kNotFound) { |
| 233 decodedSnippet.truncate(position); |
| 234 } |
| 235 } |
| 236 |
| 191 static ReflectedXSSDisposition combineXSSProtectionHeaderAndCSP(ReflectedXSSDisp
osition xssProtection, ReflectedXSSDisposition reflectedXSS) | 237 static ReflectedXSSDisposition combineXSSProtectionHeaderAndCSP(ReflectedXSSDisp
osition xssProtection, ReflectedXSSDisposition reflectedXSS) |
| 192 { | 238 { |
| 193 ReflectedXSSDisposition result = std::max(xssProtection, reflectedXSS); | 239 ReflectedXSSDisposition result = std::max(xssProtection, reflectedXSS); |
| 194 | 240 |
| 195 if (result == ReflectedXSSInvalid || result == FilterReflectedXSS || result
== ReflectedXSSUnset) | 241 if (result == ReflectedXSSInvalid || result == FilterReflectedXSS || result
== ReflectedXSSUnset) |
| 196 return FilterReflectedXSS; | 242 return FilterReflectedXSS; |
| 197 | 243 |
| 198 return result; | 244 return result; |
| 199 } | 245 } |
| 200 | 246 |
| (...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 315 void XSSAuditor::setEncoding(const WTF::TextEncoding& encoding) | 361 void XSSAuditor::setEncoding(const WTF::TextEncoding& encoding) |
| 316 { | 362 { |
| 317 const size_t miniumLengthForSuffixTree = 512; // FIXME: Tune this parameter. | 363 const size_t miniumLengthForSuffixTree = 512; // FIXME: Tune this parameter. |
| 318 const int suffixTreeDepth = 5; | 364 const int suffixTreeDepth = 5; |
| 319 | 365 |
| 320 if (!encoding.isValid()) | 366 if (!encoding.isValid()) |
| 321 return; | 367 return; |
| 322 | 368 |
| 323 m_encoding = encoding; | 369 m_encoding = encoding; |
| 324 | 370 |
| 325 m_decodedURL = canonicalize(fullyDecodeString(m_documentURL.string(), m_enco
ding)); | 371 m_decodedURL = canonicalize(m_documentURL.string(), NoTruncation); |
| 326 if (m_decodedURL.find(isRequiredForInjection) == kNotFound) | 372 if (m_decodedURL.find(isRequiredForInjection) == kNotFound) |
| 327 m_decodedURL = String(); | 373 m_decodedURL = String(); |
| 328 | 374 |
| 329 if (!m_httpBodyAsString.isEmpty()) { | 375 if (!m_httpBodyAsString.isEmpty()) { |
| 330 m_decodedHTTPBody = canonicalize(fullyDecodeString(m_httpBodyAsString, m
_encoding)); | 376 m_decodedHTTPBody = canonicalize(m_httpBodyAsString, NoTruncation); |
| 331 m_httpBodyAsString = String(); | 377 m_httpBodyAsString = String(); |
| 332 if (m_decodedHTTPBody.find(isRequiredForInjection) == kNotFound) | 378 if (m_decodedHTTPBody.find(isRequiredForInjection) == kNotFound) |
| 333 m_decodedHTTPBody = String(); | 379 m_decodedHTTPBody = String(); |
| 334 if (m_decodedHTTPBody.length() >= miniumLengthForSuffixTree) | 380 if (m_decodedHTTPBody.length() >= miniumLengthForSuffixTree) |
| 335 m_decodedHTTPBodySuffixTree = adoptPtr(new SuffixTree<ASCIICodeb
ook>(m_decodedHTTPBody, suffixTreeDepth)); | 381 m_decodedHTTPBodySuffixTree = adoptPtr(new SuffixTree<ASCIICodeb
ook>(m_decodedHTTPBody, suffixTreeDepth)); |
| 336 } | 382 } |
| 337 | 383 |
| 338 if (m_decodedURL.isEmpty() && m_decodedHTTPBody.isEmpty()) | 384 if (m_decodedURL.isEmpty() && m_decodedHTTPBody.isEmpty()) |
| 339 m_isEnabled = false; | 385 m_isEnabled = false; |
| 340 } | 386 } |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 407 } | 453 } |
| 408 | 454 |
| 409 bool XSSAuditor::filterCharacterToken(const FilterTokenRequest& request) | 455 bool XSSAuditor::filterCharacterToken(const FilterTokenRequest& request) |
| 410 { | 456 { |
| 411 ASSERT(m_scriptTagNestingLevel); | 457 ASSERT(m_scriptTagNestingLevel); |
| 412 ASSERT(m_state != Uninitialized); | 458 ASSERT(m_state != Uninitialized); |
| 413 if (m_state == PermittingAdjacentCharacterTokens) | 459 if (m_state == PermittingAdjacentCharacterTokens) |
| 414 return false; | 460 return false; |
| 415 | 461 |
| 416 if ((m_state == SuppressingAdjacentCharacterTokens) | 462 if ((m_state == SuppressingAdjacentCharacterTokens) |
| 417 || (m_scriptTagFoundInRequest && isContainedInRequest(decodedSnippetForJ
avaScript(request)))) { | 463 || (m_scriptTagFoundInRequest && isContainedInRequest(canonicalizedSnipp
etForJavaScript(request)))) { |
| 418 request.token.eraseCharacters(); | 464 request.token.eraseCharacters(); |
| 419 request.token.appendToCharacter(' '); // Technically, character tokens c
an't be empty. | 465 request.token.appendToCharacter(' '); // Technically, character tokens c
an't be empty. |
| 420 m_state = SuppressingAdjacentCharacterTokens; | 466 m_state = SuppressingAdjacentCharacterTokens; |
| 421 return true; | 467 return true; |
| 422 } | 468 } |
| 423 | 469 |
| 424 m_state = PermittingAdjacentCharacterTokens; | 470 m_state = PermittingAdjacentCharacterTokens; |
| 425 return false; | 471 return false; |
| 426 } | 472 } |
| 427 | 473 |
| 428 bool XSSAuditor::filterScriptToken(const FilterTokenRequest& request) | 474 bool XSSAuditor::filterScriptToken(const FilterTokenRequest& request) |
| 429 { | 475 { |
| 430 ASSERT(request.token.type() == HTMLToken::StartTag); | 476 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 431 ASSERT(hasName(request.token, scriptTag)); | 477 ASSERT(hasName(request.token, scriptTag)); |
| 432 | 478 |
| 433 bool didBlockScript = false; | 479 bool didBlockScript = false; |
| 434 m_scriptTagFoundInRequest = isContainedInRequest(decodedSnippetForName(reque
st)); | 480 m_scriptTagFoundInRequest = isContainedInRequest(canonicalizedSnippetForTagN
ame(request)); |
| 435 if (m_scriptTagFoundInRequest) { | 481 if (m_scriptTagFoundInRequest) { |
| 436 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().
string(), SrcLikeAttribute); | 482 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().
string(), SrcLikeAttributeTruncation); |
| 437 didBlockScript |= eraseAttributeIfInjected(request, XLinkNames::hrefAttr
, blankURL().string(), SrcLikeAttribute); | 483 didBlockScript |= eraseAttributeIfInjected(request, XLinkNames::hrefAttr
, blankURL().string(), SrcLikeAttributeTruncation); |
| 438 } | 484 } |
| 439 return didBlockScript; | 485 return didBlockScript; |
| 440 } | 486 } |
| 441 | 487 |
| 442 bool XSSAuditor::filterObjectToken(const FilterTokenRequest& request) | 488 bool XSSAuditor::filterObjectToken(const FilterTokenRequest& request) |
| 443 { | 489 { |
| 444 ASSERT(request.token.type() == HTMLToken::StartTag); | 490 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 445 ASSERT(hasName(request.token, objectTag)); | 491 ASSERT(hasName(request.token, objectTag)); |
| 446 | 492 |
| 447 bool didBlockScript = false; | 493 bool didBlockScript = false; |
| 448 if (isContainedInRequest(decodedSnippetForName(request))) { | 494 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) { |
| 449 didBlockScript |= eraseAttributeIfInjected(request, dataAttr, blankURL()
.string(), SrcLikeAttribute); | 495 didBlockScript |= eraseAttributeIfInjected(request, dataAttr, blankURL()
.string(), SrcLikeAttributeTruncation); |
| 450 didBlockScript |= eraseAttributeIfInjected(request, typeAttr); | 496 didBlockScript |= eraseAttributeIfInjected(request, typeAttr); |
| 451 didBlockScript |= eraseAttributeIfInjected(request, classidAttr); | 497 didBlockScript |= eraseAttributeIfInjected(request, classidAttr); |
| 452 } | 498 } |
| 453 return didBlockScript; | 499 return didBlockScript; |
| 454 } | 500 } |
| 455 | 501 |
| 456 bool XSSAuditor::filterParamToken(const FilterTokenRequest& request) | 502 bool XSSAuditor::filterParamToken(const FilterTokenRequest& request) |
| 457 { | 503 { |
| 458 ASSERT(request.token.type() == HTMLToken::StartTag); | 504 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 459 ASSERT(hasName(request.token, paramTag)); | 505 ASSERT(hasName(request.token, paramTag)); |
| 460 | 506 |
| 461 size_t indexOfNameAttribute; | 507 size_t indexOfNameAttribute; |
| 462 if (!findAttributeWithName(request.token, nameAttr, indexOfNameAttribute)) | 508 if (!findAttributeWithName(request.token, nameAttr, indexOfNameAttribute)) |
| 463 return false; | 509 return false; |
| 464 | 510 |
| 465 const HTMLToken::Attribute& nameAttribute = request.token.attributes().at(in
dexOfNameAttribute); | 511 const HTMLToken::Attribute& nameAttribute = request.token.attributes().at(in
dexOfNameAttribute); |
| 466 if (!HTMLParamElement::isURLParameter(String(nameAttribute.value))) | 512 if (!HTMLParamElement::isURLParameter(String(nameAttribute.value))) |
| 467 return false; | 513 return false; |
| 468 | 514 |
| 469 return eraseAttributeIfInjected(request, valueAttr, blankURL().string(), Src
LikeAttribute); | 515 return eraseAttributeIfInjected(request, valueAttr, blankURL().string(), Src
LikeAttributeTruncation); |
| 470 } | 516 } |
| 471 | 517 |
| 472 bool XSSAuditor::filterEmbedToken(const FilterTokenRequest& request) | 518 bool XSSAuditor::filterEmbedToken(const FilterTokenRequest& request) |
| 473 { | 519 { |
| 474 ASSERT(request.token.type() == HTMLToken::StartTag); | 520 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 475 ASSERT(hasName(request.token, embedTag)); | 521 ASSERT(hasName(request.token, embedTag)); |
| 476 | 522 |
| 477 bool didBlockScript = false; | 523 bool didBlockScript = false; |
| 478 if (isContainedInRequest(decodedSnippetForName(request))) { | 524 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) { |
| 479 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(),
SrcLikeAttribute); | 525 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(),
SrcLikeAttributeTruncation); |
| 480 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().
string(), SrcLikeAttribute); | 526 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, blankURL().
string(), SrcLikeAttributeTruncation); |
| 481 didBlockScript |= eraseAttributeIfInjected(request, typeAttr); | 527 didBlockScript |= eraseAttributeIfInjected(request, typeAttr); |
| 482 } | 528 } |
| 483 return didBlockScript; | 529 return didBlockScript; |
| 484 } | 530 } |
| 485 | 531 |
| 486 bool XSSAuditor::filterAppletToken(const FilterTokenRequest& request) | 532 bool XSSAuditor::filterAppletToken(const FilterTokenRequest& request) |
| 487 { | 533 { |
| 488 ASSERT(request.token.type() == HTMLToken::StartTag); | 534 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 489 ASSERT(hasName(request.token, appletTag)); | 535 ASSERT(hasName(request.token, appletTag)); |
| 490 | 536 |
| 491 bool didBlockScript = false; | 537 bool didBlockScript = false; |
| 492 if (isContainedInRequest(decodedSnippetForName(request))) { | 538 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) { |
| 493 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(),
SrcLikeAttribute); | 539 didBlockScript |= eraseAttributeIfInjected(request, codeAttr, String(),
SrcLikeAttributeTruncation); |
| 494 didBlockScript |= eraseAttributeIfInjected(request, objectAttr); | 540 didBlockScript |= eraseAttributeIfInjected(request, objectAttr); |
| 495 } | 541 } |
| 496 return didBlockScript; | 542 return didBlockScript; |
| 497 } | 543 } |
| 498 | 544 |
| 499 bool XSSAuditor::filterFrameToken(const FilterTokenRequest& request) | 545 bool XSSAuditor::filterFrameToken(const FilterTokenRequest& request) |
| 500 { | 546 { |
| 501 ASSERT(request.token.type() == HTMLToken::StartTag); | 547 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 502 ASSERT(hasName(request.token, iframeTag) || hasName(request.token, frameTag)
); | 548 ASSERT(hasName(request.token, iframeTag) || hasName(request.token, frameTag)
); |
| 503 | 549 |
| 504 bool didBlockScript = eraseAttributeIfInjected(request, srcdocAttr, String()
, ScriptLikeAttribute); | 550 bool didBlockScript = eraseAttributeIfInjected(request, srcdocAttr, String()
, ScriptLikeAttributeTruncation); |
| 505 if (isContainedInRequest(decodedSnippetForName(request))) | 551 if (isContainedInRequest(canonicalizedSnippetForTagName(request))) |
| 506 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, String(), S
rcLikeAttribute); | 552 didBlockScript |= eraseAttributeIfInjected(request, srcAttr, String(), S
rcLikeAttributeTruncation); |
| 507 | 553 |
| 508 return didBlockScript; | 554 return didBlockScript; |
| 509 } | 555 } |
| 510 | 556 |
| 511 bool XSSAuditor::filterMetaToken(const FilterTokenRequest& request) | 557 bool XSSAuditor::filterMetaToken(const FilterTokenRequest& request) |
| 512 { | 558 { |
| 513 ASSERT(request.token.type() == HTMLToken::StartTag); | 559 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 514 ASSERT(hasName(request.token, metaTag)); | 560 ASSERT(hasName(request.token, metaTag)); |
| 515 | 561 |
| 516 return eraseAttributeIfInjected(request, http_equivAttr); | 562 return eraseAttributeIfInjected(request, http_equivAttr); |
| (...skipping 13 matching lines...) Expand all Loading... |
| 530 ASSERT(hasName(request.token, formTag)); | 576 ASSERT(hasName(request.token, formTag)); |
| 531 | 577 |
| 532 return eraseAttributeIfInjected(request, actionAttr, kURLWithUniqueOrigin); | 578 return eraseAttributeIfInjected(request, actionAttr, kURLWithUniqueOrigin); |
| 533 } | 579 } |
| 534 | 580 |
| 535 bool XSSAuditor::filterInputToken(const FilterTokenRequest& request) | 581 bool XSSAuditor::filterInputToken(const FilterTokenRequest& request) |
| 536 { | 582 { |
| 537 ASSERT(request.token.type() == HTMLToken::StartTag); | 583 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 538 ASSERT(hasName(request.token, inputTag)); | 584 ASSERT(hasName(request.token, inputTag)); |
| 539 | 585 |
| 540 return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigi
n, SrcLikeAttribute); | 586 return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigi
n, SrcLikeAttributeTruncation); |
| 541 } | 587 } |
| 542 | 588 |
| 543 bool XSSAuditor::filterButtonToken(const FilterTokenRequest& request) | 589 bool XSSAuditor::filterButtonToken(const FilterTokenRequest& request) |
| 544 { | 590 { |
| 545 ASSERT(request.token.type() == HTMLToken::StartTag); | 591 ASSERT(request.token.type() == HTMLToken::StartTag); |
| 546 ASSERT(hasName(request.token, buttonTag)); | 592 ASSERT(hasName(request.token, buttonTag)); |
| 547 | 593 |
| 548 return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigi
n, SrcLikeAttribute); | 594 return eraseAttributeIfInjected(request, formactionAttr, kURLWithUniqueOrigi
n, SrcLikeAttributeTruncation); |
| 549 } | 595 } |
| 550 | 596 |
| 551 bool XSSAuditor::eraseDangerousAttributesIfInjected(const FilterTokenRequest& re
quest) | 597 bool XSSAuditor::eraseDangerousAttributesIfInjected(const FilterTokenRequest& re
quest) |
| 552 { | 598 { |
| 553 DEFINE_STATIC_LOCAL(String, safeJavaScriptURL, ("javascript:void(0)")); | 599 DEFINE_STATIC_LOCAL(String, safeJavaScriptURL, ("javascript:void(0)")); |
| 554 | 600 |
| 555 bool didBlockScript = false; | 601 bool didBlockScript = false; |
| 556 for (size_t i = 0; i < request.token.attributes().size(); ++i) { | 602 for (size_t i = 0; i < request.token.attributes().size(); ++i) { |
| 557 const HTMLToken::Attribute& attribute = request.token.attributes().at(i)
; | 603 const HTMLToken::Attribute& attribute = request.token.attributes().at(i)
; |
| 558 bool isInlineEventHandler = isNameOfInlineEventHandler(attribute.name); | 604 bool isInlineEventHandler = isNameOfInlineEventHandler(attribute.name); |
| 559 // FIXME: It would be better if we didn't create a new String for every
attribute in the document. | 605 // FIXME: It would be better if we didn't create a new String for every
attribute in the document. |
| 560 String strippedValue = stripLeadingAndTrailingHTMLSpaces(String(attribut
e.value)); | 606 String strippedValue = stripLeadingAndTrailingHTMLSpaces(String(attribut
e.value)); |
| 561 bool valueContainsJavaScriptURL = (!isInlineEventHandler && protocolIsJa
vaScript(strippedValue)) || (isSemicolonSeparatedAttribute(attribute) && semicol
onSeparatedValueContainsJavaScriptURL(strippedValue)); | 607 bool valueContainsJavaScriptURL = (!isInlineEventHandler && protocolIsJa
vaScript(strippedValue)) || (isSemicolonSeparatedAttribute(attribute) && semicol
onSeparatedValueContainsJavaScriptURL(strippedValue)); |
| 562 if (!isInlineEventHandler && !valueContainsJavaScriptURL) | 608 if (!isInlineEventHandler && !valueContainsJavaScriptURL) |
| 563 continue; | 609 continue; |
| 564 if (!isContainedInRequest(decodedSnippetForAttribute(request, attribute,
ScriptLikeAttribute))) | 610 if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, att
ribute), ScriptLikeAttributeTruncation))) |
| 565 continue; | 611 continue; |
| 566 request.token.eraseValueOfAttribute(i); | 612 request.token.eraseValueOfAttribute(i); |
| 567 if (valueContainsJavaScriptURL) | 613 if (valueContainsJavaScriptURL) |
| 568 request.token.appendToAttributeValue(i, safeJavaScriptURL); | 614 request.token.appendToAttributeValue(i, safeJavaScriptURL); |
| 569 didBlockScript = true; | 615 didBlockScript = true; |
| 570 } | 616 } |
| 571 return didBlockScript; | 617 return didBlockScript; |
| 572 } | 618 } |
| 573 | 619 |
| 574 bool XSSAuditor::eraseAttributeIfInjected(const FilterTokenRequest& request, con
st QualifiedName& attributeName, const String& replacementValue, AttributeKind t
reatment) | 620 bool XSSAuditor::eraseAttributeIfInjected(const FilterTokenRequest& request, con
st QualifiedName& attributeName, const String& replacementValue, TruncationKind
treatment) |
| 575 { | 621 { |
| 576 size_t indexOfAttribute = 0; | 622 size_t indexOfAttribute = 0; |
| 577 if (findAttributeWithName(request.token, attributeName, indexOfAttribute)) { | 623 if (!findAttributeWithName(request.token, attributeName, indexOfAttribute)) |
| 578 const HTMLToken::Attribute& attribute = request.token.attributes().at(in
dexOfAttribute); | 624 return false; |
| 579 if (isContainedInRequest(decodedSnippetForAttribute(request, attribute,
treatment))) { | 625 |
| 580 if (threadSafeMatch(attributeName, srcAttr) && isLikelySafeResource(
String(attribute.value))) | 626 const HTMLToken::Attribute& attribute = request.token.attributes().at(indexO
fAttribute); |
| 581 return false; | 627 if (!isContainedInRequest(canonicalize(snippetFromAttribute(request, attribu
te), treatment))) |
| 582 if (threadSafeMatch(attributeName, http_equivAttr) && !isDangerousHT
TPEquiv(String(attribute.value))) | 628 return false; |
| 583 return false; | 629 |
| 584 request.token.eraseValueOfAttribute(indexOfAttribute); | 630 if (threadSafeMatch(attributeName, srcAttr)) { |
| 585 if (!replacementValue.isEmpty()) | 631 if (isLikelySafeResource(String(attribute.value))) |
| 586 request.token.appendToAttributeValue(indexOfAttribute, replaceme
ntValue); | 632 return false; |
| 587 return true; | 633 } else if (threadSafeMatch(attributeName, http_equivAttr)) { |
| 588 } | 634 if (!isDangerousHTTPEquiv(String(attribute.value))) |
| 635 return false; |
| 589 } | 636 } |
| 590 return false; | 637 |
| 638 request.token.eraseValueOfAttribute(indexOfAttribute); |
| 639 if (!replacementValue.isEmpty()) |
| 640 request.token.appendToAttributeValue(indexOfAttribute, replacementValue)
; |
| 641 |
| 642 return true; |
| 591 } | 643 } |
| 592 | 644 |
| 593 String XSSAuditor::decodedSnippetForName(const FilterTokenRequest& request) | 645 String XSSAuditor::canonicalizedSnippetForTagName(const FilterTokenRequest& requ
est) |
| 594 { | 646 { |
| 595 // Grab a fixed number of characters equal to the length of the token's name
plus one (to account for the "<"). | 647 // Grab a fixed number of characters equal to the length of the token's name
plus one (to account for the "<"). |
| 596 return canonicalize(fullyDecodeString(request.sourceTracker.sourceForToken(r
equest.token), m_encoding).substring(0, request.token.name().size() + 1)); | 648 return canonicalize(request.sourceTracker.sourceForToken(request.token).subs
tring(0, request.token.name().size() + 1), NoTruncation); |
| 597 } | 649 } |
| 598 | 650 |
| 599 String XSSAuditor::decodedSnippetForAttribute(const FilterTokenRequest& request,
const HTMLToken::Attribute& attribute, AttributeKind treatment) | 651 String XSSAuditor::snippetFromAttribute(const FilterTokenRequest& request, const
HTMLToken::Attribute& attribute) |
| 600 { | 652 { |
| 601 // The range doesn't inlcude the character which terminates the value. So, | 653 // The range doesn't inlcude the character which terminates the value. So, |
| 602 // for an input of |name="value"|, the snippet is |name="value|. For an | 654 // for an input of |name="value"|, the snippet is |name="value|. For an |
| 603 // unquoted input of |name=value |, the snippet is |name=value|. | 655 // unquoted input of |name=value |, the snippet is |name=value|. |
| 604 // FIXME: We should grab one character before the name also. | 656 // FIXME: We should grab one character before the name also. |
| 605 int start = attribute.nameRange.start - request.token.startIndex(); | 657 int start = attribute.nameRange.start - request.token.startIndex(); |
| 606 int end = attribute.valueRange.end - request.token.startIndex(); | 658 int end = attribute.valueRange.end - request.token.startIndex(); |
| 607 String decodedSnippet = fullyDecodeString(request.sourceTracker.sourceForTok
en(request.token).substring(start, end - start), m_encoding); | 659 return request.sourceTracker.sourceForToken(request.token).substring(start,
end - start); |
| 608 decodedSnippet.truncate(kMaximumFragmentLengthTarget); | |
| 609 if (treatment == SrcLikeAttribute) { | |
| 610 int slashCount = 0; | |
| 611 bool commaSeen = false; | |
| 612 // In HTTP URLs, characters following the first ?, #, or third slash may
come from | |
| 613 // the page itself and can be merely ignored by an attacker's server whe
n a remote | |
| 614 // script or script-like resource is requested. In DATA URLS, the payloa
d starts at | |
| 615 // the first comma, and the the first /*, //, or <!-- may introduce a co
mment. Characters | |
| 616 // following this may come from the page itself and may be ignored when
the script is | |
| 617 // executed. For simplicity, we don't differentiate based on URL scheme,
and stop at | |
| 618 // the first # or ?, the third slash, or the first slash or < once a com
ma is seen. | |
| 619 for (size_t currentLength = 0; currentLength < decodedSnippet.length();
++currentLength) { | |
| 620 UChar currentChar = decodedSnippet[currentLength]; | |
| 621 if (currentChar == '?' | |
| 622 || currentChar == '#' | |
| 623 || ((currentChar == '/' || currentChar == '\\') && (commaSeen ||
++slashCount > 2)) | |
| 624 || (currentChar == '<' && commaSeen)) { | |
| 625 decodedSnippet.truncate(currentLength); | |
| 626 break; | |
| 627 } | |
| 628 if (currentChar == ',') | |
| 629 commaSeen = true; | |
| 630 } | |
| 631 } else if (treatment == ScriptLikeAttribute) { | |
| 632 // Beware of trailing characters which came from the page itself, not th
e | |
| 633 // injected vector. Excluding the terminating character covers common ca
ses | |
| 634 // where the page immediately ends the attribute, but doesn't cover more | |
| 635 // complex cases where there is other page data following the injection. | |
| 636 // Generally, these won't parse as javascript, so the injected vector | |
| 637 // typically excludes them from consideration via a single-line comment
or | |
| 638 // by enclosing them in a string literal terminated later by the page's
own | |
| 639 // closing punctuation. Since the snippet has not been parsed, the vecto
r | |
| 640 // may also try to introduce these via entities. As a result, we'd like
to | |
| 641 // stop before the first "//", the first <!--, the first entity, or the
first | |
| 642 // quote not immediately following the first equals sign (taking whitesp
ace | |
| 643 // into consideration). To keep things simpler, we don't try to distingu
ish | |
| 644 // between entity-introducing amperands vs. other uses, nor do we bother
to | |
| 645 // check for a second slash for a comment, nor do we bother to check for | |
| 646 // !-- following a less-than sign. We stop instead on any ampersand | |
| 647 // slash, or less-than sign. | |
| 648 size_t position = 0; | |
| 649 if ((position = decodedSnippet.find("=")) != kNotFound | |
| 650 && (position = decodedSnippet.find(isNotHTMLSpace<UChar>, position +
1)) != kNotFound | |
| 651 && (position = decodedSnippet.find(isTerminatingCharacter, isHTMLQuo
te(decodedSnippet[position]) ? position + 1 : position)) != kNotFound) { | |
| 652 decodedSnippet.truncate(position); | |
| 653 } | |
| 654 } | |
| 655 return canonicalize(decodedSnippet); | |
| 656 } | 660 } |
| 657 | 661 |
| 658 String XSSAuditor::decodedSnippetForJavaScript(const FilterTokenRequest& request
) | 662 String XSSAuditor::canonicalize(String snippet, TruncationKind treatment) |
| 663 { |
| 664 String decodedSnippet = fullyDecodeString(snippet, m_encoding); |
| 665 |
| 666 if (treatment != NoTruncation) { |
| 667 decodedSnippet.truncate(kMaximumFragmentLengthTarget); |
| 668 if (treatment == SrcLikeAttributeTruncation) |
| 669 truncateForSrcLikeAttribute(decodedSnippet); |
| 670 else if (treatment == ScriptLikeAttributeTruncation) |
| 671 truncateForScriptLikeAttribute(decodedSnippet); |
| 672 } |
| 673 |
| 674 return decodedSnippet.removeCharacters(&isNonCanonicalCharacter); |
| 675 } |
| 676 |
| 677 String XSSAuditor::canonicalizedSnippetForJavaScript(const FilterTokenRequest& r
equest) |
| 659 { | 678 { |
| 660 String string = request.sourceTracker.sourceForToken(request.token); | 679 String string = request.sourceTracker.sourceForToken(request.token); |
| 661 size_t startPosition = 0; | 680 size_t startPosition = 0; |
| 662 size_t endPosition = string.length(); | 681 size_t endPosition = string.length(); |
| 663 size_t foundPosition = kNotFound; | 682 size_t foundPosition = kNotFound; |
| 664 size_t lastNonSpacePosition = kNotFound; | 683 size_t lastNonSpacePosition = kNotFound; |
| 665 | 684 |
| 666 // Skip over initial comments to find start of code. | 685 // Skip over initial comments to find start of code. |
| 667 while (startPosition < endPosition) { | 686 while (startPosition < endPosition) { |
| 668 while (startPosition < endPosition && isHTMLSpace<UChar>(string[startPos
ition])) | 687 while (startPosition < endPosition && isHTMLSpace<UChar>(string[startPos
ition])) |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 702 break; | 721 break; |
| 703 } | 722 } |
| 704 } | 723 } |
| 705 if (string[foundPosition] == ',') | 724 if (string[foundPosition] == ',') |
| 706 break; | 725 break; |
| 707 | 726 |
| 708 if (lastNonSpacePosition != kNotFound && startsOpeningScriptTagAt(st
ring, foundPosition)) { | 727 if (lastNonSpacePosition != kNotFound && startsOpeningScriptTagAt(st
ring, foundPosition)) { |
| 709 foundPosition = lastNonSpacePosition; | 728 foundPosition = lastNonSpacePosition; |
| 710 break; | 729 break; |
| 711 } | 730 } |
| 712 | |
| 713 if (foundPosition > startPosition + kMaximumFragmentLengthTarget) { | 731 if (foundPosition > startPosition + kMaximumFragmentLengthTarget) { |
| 714 // After hitting the length target, we can only stop at a point
where we know we are | 732 // After hitting the length target, we can only stop at a point
where we know we are |
| 715 // not in the middle of a %-escape sequence. For the sake of sim
plicity, approximate | 733 // not in the middle of a %-escape sequence. For the sake of sim
plicity, approximate |
| 716 // not stopping inside a (possibly multiply encoded) %-escape se
quence by breaking on | 734 // not stopping inside a (possibly multiply encoded) %-escape se
quence by breaking on |
| 717 // whitespace only. We should have enough text in these cases to
avoid false positives. | 735 // whitespace only. We should have enough text in these cases to
avoid false positives. |
| 718 if (isHTMLSpace<UChar>(string[foundPosition])) | 736 if (isHTMLSpace<UChar>(string[foundPosition])) |
| 719 break; | 737 break; |
| 720 } | 738 } |
| 721 | |
| 722 if (!isHTMLSpace<UChar>(string[foundPosition])) | 739 if (!isHTMLSpace<UChar>(string[foundPosition])) |
| 723 lastNonSpacePosition = foundPosition; | 740 lastNonSpacePosition = foundPosition; |
| 724 } | 741 } |
| 725 | 742 result = canonicalize(string.substring(startPosition, foundPosition - st
artPosition), NoTruncation); |
| 726 result = canonicalize(fullyDecodeString(string.substring(startPosition,
foundPosition - startPosition), m_encoding)); | |
| 727 startPosition = foundPosition + 1; | 743 startPosition = foundPosition + 1; |
| 728 } | 744 } |
| 745 |
| 729 return result; | 746 return result; |
| 730 } | 747 } |
| 731 | 748 |
| 732 bool XSSAuditor::isContainedInRequest(const String& decodedSnippet) | 749 bool XSSAuditor::isContainedInRequest(const String& decodedSnippet) |
| 733 { | 750 { |
| 734 if (decodedSnippet.isEmpty()) | 751 if (decodedSnippet.isEmpty()) |
| 735 return false; | 752 return false; |
| 736 if (m_decodedURL.find(decodedSnippet, 0, false) != kNotFound) | 753 if (m_decodedURL.find(decodedSnippet, 0, false) != kNotFound) |
| 737 return true; | 754 return true; |
| 738 if (m_decodedHTTPBodySuffixTree && !m_decodedHTTPBodySuffixTree->mightContai
n(decodedSnippet)) | 755 if (m_decodedHTTPBodySuffixTree && !m_decodedHTTPBodySuffixTree->mightContai
n(decodedSnippet)) |
| (...skipping 24 matching lines...) Expand all Loading... |
| 763 | 780 |
| 764 bool XSSAuditor::isSafeToSendToAnotherThread() const | 781 bool XSSAuditor::isSafeToSendToAnotherThread() const |
| 765 { | 782 { |
| 766 return m_documentURL.isSafeToSendToAnotherThread() | 783 return m_documentURL.isSafeToSendToAnotherThread() |
| 767 && m_decodedURL.isSafeToSendToAnotherThread() | 784 && m_decodedURL.isSafeToSendToAnotherThread() |
| 768 && m_decodedHTTPBody.isSafeToSendToAnotherThread() | 785 && m_decodedHTTPBody.isSafeToSendToAnotherThread() |
| 769 && m_httpBodyAsString.isSafeToSendToAnotherThread(); | 786 && m_httpBodyAsString.isSafeToSendToAnotherThread(); |
| 770 } | 787 } |
| 771 | 788 |
| 772 } // namespace WebCore | 789 } // namespace WebCore |
| OLD | NEW |