Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(73)

Side by Side Diff: Source/WebCore/html/parser/XSSAuditor.cpp

Issue 9701035: Merge 108881 - XSS Auditor targeting legitimate frames as false positives. (Closed) Base URL: http://svn.webkit.org/repository/webkit/branches/chromium/1025/
Patch Set: Created 8 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « Source/WebCore/html/parser/XSSAuditor.h ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
OLDNEW
« no previous file with comments | « Source/WebCore/html/parser/XSSAuditor.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698