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 |