summaryrefslogtreecommitdiffstats
path: root/WebCore/page/XSSAuditor.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'WebCore/page/XSSAuditor.cpp')
-rw-r--r--WebCore/page/XSSAuditor.cpp134
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.
}