diff options
Diffstat (limited to 'Source/WebCore/page/XSSAuditor.cpp')
-rw-r--r-- | Source/WebCore/page/XSSAuditor.cpp | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/Source/WebCore/page/XSSAuditor.cpp b/Source/WebCore/page/XSSAuditor.cpp new file mode 100644 index 0000000..1b0e83f --- /dev/null +++ b/Source/WebCore/page/XSSAuditor.cpp @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2008, 2009 Daniel Bates (dbates@intudata.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "XSSAuditor.h" + +#include <wtf/StdLibExtras.h> +#include <wtf/Vector.h> + +#include "Console.h" +#include "DocumentLoader.h" +#include "DOMWindow.h" +#include "Frame.h" +#include "HTMLEntityParser.h" +#include "KURL.h" +#include "ResourceResponseBase.h" +#include "ScriptSourceCode.h" +#include "Settings.h" +#include "TextResourceDecoder.h" +#include <wtf/text/CString.h> +#include <wtf/text/StringConcatenate.h> + +namespace WebCore { + +static bool isNonCanonicalCharacter(UChar c) +{ + // We remove all non-ASCII characters, including non-printable ASCII characters. + // + // Note, we don't remove backslashes like PHP stripslashes(), which among other things converts "\\0" to the \0 character. + // Instead, we remove backslashes and zeros (since the string "\\0" =(remove backslashes)=> "0"). However, this has the + // adverse effect that we remove any legitimate zeros from a string. + // + // For instance: new String("http://localhost:8000") => new String("http://localhost:8"). + return (c == '\\' || c == '0' || c < ' ' || c >= 127); +} + +static bool isIllegalURICharacter(UChar c) +{ + // The characters described in section 2.4.3 of RFC 2396 <http://www.faqs.org/rfcs/rfc2396.html> in addition to the + // single quote character "'" are considered illegal URI characters. That is, the following characters cannot appear + // in a valid URI: ', ", <, > + // + // If the request does not contain these characters then we can assume that no inline scripts have been injected + // into the response page, because it is impossible to write an inline script of the form <script>...</script> + // without "<", ">". + return (c == '\'' || c == '"' || c == '<' || c == '>'); +} + +String XSSAuditor::CachingURLCanonicalizer::canonicalizeURL(FormData* formData, const TextEncoding& encoding, bool decodeEntities, + bool decodeURLEscapeSequencesTwice) +{ + if (decodeEntities == m_decodeEntities && decodeURLEscapeSequencesTwice == m_decodeURLEscapeSequencesTwice + && encoding == m_encoding && formData == m_formData) + return m_cachedCanonicalizedURL; + m_formData = formData; + return canonicalizeURL(formData->flattenToString(), encoding, decodeEntities, decodeURLEscapeSequencesTwice); +} + +String XSSAuditor::CachingURLCanonicalizer::canonicalizeURL(const String& url, const TextEncoding& encoding, bool decodeEntities, + bool decodeURLEscapeSequencesTwice) +{ + if (decodeEntities == m_decodeEntities && decodeURLEscapeSequencesTwice == m_decodeURLEscapeSequencesTwice + && encoding == m_encoding && url == m_inputURL) + return m_cachedCanonicalizedURL; + + m_cachedCanonicalizedURL = canonicalize(decodeURL(url, encoding, decodeEntities, decodeURLEscapeSequencesTwice)); + m_inputURL = url; + m_encoding = encoding; + m_decodeEntities = decodeEntities; + m_decodeURLEscapeSequencesTwice = decodeURLEscapeSequencesTwice; + ++m_generation; + return m_cachedCanonicalizedURL; +} + +void XSSAuditor::CachingURLCanonicalizer::clear() +{ + m_formData.clear(); + m_inputURL = String(); +} + +XSSAuditor::XSSAuditor(Frame* frame) + : m_frame(frame) + , m_generationOfSuffixTree(-1) +{ +} + +XSSAuditor::~XSSAuditor() +{ +} + +bool XSSAuditor::isEnabled() const +{ + Settings* settings = m_frame->settings(); + return (settings && settings->xssAuditorEnabled()); +} + +bool XSSAuditor::canEvaluate(const String& code) const +{ + if (!isEnabled()) + return true; + + FindTask task; + task.string = code; + task.decodeEntities = false; + task.allowRequestIfNoIllegalURICharacters = true; + + if (findInRequest(task)) { + DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaScript script. Source code of script found within request.\n")); + m_frame->domWindow()->console()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage, 1, String()); + return false; + } + return true; +} + +bool XSSAuditor::canEvaluateJavaScriptURL(const String& code) const +{ + if (!isEnabled()) + return true; + + FindTask task; + task.string = code; + task.decodeURLEscapeSequencesTwice = true; + + if (findInRequest(task)) { + DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaScript script. Source code of script found within request.\n")); + m_frame->domWindow()->console()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage, 1, String()); + return false; + } + return true; +} + +bool XSSAuditor::canCreateInlineEventListener(const String&, const String& code) const +{ + if (!isEnabled()) + return true; + + FindTask task; + task.string = code; + task.allowRequestIfNoIllegalURICharacters = true; + + if (findInRequest(task)) { + DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaScript script. Source code of script found within request.\n")); + m_frame->domWindow()->console()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage, 1, String()); + return false; + } + return true; +} + +bool XSSAuditor::canLoadExternalScriptFromSrc(const String& url) const +{ + if (!isEnabled()) + return true; + + if (isSameOriginResource(url)) + return true; + + FindTask task; + task.string = url; + task.allowRequestIfNoIllegalURICharacters = true; + + if (findInRequest(task)) { + DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaScript script. Source code of script found within request.\n")); + m_frame->domWindow()->console()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage, 1, String()); + return false; + } + return true; +} + +bool XSSAuditor::canLoadObject(const String& url) const +{ + if (!isEnabled()) + return true; + + if (isSameOriginResource(url)) + return true; + + FindTask task; + task.string = url; + task.allowRequestIfNoIllegalURICharacters = true; + + if (findInRequest(task)) { + String consoleMessage = makeString("Refused to load an object. URL found within request: \"", url, "\".\n"); + m_frame->domWindow()->console()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage, 1, String()); + return false; + } + return true; +} + +bool XSSAuditor::canSetBaseElementURL(const String& url) const +{ + if (!isEnabled()) + return true; + + if (isSameOriginResource(url)) + return true; + + FindTask task; + task.string = url; + task.allowRequestIfNoIllegalURICharacters = true; + + if (findInRequest(task)) { + DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to load from document base URL. URL found within request.\n")); + m_frame->domWindow()->console()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage, 1, String()); + return false; + } + return true; +} + +String XSSAuditor::canonicalize(const String& string) +{ + String result = decodeHTMLEntities(string); + return result.removeCharacters(&isNonCanonicalCharacter); +} + +String XSSAuditor::decodeURL(const String& string, const TextEncoding& encoding, bool decodeEntities, bool decodeURLEscapeSequencesTwice) +{ + String result; + String url = string; + + url.replace('+', ' '); + result = decodeURLEscapeSequences(url); + CString utf8Url = result.utf8(); + String decodedResult = encoding.decode(utf8Url.data(), utf8Url.length()); + if (!decodedResult.isEmpty()) + result = decodedResult; + if (decodeURLEscapeSequencesTwice) { + result = decodeURLEscapeSequences(result); + utf8Url = result.utf8(); + decodedResult = encoding.decode(utf8Url.data(), utf8Url.length()); + if (!decodedResult.isEmpty()) + result = decodedResult; + } + if (decodeEntities) + result = decodeHTMLEntities(result); + return result; +} + +String XSSAuditor::decodeHTMLEntities(const String& string, bool leaveUndecodableEntitiesUntouched) +{ + SegmentedString source(string); + SegmentedString sourceShadow; + Vector<UChar> result; + + while (!source.isEmpty()) { + UChar cc = *source; + source.advance(); + + if (cc != '&') { + result.append(cc); + continue; + } + + if (leaveUndecodableEntitiesUntouched) + sourceShadow = source; + bool notEnoughCharacters = false; + Vector<UChar, 16> decodedEntity; + bool success = consumeHTMLEntity(source, decodedEntity, notEnoughCharacters); + // We ignore notEnoughCharacters because we might as well use this loop + // to copy the remaining characters into |result|. + if (!success || (!leaveUndecodableEntitiesUntouched && decodedEntity.size() == 1 && decodedEntity[0] == 0xFFFD)) { + result.append('&'); + if (leaveUndecodableEntitiesUntouched) + source = sourceShadow; + } else { + Vector<UChar>::const_iterator iter = decodedEntity.begin(); + for (; iter != decodedEntity.end(); ++iter) + result.append(*iter); + } + } + + return String::adopt(result); +} + +bool XSSAuditor::isSameOriginResource(const String& url) const +{ + // If the resource is loaded from the same URL as the enclosing page, it's + // probably not an XSS attack, so we reduce false positives by allowing the + // request. If the resource has a query string, we're more suspicious, + // however, because that's pretty rare and the attacker might be able to + // trick a server-side script into doing something dangerous with the query + // string. + KURL resourceURL(m_frame->document()->url(), url); + return (m_frame->document()->url().host() == resourceURL.host() && resourceURL.query().isEmpty()); +} + +XSSProtectionDisposition XSSAuditor::xssProtection() const +{ + DEFINE_STATIC_LOCAL(String, XSSProtectionHeader, ("X-XSS-Protection")); + + Frame* frame = m_frame; + if (frame->document()->url() == blankURL()) + frame = m_frame->tree()->parent(); + + return parseXSSProtectionHeader(frame->loader()->documentLoader()->response().httpHeaderField(XSSProtectionHeader)); +} + +bool XSSAuditor::findInRequest(const FindTask& task) const +{ + bool result = false; + Frame* parentFrame = m_frame->tree()->parent(); + Frame* blockFrame = parentFrame; + if (parentFrame && m_frame->document()->url() == blankURL()) + result = findInRequest(parentFrame, task); + if (!result) { + result = findInRequest(m_frame, task); + blockFrame = m_frame; + } + if (!result) + return false; + + switch (xssProtection()) { + case XSSProtectionDisabled: + return false; + case XSSProtectionEnabled: + break; + case XSSProtectionBlockEnabled: + if (blockFrame) { + blockFrame->loader()->stopAllLoaders(); + blockFrame->navigationScheduler()->scheduleLocationChange(blockFrame->document()->securityOrigin(), blankURL(), String()); + } + break; + default: + ASSERT_NOT_REACHED(); + } + return true; +} + +bool XSSAuditor::findInRequest(Frame* frame, const FindTask& task) const +{ + ASSERT(frame->document()); + + if (!frame->document()->decoder()) { + // Note, JavaScript URLs do not have a charset. + return false; + } + + if (task.string.isEmpty()) + return false; + + DocumentLoader *documentLoader = frame->loader()->documentLoader(); + if (!documentLoader) + return false; + + FormData* formDataObj = documentLoader->originalRequest().httpBody(); + const bool hasFormData = formDataObj && !formDataObj->isEmpty(); + String pageURL = frame->document()->url().string(); + + if (!hasFormData) { + // We clear out our form data caches, in case we're holding onto a bunch of memory. + m_formDataCache.clear(); + m_formDataSuffixTree.clear(); + } + + String canonicalizedString; + if (!hasFormData && task.string.length() > 2 * pageURL.length()) { + // Q: Why do we bother to do this check at all? + // A: Canonicalizing large inline scripts can be expensive. We want to + // reduce the size of the string before we call canonicalize below, + // since it could result in an unneeded allocation and memcpy. + // + // Q: Why do we multiply by two here? + // A: We attempt to detect reflected XSS even when the server + // transforms the attacker's input with addSlashes. The best the + // attacker can do get the server to inflate his/her input by a + // factor of two by sending " characters, which the server + // transforms to \". + canonicalizedString = task.string.substring(0, 2 * pageURL.length()); + } else + canonicalizedString = task.string; + + if (frame->document()->url().protocolIsData()) + return false; + + canonicalizedString = canonicalize(canonicalizedString); + if (canonicalizedString.isEmpty()) + return false; + + if (!task.context.isEmpty()) + canonicalizedString = task.context + canonicalizedString; + + String decodedPageURL = m_pageURLCache.canonicalizeURL(pageURL, frame->document()->decoder()->encoding(), task.decodeEntities, task.decodeURLEscapeSequencesTwice); + + if (task.allowRequestIfNoIllegalURICharacters && !hasFormData && decodedPageURL.find(&isIllegalURICharacter, 0) == notFound) + return false; // Injection is impossible because the request does not contain any illegal URI characters. + + if (decodedPageURL.find(canonicalizedString, 0, false) != notFound) + return true; // We've found the string in the GET data. + + if (hasFormData) { + String decodedFormData = m_formDataCache.canonicalizeURL(formDataObj, frame->document()->decoder()->encoding(), task.decodeEntities, task.decodeURLEscapeSequencesTwice); + + if (m_generationOfSuffixTree != m_formDataCache.generation()) { + m_formDataSuffixTree = new SuffixTree<ASCIICodebook>(decodedFormData, 5); + m_generationOfSuffixTree = m_formDataCache.generation(); + } + + // Try a fast-reject via the suffixTree. + if (m_formDataSuffixTree && !m_formDataSuffixTree->mightContain(canonicalizedString)) + return false; + + if (decodedFormData.find(canonicalizedString, 0, false) != notFound) + return true; // We found the string in the POST data. + } + + return false; +} + +} // namespace WebCore + |