diff options
Diffstat (limited to 'WebCore/page/XSSAuditor.cpp')
| -rw-r--r-- | WebCore/page/XSSAuditor.cpp | 134 |
1 files changed, 107 insertions, 27 deletions
diff --git a/WebCore/page/XSSAuditor.cpp b/WebCore/page/XSSAuditor.cpp index 70b691b..72c2591 100644 --- a/WebCore/page/XSSAuditor.cpp +++ b/WebCore/page/XSSAuditor.cpp @@ -48,12 +48,41 @@ 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); + 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(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; + return m_cachedCanonicalizedURL; } XSSAuditor::XSSAuditor(Frame* frame) @@ -76,7 +105,7 @@ bool XSSAuditor::canEvaluate(const String& code) const if (!isEnabled()) return true; - if (findInRequest(code, false)) { + if (findInRequest(code, false, true)) { 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; @@ -89,7 +118,7 @@ bool XSSAuditor::canEvaluateJavaScriptURL(const String& code) const if (!isEnabled()) return true; - if (findInRequest(code)) { + if (findInRequest(code, true, false, true)) { 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; @@ -102,7 +131,7 @@ bool XSSAuditor::canCreateInlineEventListener(const String&, const String& code) if (!isEnabled()) return true; - if (findInRequest(code)) { + if (findInRequest(code, true, true)) { 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; @@ -115,6 +144,9 @@ bool XSSAuditor::canLoadExternalScriptFromSrc(const String& context, const Strin if (!isEnabled()) return true; + if (isSameOriginResource(url)) + return true; + if (findInRequest(context + url)) { 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()); @@ -128,8 +160,11 @@ bool XSSAuditor::canLoadObject(const String& url) const if (!isEnabled()) return true; + if (isSameOriginResource(url)) + return true; + if (findInRequest(url)) { - DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaScript script. Source code of script found within request")); + String consoleMessage = String::format("Refused to load an object. URL found within request: \"%s\".\n", url.utf8().data()); m_frame->domWindow()->console()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, consoleMessage, 1, String()); return false; } @@ -140,10 +175,12 @@ bool XSSAuditor::canSetBaseElementURL(const String& url) const { if (!isEnabled()) return true; - - KURL baseElementURL(m_frame->document()->url(), url); - if (m_frame->document()->url().host() != baseElementURL.host() && findInRequest(url)) { - DEFINE_STATIC_LOCAL(String, consoleMessage, ("Refused to execute a JavaScript script. Source code of script found within request")); + + if (isSameOriginResource(url)) + return true; + + if (findInRequest(url)) { + 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; } @@ -156,22 +193,30 @@ String XSSAuditor::canonicalize(const String& string) return result.removeCharacters(&isNonCanonicalCharacter); } -String XSSAuditor::decodeURL(const String& string, const TextEncoding& encoding, bool decodeHTMLentities) +String XSSAuditor::decodeURL(const String& string, const TextEncoding& encoding, bool decodeEntities, bool decodeURLEscapeSequencesTwice) { String result; String url = string; url.replace('+', ' '); result = decodeURLEscapeSequences(url); - String decodedResult = encoding.decode(result.utf8().data(), result.length()); + CString utf8Url = result.utf8(); + String decodedResult = encoding.decode(utf8Url.data(), utf8Url.length()); if (!decodedResult.isEmpty()) result = decodedResult; - if (decodeHTMLentities) + 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 leaveUndecodableHTMLEntitiesUntouched) +String XSSAuditor::decodeHTMLEntities(const String& string, bool leaveUndecodableEntitiesUntouched) { SegmentedString source(string); SegmentedString sourceShadow; @@ -186,7 +231,7 @@ String XSSAuditor::decodeHTMLEntities(const String& string, bool leaveUndecodabl continue; } - if (leaveUndecodableHTMLEntitiesUntouched) + if (leaveUndecodableEntitiesUntouched) sourceShadow = source; bool notEnoughCharacters = false; unsigned entity = PreloadScanner::consumeEntity(source, notEnoughCharacters); @@ -196,11 +241,11 @@ String XSSAuditor::decodeHTMLEntities(const String& string, bool leaveUndecodabl if (entity > 0xFFFF) { result.append(U16_LEAD(entity)); result.append(U16_TRAIL(entity)); - } else if (entity && (!leaveUndecodableHTMLEntitiesUntouched || entity != 0xFFFD)){ + } else if (entity && (!leaveUndecodableEntitiesUntouched || entity != 0xFFFD)){ result.append(entity); } else { result.append('&'); - if (leaveUndecodableHTMLEntitiesUntouched) + if (leaveUndecodableEntitiesUntouched) source = sourceShadow; } } @@ -208,31 +253,62 @@ String XSSAuditor::decodeHTMLEntities(const String& string, bool leaveUndecodabl return String::adopt(result); } -bool XSSAuditor::findInRequest(const String& string, bool decodeHTMLentities) const +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()); +} + +bool XSSAuditor::findInRequest(const String& string, bool decodeEntities, bool allowRequestIfNoIllegalURICharacters, + bool decodeURLEscapeSequencesTwice) const { bool result = false; Frame* parentFrame = m_frame->tree()->parent(); if (parentFrame && m_frame->document()->url() == blankURL()) - result = findInRequest(parentFrame, string, decodeHTMLentities); + result = findInRequest(parentFrame, string, decodeEntities, allowRequestIfNoIllegalURICharacters, decodeURLEscapeSequencesTwice); if (!result) - result = findInRequest(m_frame, string, decodeHTMLentities); + result = findInRequest(m_frame, string, decodeEntities, allowRequestIfNoIllegalURICharacters, decodeURLEscapeSequencesTwice); return result; } -bool XSSAuditor::findInRequest(Frame* frame, const String& string, bool decodeHTMLentities) const +bool XSSAuditor::findInRequest(Frame* frame, const String& string, bool decodeEntities, bool allowRequestIfNoIllegalURICharacters, + bool decodeURLEscapeSequencesTwice) const { ASSERT(frame->document()); - String pageURL = frame->document()->url().string(); if (!frame->document()->decoder()) { // Note, JavaScript URLs do not have a charset. return false; } - if (protocolIs(pageURL, "data")) + if (string.isEmpty()) return false; - if (string.isEmpty()) + FormData* formDataObj = frame->loader()->documentLoader()->originalRequest().httpBody(); + String pageURL = frame->document()->url().string(); + + if (!formDataObj && 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 + // bail out before the call to canonicalize below, which 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 \". + return false; + } + + if (frame->document()->url().protocolIs("data")) return false; String canonicalizedString = canonicalize(string); @@ -241,12 +317,16 @@ bool XSSAuditor::findInRequest(Frame* frame, const String& string, bool decodeHT if (string.length() < pageURL.length()) { // The string can actually fit inside the pageURL. - String decodedPageURL = canonicalize(decodeURL(pageURL, frame->document()->decoder()->encoding(), decodeHTMLentities)); + String decodedPageURL = m_cache.canonicalizeURL(pageURL, frame->document()->decoder()->encoding(), decodeEntities, decodeURLEscapeSequencesTwice); + + if (allowRequestIfNoIllegalURICharacters && (!formDataObj || formDataObj->isEmpty()) + && decodedPageURL.find(&isIllegalURICharacter, 0) == -1) + return false; // Injection is impossible because the request does not contain any illegal URI characters. + if (decodedPageURL.find(canonicalizedString, 0, false) != -1) - return true; // We've found the smoking gun. + return true; // We've found the smoking gun. } - FormData* formDataObj = frame->loader()->documentLoader()->originalRequest().httpBody(); if (formDataObj && !formDataObj->isEmpty()) { String formData = formDataObj->flattenToString(); if (string.length() < formData.length()) { @@ -254,7 +334,7 @@ bool XSSAuditor::findInRequest(Frame* frame, const String& string, bool decodeHT // the url-encoded POST data because the length of the url-decoded // code is less than or equal to the length of the url-encoded // string. - String decodedFormData = canonicalize(decodeURL(formData, frame->document()->decoder()->encoding(), decodeHTMLentities)); + String decodedFormData = m_cache.canonicalizeURL(formData, frame->document()->decoder()->encoding(), decodeEntities, decodeURLEscapeSequencesTwice); if (decodedFormData.find(canonicalizedString, 0, false) != -1) return true; // We found the string in the POST data. } |
