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 259 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
270 switch (m_state) { | 270 switch (m_state) { |
271 case Uninitialized: | 271 case Uninitialized: |
272 ASSERT_NOT_REACHED(); | 272 ASSERT_NOT_REACHED(); |
273 break; | 273 break; |
274 case Initial: | 274 case Initial: |
275 didBlockScript = filterTokenInitial(token); | 275 didBlockScript = filterTokenInitial(token); |
276 break; | 276 break; |
277 case AfterScriptStartTag: | 277 case AfterScriptStartTag: |
278 didBlockScript = filterTokenAfterScriptStartTag(token); | 278 didBlockScript = filterTokenAfterScriptStartTag(token); |
279 ASSERT(m_state == Initial); | 279 ASSERT(m_state == Initial); |
280 m_cachedSnippet = String(); | 280 m_cachedDecodedSnippet = String(); |
281 break; | 281 break; |
282 } | 282 } |
283 | 283 |
284 if (didBlockScript) { | 284 if (didBlockScript) { |
285 // FIXME: Consider using a more helpful console message. | 285 // FIXME: Consider using a more helpful console message. |
286 DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaS
cript script. Source code of script found within request.\n")); | 286 DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaS
cript script. Source code of script found within request.\n")); |
287 m_parser->document()->addConsoleMessage(JSMessageSource, LogMessageType,
ErrorMessageLevel, consoleMessage); | 287 m_parser->document()->addConsoleMessage(JSMessageSource, LogMessageType,
ErrorMessageLevel, consoleMessage); |
288 | 288 |
289 bool didBlockEntirePage = (m_xssProtection == XSSProtectionBlockEnabled)
; | 289 bool didBlockEntirePage = (m_xssProtection == XSSProtectionBlockEnabled)
; |
290 if (didBlockEntirePage) | 290 if (didBlockEntirePage) |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
334 bool XSSAuditor::filterTokenAfterScriptStartTag(HTMLToken& token) | 334 bool XSSAuditor::filterTokenAfterScriptStartTag(HTMLToken& token) |
335 { | 335 { |
336 ASSERT(m_state == AfterScriptStartTag); | 336 ASSERT(m_state == AfterScriptStartTag); |
337 m_state = Initial; | 337 m_state = Initial; |
338 | 338 |
339 if (token.type() != HTMLTokenTypes::Character) { | 339 if (token.type() != HTMLTokenTypes::Character) { |
340 ASSERT(token.type() == HTMLTokenTypes::EndTag || token.type() == HTMLTok
enTypes::EndOfFile); | 340 ASSERT(token.type() == HTMLTokenTypes::EndTag || token.type() == HTMLTok
enTypes::EndOfFile); |
341 return false; | 341 return false; |
342 } | 342 } |
343 | 343 |
344 TextResourceDecoder* decoder = m_parser->document()->decoder(); | 344 if (isContainedInRequest(m_cachedDecodedSnippet) && isContainedInRequest(dec
odedSnippetForJavaScript(token))) { |
345 if (isContainedInRequest(fullyDecodeString(m_cachedSnippet, decoder))) { | 345 token.eraseCharacters(); |
346 int start = 0; | 346 token.appendToCharacter(' '); // Technically, character tokens can't be
empty. |
347 int end = token.endIndex() - token.startIndex(); | 347 return true; |
348 String snippet = snippetForJavaScript(snippetForRange(token, start, end)
); | |
349 if (isContainedInRequest(fullyDecodeString(snippet, decoder))) { | |
350 token.eraseCharacters(); | |
351 token.appendToCharacter(' '); // Technically, character tokens can't
be empty. | |
352 return true; | |
353 } | |
354 } | 348 } |
355 return false; | 349 return false; |
356 } | 350 } |
357 | 351 |
358 bool XSSAuditor::filterScriptToken(HTMLToken& token) | 352 bool XSSAuditor::filterScriptToken(HTMLToken& token) |
359 { | 353 { |
360 ASSERT(m_state == Initial); | 354 ASSERT(m_state == Initial); |
361 ASSERT(token.type() == HTMLTokenTypes::StartTag); | 355 ASSERT(token.type() == HTMLTokenTypes::StartTag); |
362 ASSERT(hasName(token, scriptTag)); | 356 ASSERT(hasName(token, scriptTag)); |
363 | 357 |
364 if (eraseAttributeIfInjected(token, srcAttr, blankURL().string(), SrcLikeAtt
ribute)) | 358 m_state = AfterScriptStartTag; |
365 return true; | 359 m_cachedDecodedSnippet = stripLeadingAndTrailingHTMLSpaces(decodedSnippetFor
Token(token)); |
366 | 360 |
367 m_state = AfterScriptStartTag; | 361 if (isContainedInRequest(decodedSnippetForName(token))) |
368 m_cachedSnippet = m_parser->sourceForToken(token); | 362 return eraseAttributeIfInjected(token, srcAttr, blankURL().string(), Src
LikeAttribute); |
| 363 |
369 return false; | 364 return false; |
370 } | 365 } |
371 | 366 |
372 bool XSSAuditor::filterObjectToken(HTMLToken& token) | 367 bool XSSAuditor::filterObjectToken(HTMLToken& token) |
373 { | 368 { |
374 ASSERT(m_state == Initial); | 369 ASSERT(m_state == Initial); |
375 ASSERT(token.type() == HTMLTokenTypes::StartTag); | 370 ASSERT(token.type() == HTMLTokenTypes::StartTag); |
376 ASSERT(hasName(token, objectTag)); | 371 ASSERT(hasName(token, objectTag)); |
377 | 372 |
378 bool didBlockScript = false; | 373 bool didBlockScript = false; |
379 | 374 if (isContainedInRequest(decodedSnippetForName(token))) { |
380 didBlockScript |= eraseAttributeIfInjected(token, dataAttr, blankURL().strin
g(), SrcLikeAttribute); | 375 didBlockScript |= eraseAttributeIfInjected(token, dataAttr, blankURL().s
tring(), SrcLikeAttribute); |
381 didBlockScript |= eraseAttributeIfInjected(token, typeAttr); | 376 didBlockScript |= eraseAttributeIfInjected(token, typeAttr); |
382 didBlockScript |= eraseAttributeIfInjected(token, classidAttr); | 377 didBlockScript |= eraseAttributeIfInjected(token, classidAttr); |
383 | 378 } |
384 return didBlockScript; | 379 return didBlockScript; |
385 } | 380 } |
386 | 381 |
387 bool XSSAuditor::filterParamToken(HTMLToken& token) | 382 bool XSSAuditor::filterParamToken(HTMLToken& token) |
388 { | 383 { |
389 ASSERT(m_state == Initial); | 384 ASSERT(m_state == Initial); |
390 ASSERT(token.type() == HTMLTokenTypes::StartTag); | 385 ASSERT(token.type() == HTMLTokenTypes::StartTag); |
391 ASSERT(hasName(token, paramTag)); | 386 ASSERT(hasName(token, paramTag)); |
392 | 387 |
393 size_t indexOfNameAttribute; | 388 size_t indexOfNameAttribute; |
394 if (!findAttributeWithName(token, nameAttr, indexOfNameAttribute)) | 389 if (!findAttributeWithName(token, nameAttr, indexOfNameAttribute)) |
395 return false; | 390 return false; |
396 | 391 |
397 const HTMLToken::Attribute& nameAttribute = token.attributes().at(indexOfNam
eAttribute); | 392 const HTMLToken::Attribute& nameAttribute = token.attributes().at(indexOfNam
eAttribute); |
398 String name = String(nameAttribute.m_value.data(), nameAttribute.m_value.siz
e()); | 393 String name = String(nameAttribute.m_value.data(), nameAttribute.m_value.siz
e()); |
399 | 394 |
400 if (!HTMLParamElement::isURLParameter(name)) | 395 if (!HTMLParamElement::isURLParameter(name)) |
401 return false; | 396 return false; |
402 | 397 |
403 return eraseAttributeIfInjected(token, valueAttr, blankURL().string(), SrcLi
keAttribute); | 398 return eraseAttributeIfInjected(token, valueAttr, blankURL().string(), SrcLi
keAttribute); |
404 } | 399 } |
405 | 400 |
406 bool XSSAuditor::filterEmbedToken(HTMLToken& token) | 401 bool XSSAuditor::filterEmbedToken(HTMLToken& token) |
407 { | 402 { |
408 ASSERT(m_state == Initial); | 403 ASSERT(m_state == Initial); |
409 ASSERT(token.type() == HTMLTokenTypes::StartTag); | 404 ASSERT(token.type() == HTMLTokenTypes::StartTag); |
410 ASSERT(hasName(token, embedTag)); | 405 ASSERT(hasName(token, embedTag)); |
411 | 406 |
412 bool didBlockScript = false; | 407 bool didBlockScript = false; |
413 | 408 if (isContainedInRequest(decodedSnippetForName(token))) { |
414 didBlockScript |= eraseAttributeIfInjected(token, codeAttr, String(), SrcLik
eAttribute); | 409 didBlockScript |= eraseAttributeIfInjected(token, codeAttr, String(), Sr
cLikeAttribute); |
415 didBlockScript |= eraseAttributeIfInjected(token, srcAttr, blankURL().string
(), SrcLikeAttribute); | 410 didBlockScript |= eraseAttributeIfInjected(token, srcAttr, blankURL().st
ring(), SrcLikeAttribute); |
416 didBlockScript |= eraseAttributeIfInjected(token, typeAttr); | 411 didBlockScript |= eraseAttributeIfInjected(token, typeAttr); |
417 | 412 } |
418 return didBlockScript; | 413 return didBlockScript; |
419 } | 414 } |
420 | 415 |
421 bool XSSAuditor::filterAppletToken(HTMLToken& token) | 416 bool XSSAuditor::filterAppletToken(HTMLToken& token) |
422 { | 417 { |
423 ASSERT(m_state == Initial); | 418 ASSERT(m_state == Initial); |
424 ASSERT(token.type() == HTMLTokenTypes::StartTag); | 419 ASSERT(token.type() == HTMLTokenTypes::StartTag); |
425 ASSERT(hasName(token, appletTag)); | 420 ASSERT(hasName(token, appletTag)); |
426 | 421 |
427 bool didBlockScript = false; | 422 bool didBlockScript = false; |
428 | 423 if (isContainedInRequest(decodedSnippetForName(token))) { |
429 didBlockScript |= eraseAttributeIfInjected(token, codeAttr, String(), SrcLik
eAttribute); | 424 didBlockScript |= eraseAttributeIfInjected(token, codeAttr, String(), Sr
cLikeAttribute); |
430 didBlockScript |= eraseAttributeIfInjected(token, objectAttr); | 425 didBlockScript |= eraseAttributeIfInjected(token, objectAttr); |
431 | 426 } |
432 return didBlockScript; | 427 return didBlockScript; |
433 } | 428 } |
434 | 429 |
435 bool XSSAuditor::filterIframeToken(HTMLToken& token) | 430 bool XSSAuditor::filterIframeToken(HTMLToken& token) |
436 { | 431 { |
437 ASSERT(m_state == Initial); | 432 ASSERT(m_state == Initial); |
438 ASSERT(token.type() == HTMLTokenTypes::StartTag); | 433 ASSERT(token.type() == HTMLTokenTypes::StartTag); |
439 ASSERT(hasName(token, iframeTag)); | 434 ASSERT(hasName(token, iframeTag)); |
440 | 435 |
441 return eraseAttributeIfInjected(token, srcAttr, String(), SrcLikeAttribute); | 436 if (isContainedInRequest(decodedSnippetForName(token))) |
| 437 return eraseAttributeIfInjected(token, srcAttr, String(), SrcLikeAttribu
te); |
| 438 |
| 439 return false; |
442 } | 440 } |
443 | 441 |
444 bool XSSAuditor::filterMetaToken(HTMLToken& token) | 442 bool XSSAuditor::filterMetaToken(HTMLToken& token) |
445 { | 443 { |
446 ASSERT(m_state == Initial); | 444 ASSERT(m_state == Initial); |
447 ASSERT(token.type() == HTMLTokenTypes::StartTag); | 445 ASSERT(token.type() == HTMLTokenTypes::StartTag); |
448 ASSERT(hasName(token, metaTag)); | 446 ASSERT(hasName(token, metaTag)); |
449 | 447 |
450 return eraseAttributeIfInjected(token, http_equivAttr); | 448 return eraseAttributeIfInjected(token, http_equivAttr); |
451 } | 449 } |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
524 return false; | 522 return false; |
525 token.eraseValueOfAttribute(indexOfAttribute); | 523 token.eraseValueOfAttribute(indexOfAttribute); |
526 if (!replacementValue.isEmpty()) | 524 if (!replacementValue.isEmpty()) |
527 token.appendToAttributeValue(indexOfAttribute, replacementValue)
; | 525 token.appendToAttributeValue(indexOfAttribute, replacementValue)
; |
528 return true; | 526 return true; |
529 } | 527 } |
530 } | 528 } |
531 return false; | 529 return false; |
532 } | 530 } |
533 | 531 |
534 String XSSAuditor::snippetForRange(const HTMLToken& token, int start, int end) | 532 String XSSAuditor::decodedSnippetForToken(const HTMLToken& token) |
535 { | 533 { |
536 // FIXME: There's an extra allocation here that we could save by | 534 String snippet = m_parser->sourceForToken(token); |
537 // passing the range to the parser. | 535 return fullyDecodeString(snippet, m_parser->document()->decoder()); |
538 return m_parser->sourceForToken(token).substring(start, end - start); | 536 } |
| 537 |
| 538 String XSSAuditor::decodedSnippetForName(const HTMLToken& token) |
| 539 { |
| 540 // Grab a fixed number of characters equal to the length of the token's |
| 541 // name plus one (to account for the "<"). |
| 542 return decodedSnippetForToken(token).substring(0, token.name().size() + 1); |
539 } | 543 } |
540 | 544 |
541 String XSSAuditor::decodedSnippetForAttribute(const HTMLToken& token, const HTML
Token::Attribute& attribute, AttributeKind treatment) | 545 String XSSAuditor::decodedSnippetForAttribute(const HTMLToken& token, const HTML
Token::Attribute& attribute, AttributeKind treatment) |
542 { | 546 { |
543 const size_t kMaximumSnippetLength = 100; | 547 const size_t kMaximumSnippetLength = 100; |
544 | 548 |
545 // The range doesn't inlcude the character which terminates the value. So, | 549 // The range doesn't inlcude the character which terminates the value. So, |
546 // for an input of |name="value"|, the snippet is |name="value|. For an | 550 // for an input of |name="value"|, the snippet is |name="value|. For an |
547 // unquoted input of |name=value |, the snippet is |name=value|. | 551 // unquoted input of |name=value |, the snippet is |name=value|. |
548 // FIXME: We should grab one character before the name also. | 552 // FIXME: We should grab one character before the name also. |
549 int start = attribute.m_nameRange.m_start - token.startIndex(); | 553 int start = attribute.m_nameRange.m_start - token.startIndex(); |
550 int end = attribute.m_valueRange.m_end - token.startIndex(); | 554 int end = attribute.m_valueRange.m_end - token.startIndex(); |
551 String decodedSnippet = fullyDecodeString(snippetForRange(token, start, end)
, m_parser->document()->decoder()); | 555 String decodedSnippet = fullyDecodeString(m_parser->sourceForToken(token).su
bstring(start, end - start), m_parser->document()->decoder()); |
552 decodedSnippet.truncate(kMaximumSnippetLength); | 556 decodedSnippet.truncate(kMaximumSnippetLength); |
553 if (treatment == SrcLikeAttribute) { | 557 if (treatment == SrcLikeAttribute) { |
554 int slashCount; | 558 int slashCount; |
555 size_t currentLength; | 559 size_t currentLength; |
556 // Characters following the first ?, #, or third slash may come from | 560 // Characters following the first ?, #, or third slash may come from |
557 // the page itself and can be merely ignored by an attacker's server | 561 // the page itself and can be merely ignored by an attacker's server |
558 // when a remote script or script-like resource is requested. | 562 // when a remote script or script-like resource is requested. |
559 for (slashCount = 0, currentLength = 0; currentLength < decodedSnippet.l
ength(); ++currentLength) { | 563 for (slashCount = 0, currentLength = 0; currentLength < decodedSnippet.l
ength(); ++currentLength) { |
560 if (decodedSnippet[currentLength] == '?' || decodedSnippet[currentLe
ngth] == '#' | 564 if (decodedSnippet[currentLength] == '?' || decodedSnippet[currentLe
ngth] == '#' |
561 || ((decodedSnippet[currentLength] == '/' || decodedSnippet[curr
entLength] == '\\') && ++slashCount > 2)) { | 565 || ((decodedSnippet[currentLength] == '/' || decodedSnippet[curr
entLength] == '\\') && ++slashCount > 2)) { |
562 decodedSnippet.truncate(currentLength); | 566 decodedSnippet.truncate(currentLength); |
563 break; | 567 break; |
564 } | 568 } |
565 } | 569 } |
566 } | 570 } |
567 return decodedSnippet; | 571 return decodedSnippet; |
568 } | 572 } |
569 | 573 |
570 bool XSSAuditor::isContainedInRequest(const String& decodedSnippet) | 574 String XSSAuditor::decodedSnippetForJavaScript(const HTMLToken& token) |
571 { | 575 { |
572 if (decodedSnippet.isEmpty()) | 576 String string = m_parser->sourceForToken(token); |
573 return false; | |
574 if (m_decodedURL.find(decodedSnippet, 0, false) != notFound) | |
575 return true; | |
576 if (m_decodedHTTPBodySuffixTree && !m_decodedHTTPBodySuffixTree->mightContai
n(decodedSnippet)) | |
577 return false; | |
578 return m_decodedHTTPBody.find(decodedSnippet, 0, false) != notFound; | |
579 } | |
580 | |
581 bool XSSAuditor::isSameOriginResource(const String& url) | |
582 { | |
583 // If the resource is loaded from the same URL as the enclosing page, it's | |
584 // probably not an XSS attack, so we reduce false positives by allowing the | |
585 // request. If the resource has a query string, we're more suspicious, | |
586 // however, because that's pretty rare and the attacker might be able to | |
587 // trick a server-side script into doing something dangerous with the query | |
588 // string. | |
589 KURL resourceURL(m_parser->document()->url(), url); | |
590 return (m_parser->document()->url().host() == resourceURL.host() && resource
URL.query().isEmpty()); | |
591 } | |
592 | |
593 String XSSAuditor::snippetForJavaScript(const String& string) | |
594 { | |
595 const size_t kMaximumFragmentLengthTarget = 100; | 577 const size_t kMaximumFragmentLengthTarget = 100; |
596 | 578 |
597 size_t startPosition = 0; | 579 size_t startPosition = 0; |
598 size_t endPosition = string.length(); | 580 size_t endPosition = string.length(); |
599 size_t foundPosition = notFound; | 581 size_t foundPosition = notFound; |
600 | 582 |
601 // Skip over initial comments to find start of code. | 583 // Skip over initial comments to find start of code. |
602 while (startPosition < endPosition) { | 584 while (startPosition < endPosition) { |
603 while (startPosition < endPosition && isHTMLSpace(string[startPosition])
) | 585 while (startPosition < endPosition && isHTMLSpace(string[startPosition])
) |
604 startPosition++; | 586 startPosition++; |
(...skipping 25 matching lines...) Expand all Loading... |
630 if (startsHTMLCommentAt(string, foundPosition)) { | 612 if (startsHTMLCommentAt(string, foundPosition)) { |
631 endPosition = foundPosition + 4; | 613 endPosition = foundPosition + 4; |
632 break; | 614 break; |
633 } | 615 } |
634 if (foundPosition > startPosition + kMaximumFragmentLengthTarget && isHT
MLSpace(string[foundPosition])) { | 616 if (foundPosition > startPosition + kMaximumFragmentLengthTarget && isHT
MLSpace(string[foundPosition])) { |
635 endPosition = foundPosition; | 617 endPosition = foundPosition; |
636 break; | 618 break; |
637 } | 619 } |
638 } | 620 } |
639 | 621 |
640 return string.substring(startPosition, endPosition - startPosition); | 622 return fullyDecodeString(string.substring(startPosition, endPosition - start
Position), m_parser->document()->decoder()); |
| 623 } |
| 624 |
| 625 bool XSSAuditor::isContainedInRequest(const String& decodedSnippet) |
| 626 { |
| 627 if (decodedSnippet.isEmpty()) |
| 628 return false; |
| 629 if (m_decodedURL.find(decodedSnippet, 0, false) != notFound) |
| 630 return true; |
| 631 if (m_decodedHTTPBodySuffixTree && !m_decodedHTTPBodySuffixTree->mightContai
n(decodedSnippet)) |
| 632 return false; |
| 633 return m_decodedHTTPBody.find(decodedSnippet, 0, false) != notFound; |
| 634 } |
| 635 |
| 636 bool XSSAuditor::isSameOriginResource(const String& url) |
| 637 { |
| 638 // If the resource is loaded from the same URL as the enclosing page, it's |
| 639 // probably not an XSS attack, so we reduce false positives by allowing the |
| 640 // request. If the resource has a query string, we're more suspicious, |
| 641 // however, because that's pretty rare and the attacker might be able to |
| 642 // trick a server-side script into doing something dangerous with the query |
| 643 // string. |
| 644 KURL resourceURL(m_parser->document()->url(), url); |
| 645 return (m_parser->document()->url().host() == resourceURL.host() && resource
URL.query().isEmpty()); |
641 } | 646 } |
642 | 647 |
643 } // namespace WebCore | 648 } // namespace WebCore |
OLD | NEW |