# HG changeset patch
# Parent 3b58a9df4c8cf6d95af64d2a64b4f10c339b0d2c
# User Riccardo Pelizzi
Old CSP-XSS patch ported to mozilla-central
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -846,6 +846,131 @@
}
};
+const gXSSObserver = {
+
+ observe: function (aSubject, aTopic, aData)
+ {
+
+ if (!gPrefService.getBoolPref("security.csp.xsswarning"))
+ return;
+
+ var nb = gBrowser.getNotificationBox();
+ const priority = nb.PRIORITY_WARNING_HIGH;
+
+ var xssInfo = aSubject.wrappedJSObject;
+
+ // if (xssInfo.filter != "xssfilter") {
+ // return;
+ // }
+
+ // if it is a block mode event, do not display the warning
+ if (xssInfo.block == true)
+ return;
+
+ var buttons = [{
+ label: 'View Script',
+ accessKey: 'V',
+ popup: null,
+ callback: function () {
+ alert(xssInfo.url +
+ "\ncaused an XSS violation with the following script:\n" +
+ xssInfo.script);
+ }
+ }];
+
+ nb.appendNotification("[" + xssInfo.filter + "]: " + xssInfo.script.substring(0, 100), 'popup-blocked',
+ 'chrome://browser/skin/Info.png',
+ priority, buttons);
+
+ }
+};
+
+/* this does not belong to the mozilla codebase because
+ * it is used only for testing. it should have its own extension */
+var urlBarListener =
+{
+ QueryInterface: function(aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+
+ onLocationChange: function(aBrowser, aProgress, aRequest, aURI) {
+ if (aURI.scheme == 'about') return;
+ var hist = aBrowser.sessionHistory;
+ var timestamp = new Date().getTime();
+ try {
+ var entry = hist.getEntryAtIndex(hist.index-1, false);
+ dump("###PROFILE_CSP### ###URL### " + aURI.spec + " " +
+ timestamp + "\n");
+ } catch (x) {
+ //dump("###PROFILE_CSP### ###URL### " + aURI.spec + " " +
+ // timestamp + "\n");
+ }
+ },
+
+ onStateChange: function(aBrowser, aWebProgress, aRequest, aFlag, aStatus) {
+
+ },
+
+ onProgressChange: function(a, b, c, d, e, f, g) { },
+ onStatusChange: function(a, b, c, d, e) { },
+ onSecurityChange: function(a, b, c, d) { },
+ onRefreshAttempted: function(a, b, c, d, e) { },
+ onLinkIconAvailable: function(a) { }
+};
+
+var loadListener = {
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+
+
+ onStateChange : function (aWebProgress, aRequest,
+ aStateFlags, aStatus) {
+
+ // STATE_START is too early, doc is still the old page.
+ if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
+ return;
+
+ var domWin = aWebProgress.DOMWindow;
+ var domDoc = domWin.document;
+
+ // Only process things which might have HTML forms.
+ if (!(domDoc instanceof Ci.nsIDOMHTMLDocument))
+ return;
+
+ // Fastback doesn't fire DOMContentLoaded, so process forms now.
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
+ var arg = {target: domDoc};
+ this._loadListener(arg);
+ return;
+ }
+
+ // Add event listener to process page when DOM is complete.
+ domWin.addEventListener("load",
+ this._loadListener, false);
+ return;
+ },
+
+ _loadListener: function (e) {
+ var timestamp = new Date().getTime();
+ var uri = e.target.location;
+ dump("###PROFILE_CSP### ###LOAD### " + uri + " " +
+ timestamp + "\n");
+ },
+
+ // stubs for the nsIWebProgressListener interfaces which we don't use.
+ onProgressChange : function() { throw "Unexpected onProgressChange"; },
+ onLocationChange : function() { throw "Unexpected onLocationChange"; },
+ onStatusChange : function() { throw "Unexpected onStatusChange"; },
+ onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
+};
+
const gFormSubmitObserver = {
QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
@@ -1505,6 +1630,23 @@
Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false);
Services.obs.addObserver(gFormSubmitObserver, "invalidformsubmit", false);
+ // xss notices
+ Services.obs.addObserver(gXSSObserver, "xss-on-violate-policy", false);
+
+ // xss profiling stuff, only register if security.csp.profile is active
+ var prefSvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService);
+ var branch = prefSvc.getBranch("security.csp.");
+
+ var prof = branch.getBoolPref("profile");
+ if (prof) {
+ gBrowser.addTabsProgressListener(urlBarListener);
+ var progress = Cc["@mozilla.org/docloaderservice;1"].
+ getService(Ci.nsIWebProgress);
+ progress.addProgressListener(loadListener,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+ }
+
BrowserOffline.init();
OfflineApps.init();
IndexedDBPromptHelper.init();
diff --git a/browser/components/Makefile.in b/browser/components/Makefile.in
--- a/browser/components/Makefile.in
+++ b/browser/components/Makefile.in
@@ -70,6 +70,7 @@
sessionstore \
shell \
sidebar/src \
+ publictaint \
migration \
$(NULL)
diff --git a/browser/components/publictaint/Makefile.in b/browser/components/publictaint/Makefile.in
new file mode 100644
--- /dev/null
+++ b/browser/components/publictaint/Makefile.in
@@ -0,0 +1,46 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Feed Handling System
+#
+# The Initial Developer of the Original Code is Google Inc.
+# Portions created by the Initial Developer are Copyright (C) 2006
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Ben Goodger
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS = public src
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/components/publictaint/public/Makefile.in b/browser/components/publictaint/public/Makefile.in
new file mode 100644
--- /dev/null
+++ b/browser/components/publictaint/public/Makefile.in
@@ -0,0 +1,50 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 2001
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = public-ti
+XPIDL_MODULE = public-ti
+
+XPIDLSRCS = nsIPublicTaint.idl
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/components/publictaint/public/nsIPublicTaint.idl b/browser/components/publictaint/public/nsIPublicTaint.idl
new file mode 100644
--- /dev/null
+++ b/browser/components/publictaint/public/nsIPublicTaint.idl
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Riccardo Pelizzi
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(03702bc6-0a02-48b5-ba7b-69e696574f04)]
+interface nsIPublicTaint : nsISupports
+{
+ jsval p1FastMatch(in AString p, in AString s, in float distThreshold);
+ jsval p1FastMatchReverse(in AString s, in AString p, in float distThreshold);
+
+};
diff --git a/browser/components/publictaint/src/Makefile.in b/browser/components/publictaint/src/Makefile.in
new file mode 100644
--- /dev/null
+++ b/browser/components/publictaint/src/Makefile.in
@@ -0,0 +1,49 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org Code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = publicTaint.manifest
+EXTRA_PP_COMPONENTS = publicTaint.js
+
+include $(topsrcdir)/config/rules.mk
+
diff --git a/browser/components/publictaint/src/publicTaint.js b/browser/components/publictaint/src/publicTaint.js
new file mode 100644
--- /dev/null
+++ b/browser/components/publictaint/src/publicTaint.js
@@ -0,0 +1,77 @@
+# -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1999
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Stephen Lamm
+# Robert John Churchill
+# David Hyatt
+# Christopher A. Aillon
+# Myk Melez
+# Pamela Greene
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/TaintInference.jsm");
+
+const TI_CONTRACTID = "@mozilla.org/publictaint;1";
+const TI_CID = Components.ID("{03702bc6-0a02-48b5-ba7b-69e696574f04}");
+const nsIPublicTaint = Components.interfaces.nsIPublicTaint;
+const nsIClassInfo = Components.interfaces.nsIClassInfo;
+
+
+function publicTaint()
+{
+}
+
+publicTaint.prototype.classID = TI_CID;
+
+publicTaint.prototype.p1FastMatch = function (p, s, threshold) {
+ return p1FastMatch(p, s, threshold);
+}
+
+publicTaint.prototype.p1FastMatchReverse = function (s, p, threshold) {
+ return p1FastMatchReverse(s, p, threshold);
+}
+
+
+publicTaint.prototype.classInfo = XPCOMUtils.generateCI({classID: TI_CID,
+ contractID: TI_CONTRACTID,
+ classDescription: "TaintInference",
+ interfaces: [nsIPublicTaint],
+ flags: nsIClassInfo.DOM_OBJECT});
+
+publicTaint.prototype.QueryInterface = XPCOMUtils.generateQI([nsIPublicTaint]);
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([publicTaint]);
diff --git a/browser/components/publictaint/src/publicTaint.manifest b/browser/components/publictaint/src/publicTaint.manifest
new file mode 100644
--- /dev/null
+++ b/browser/components/publictaint/src/publicTaint.manifest
@@ -0,0 +1,4 @@
+component {03702bc6-0a02-48b5-ba7b-69e696574f04} publicTaint.js
+contract @mozilla.org/publictaint;1 {03702bc6-0a02-48b5-ba7b-69e696574f04}
+category JavaScript-global-property taintInference @mozilla.org/publictaint;1
+
diff --git a/browser/locales/en-US/chrome/overrides/appstrings.properties b/browser/locales/en-US/chrome/overrides/appstrings.properties
--- a/browser/locales/en-US/chrome/overrides/appstrings.properties
+++ b/browser/locales/en-US/chrome/overrides/appstrings.properties
@@ -64,5 +64,6 @@
malwareBlocked=The site at %S has been reported as an attack site and has been blocked based on your security preferences.
phishingBlocked=The website at %S has been reported as a web forgery designed to trick users into sharing personal or financial information.
cspFrameAncestorBlocked=This page has a content security policy that prevents it from being embedded in this way.
+xssBlockMode=This page contains an XSS attacks that has been blocked for your security.
corruptedContentError=The page you are trying to view cannot be shown because an error in the data transmission was detected.
remoteXUL=This page uses an unsupported technology that is no longer available by default in Firefox.
diff --git a/browser/locales/en-US/chrome/overrides/netError.dtd b/browser/locales/en-US/chrome/overrides/netError.dtd
--- a/browser/locales/en-US/chrome/overrides/netError.dtd
+++ b/browser/locales/en-US/chrome/overrides/netError.dtd
@@ -169,6 +169,9 @@
&brandShortName; prevented this page from loading in this way because the page has a content security policy that disallows it.
">
+
+&brandShortName; blocked further actions on this page because the page contains injected JavaScript code.">
+
The page you are trying to view cannot be shown because an error in the data transmission was detected.
Please contact the website owners to inform them of this problem.
">
diff --git a/caps/include/nsScriptSecurityManager.h b/caps/include/nsScriptSecurityManager.h
--- a/caps/include/nsScriptSecurityManager.h
+++ b/caps/include/nsScriptSecurityManager.h
@@ -432,6 +432,9 @@
static JSBool
ContentSecurityPolicyPermitsJSAction(JSContext *cx);
+ static JSBool
+ XSSFilterPermitsJSAction(JSContext *cx, JSString *str, XSSJSAction action);
+
// Returns null if a principal cannot be found; generally callers
// should error out at that point.
static nsIPrincipal*
diff --git a/caps/src/nsScriptSecurityManager.cpp b/caps/src/nsScriptSecurityManager.cpp
--- a/caps/src/nsScriptSecurityManager.cpp
+++ b/caps/src/nsScriptSecurityManager.cpp
@@ -584,11 +584,71 @@
fileName,
scriptSample,
lineNum);
+ return evalOK;
}
return evalOK;
}
+JSBool
+nsScriptSecurityManager::XSSFilterPermitsJSAction(JSContext *cx, JSString *str,
+ XSSJSAction action)
+{
+
+ //PR_LOG(gXssPRLog, PR_LOG_DEBUG, (">>>> XSSFilterPermitsJSAction: %d", action));
+
+ // Get the security manager
+ nsScriptSecurityManager *ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+
+ NS_ASSERTION(ssm, "Failed to get security manager service");
+ if (!ssm)
+ return JS_FALSE;
+
+ nsresult rv;
+ nsIPrincipal* subjectPrincipal = ssm->GetSubjectPrincipal(cx, &rv);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "XSS: Failed to get nsIPrincipal from js context");
+ if (NS_FAILED(rv))
+ return JS_FALSE; // Not just absence of principal, but failure.
+
+ if (!subjectPrincipal) {
+ // See bug 553448 for discussion of this case.
+ NS_ASSERTION(!JS_GetSecurityCallbacks(cx)->findObjectPrincipals,
+ "XSS: Should have been able to find subject principal."
+ "Reluctantly granting access.");
+ return JS_TRUE;
+ }
+
+ nsCOMPtr csp;
+ rv = subjectPrincipal->GetCsp(getter_AddRefs(csp));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "CSP: Failed to get CSP from principal.");
+
+ // don't do anything unless there's a CSP
+ if (!csp)
+ return JS_TRUE;
+
+ if (str == NULL) {
+ NS_WARNING("XSS: str is null");
+ return JS_TRUE;
+ }
+ size_t len = 0;
+ const jschar *jschrs = JS_GetStringCharsAndLength(cx, str, &len);
+ NS_ASSERTION(jschrs, "Can this be null?");
+ nsAutoString nsStr(jschrs);
+
+ bool safe = true;
+ rv = csp->PermitsJSAction(nsStr, &safe);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CCSP: PermitsJSAction failed");
+ return JS_TRUE; // fail open to not break sites.
+ }
+
+ return safe;
+}
+
JSBool
nsScriptSecurityManager::CheckObjectAccess(JSContext *cx, JSObject *obj,
@@ -3395,7 +3455,8 @@
CheckObjectAccess,
NULL,
NULL,
- ContentSecurityPolicyPermitsJSAction
+ ContentSecurityPolicyPermitsJSAction,
+ XSSFilterPermitsJSAction
};
#ifdef DEBUG
diff --git a/content/base/public/nsContentErrors.h b/content/base/public/nsContentErrors.h
--- a/content/base/public/nsContentErrors.h
+++ b/content/base/public/nsContentErrors.h
@@ -91,6 +91,10 @@
#define NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION \
NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_SECURITY, 99)
+/* Error codes for XSS Filter */
+#define NS_ERROR_XSS_BLOCK \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_SECURITY, 100)
+
/* Error codes for XBL */
#define NS_ERROR_XBL_BLOCKED \
NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_CONTENT, 15)
diff --git a/content/base/public/nsIContentSecurityPolicy.idl b/content/base/public/nsIContentSecurityPolicy.idl
--- a/content/base/public/nsIContentSecurityPolicy.idl
+++ b/content/base/public/nsIContentSecurityPolicy.idl
@@ -144,6 +144,67 @@
*/
boolean permitsAncestry(in nsIDocShell docShell);
+ // methods to check for XSS attacks. list taken from xssauditor.
+ // 1. inline scripts - called from nsScriptLoader - ok
+ // 2. external scripts - called from shouldLoad - ok
+ // 3. event listener - called from nsEventListenerManager, but only attrs.
+ // are attrs enough? executed only the 1st time event is triggered.
+ // also, when it is called from javascript (obj.click()) the check is not
+ // performed. is it a problem?
+ // 4. javascript url - called from nsJSProtocolHandler - lazy
+ // uses the unescaped version to be closer to param - should be ok
+ // BUG: it does not work inside an iframe, like the mochitest.
+ // seems that shouldLoad is called instead of permitsJSUrl,
+ // why? the bug is not in nsJSProtocolHandler
+ // it worked on vanilla versions because there is no CSP on
+ // default docs! wait for sid's answer. found bug
+ // 5. data urls - they come up as inline scripts in the end, but to read
+ // the unescaped value we need to interpose somewhere else.
+ // We use nsDataChannel::OpenContentStream to lazily deny execution.
+ // Previously, we used
+ // nsGenericHTMLElement::GetURIAttr, but we needed something lazier.
+ // TODO: only check "script" data urls
+ // 6. object - called from shouldLoad - control flow needs work
+ // 7. base element - nsDocument::SetBaseUri interposes completely,
+ // but many times the call would be safe and we are incurring a performance
+ // penalty by checking
+ // 8. eval/setTimeout - dynamic javascript from eval/setTimeout
+
+ // GENERAL TODO LIST:
+
+ // TODO: solve unicode bug - they seem to be more than 1 prunichar
+ // each, even when the utf-16 encoding would be 1 unichar
+ // TODO: implement a real p1MatchReverse in the XPCOM component
+ // for example, this triggers a violation on the page
+ // http://www.rgagnon.com/jsdetails/js-0083.html
+ // b/c it overestimates the match.
+ // IMPROV: extend mozilla to allow multiple csps per page
+ // TODO: fix bug #608131
+ // TODO: support multipart forms in test sjs
+ // TODO: about:home has problem with params.
+ // unfortuantely the error would be in every permits*,
+ // better not to generate any csp for the page from nsDocument.
+ // TODO: checkExternal: when the domain matches, don't try the whole
+ // string. but just the part after the domain. the domain matches
+ // already, we know it!
+ // TODO: the javascript scanner cannot support '/'.
+ // i think it's context-depended and we might need a simple parser.
+
+ // OUTDATED TODO LIST:
+
+ // TODO: see if the FP check can be avoided with other less expensive
+ // heuristics (e.g. save scripts and check later if a script was already
+ // present)
+ // TODO: try with AddScriptBlocker* for XHR bug?
+ boolean permitsInlineScript(in AString script);
+ boolean permitsExternalScript(in nsIURI uri);
+ boolean permitsJSUrl(in AString uri); // uri or script? they are both strings
+ boolean permitsEventListener(in AString script);
+ boolean permitsBaseElement(in nsIURI uri);
+ boolean permitsObject(in nsIURI uri);
+ boolean permitsDataUrl(in nsIURI uri);
+ boolean permitsJSAction(in AString code);
+
/**
* Delegate method called by the service when sub-elements of the protected
* document are being loaded. Given a bit of information about the request,
diff --git a/content/base/src/CSPProfile.jsm b/content/base/src/CSPProfile.jsm
new file mode 100644
--- /dev/null
+++ b/content/base/src/CSPProfile.jsm
@@ -0,0 +1,91 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Content Security Policy data structures.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation
+ *
+ * Contributor(s):
+ * Riccardo Pelizzi
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["ProfDump", "ProfDumpP1Fast", "ProfDumpInit",
+ "ProfDumpFunc", "ProfDumpResult"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/CSPUtils.jsm");
+
+var PROF_MARKER = "###PROFILE_CSP###";
+
+var P1_MARKER = "###P1FAST###";
+var INIT_MARKER = "###INIT###";
+var FUNC_MARKER = "###FUNC###";
+var RESULT_MARKER = "###RESULT###";
+var URL_MARKER = "###URL###"; // this is in browser.js
+var LOAD_MARKER = "###LOAD###" // this is also in browser.js
+
+// warning! the browser.js stuff requires a reboot to see changes
+
+
+function utf8_to_b64( str ) {
+ return btoa(unescape(encodeURIComponent( str )));
+}
+
+function t() { return new Date().getTime(); }
+
+// generic dump
+function ProfDump(msg) {
+ if (!gPrefObserver.profileEnabled) return;
+ dump(PROF_MARKER + " " + msg + "\n");
+}
+
+// p1 dump
+function ProfDumpP1Fast(p, s, url) {
+ if (!gPrefObserver.profileEnabled) return;
+ ProfDump(P1_MARKER + " " + utf8_to_b64(p) + " " + utf8_to_b64(s) +
+ " " + url + " " + t());
+}
+
+// count nr of CSPs created (pages visited)
+function ProfDumpInit(url) {
+ if (!gPrefObserver.profileEnabled) return;
+ ProfDump(INIT_MARKER + " " + url + " " + t());
+}
+
+// permit* call
+function ProfDumpFunc(fname, url) {
+ if (!gPrefObserver.profileEnabled) return;
+ ProfDump(FUNC_MARKER + " " + fname + " " + url + " " + t());
+}
+
+function ProfDumpResult(result, url) {
+ if (!gPrefObserver.profileEnabled) return;
+ ProfDump(RESULT_MARKER + " " + result + " " + url + " " + t());
+}
diff --git a/content/base/src/CSPUtils.jsm b/content/base/src/CSPUtils.jsm
--- a/content/base/src/CSPUtils.jsm
+++ b/content/base/src/CSPUtils.jsm
@@ -43,7 +43,8 @@
// Module stuff
var EXPORTED_SYMBOLS = ["CSPRep", "CSPSourceList", "CSPSource",
- "CSPHost", "CSPWarning", "CSPError", "CSPdebug"];
+ "CSPHost", "CSPWarning", "CSPError", "CSPdebug",
+ "gPrefObserver"];
// these are not exported
@@ -60,6 +61,13 @@
return this._debugEnabled;
},
+ get profileEnabled () {
+ if (!this._branch)
+ this._initialize();
+ return this._profileEnabled;
+ },
+
+
_initialize: function() {
var prefSvc = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService);
@@ -67,6 +75,7 @@
this._branch.QueryInterface(Components.interfaces.nsIPrefBranch2);
this._branch.addObserver("", this, false);
this._debugEnabled = this._branch.getBoolPref("debug");
+ this._profileEnabled = this._branch.getBoolPref("profile");
},
unregister: function() {
@@ -78,6 +87,8 @@
if(aTopic != "nsPref:changed") return;
if(aData === "debug")
this._debugEnabled = this._branch.getBoolPref("debug");
+ else if (aData === "profile")
+ this._profileEnabled = this._branch.getBoolPref("profile");
},
};
@@ -86,6 +97,7 @@
function CSPWarning(aMsg, aSource, aScriptSample, aLineNum) {
var textMessage = 'CSP WARN: ' + aMsg + "\n";
+ dump(textMessage);
var consoleMsg = Components.classes["@mozilla.org/scripterror;1"]
.createInstance(Components.interfaces.nsIScriptError);
consoleMsg.init('CSP: ' + aMsg, aSource, aScriptSample, aLineNum, 0,
@@ -99,6 +111,7 @@
function CSPError(aMsg) {
var textMessage = 'CSP ERROR: ' + aMsg + "\n";
+ dump(textMessage);
var consoleMsg = Components.classes["@mozilla.org/scripterror;1"]
.createInstance(Components.interfaces.nsIScriptError);
consoleMsg.init('CSP: ' + aMsg, null, null, 0, 0,
@@ -113,6 +126,7 @@
if (!gPrefObserver.debugEnabled) return;
aMsg = 'CSP debug: ' + aMsg + "\n";
+ dump(aMsg);
Components.classes["@mozilla.org/consoleservice;1"]
.getService(Components.interfaces.nsIConsoleService)
.logStringMessage(aMsg);
@@ -182,6 +196,7 @@
this._allowEval = false;
this._allowInlineScripts = false;
+ this._blockXSS = false;
// don't auto-populate _directives, so it is easier to find bugs
this._directives = {};
@@ -254,6 +269,8 @@
aCSPR._allowInlineScripts = true;
else if (opt === "eval-script")
aCSPR._allowEval = true;
+ else if (opt === "block-xss")
+ aCSPR._blockXSS = true;
else
CSPWarning("don't understand option '" + opt + "'. Ignoring it.");
}
@@ -454,7 +471,8 @@
if (this._allowEval || this._allowInlineScripts) {
dirs.push("options " + (this._allowEval ? "eval-script" : "")
- + (this._allowInlineScripts ? "inline-script" : ""));
+ + (this._allowInlineScripts ? "inline-script" : "")
+ + (this._blockXSS ? "block-xss" : ""));
}
for (var i in this._directives) {
if (this._directives[i]) {
diff --git a/content/base/src/Makefile.in b/content/base/src/Makefile.in
--- a/content/base/src/Makefile.in
+++ b/content/base/src/Makefile.in
@@ -182,6 +182,8 @@
EXTRA_JS_MODULES = \
CSPUtils.jsm \
+ CSPProfile.jsm \
+ XSSUtils.jsm \
$(NULL)
include $(topsrcdir)/config/config.mk
diff --git a/content/base/src/XSSUtils.jsm b/content/base/src/XSSUtils.jsm
new file mode 100644
--- /dev/null
+++ b/content/base/src/XSSUtils.jsm
@@ -0,0 +1,585 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Content Security Policy data structures.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation
+ *
+ * Contributor(s):
+ * Riccardo Pelizzi
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+MAXDIST = 999999;
+DELCOST = 1;
+INSCOST = 1;
+SUBCOST = 1;
+DELETE = "D";
+INSERT = "I";
+SUBST = "S";
+NONE = "-";
+
+/* general distance threshold */
+THRESHOLD = 0.2;
+/* threshold for domain lookups */
+DOM_THRES = 0.2;
+
+/* minimum lengths to perform checks. both the script and the
+ * parameter length can be bound */
+MIN_INL_LEN = 8;
+MIN_EXT_LEN = 5;
+MIN_PARAM_LEN = 8;
+
+MIN_SCRIPT_LEN = 8;
+MIN_URI_LEN = 5;
+
+// MIN_INL_LEN = 15;
+// MIN_EXT_LEN = 7;
+// MIN_PARAM_LEN = 9;
+
+var TokenType = {
+ OTHER: 0,
+ STRING: 1
+};
+
+//TODO: why are we exporting so many symbols?
+var EXPORTED_SYMBOLS = ["checkInline", "checkExternal", "parseParams",
+ "parseURL", "isSameOrigin", "filterParams", "getHostLimit",
+ "parseMimeParams", "r_unescape"];
+
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+Cu.import("resource://gre/modules/CSPUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/CSPProfile.jsm");
+Cu.import("resource://gre/modules/TaintInference.jsm");
+
+//var filter = Components.classes["@sunysb.edu/filter;1"].getService();
+
+
+// TODO: this is a candidate for C++ in xpcom as well.
+// also, support for / is temporarily dropped.
+function tokenize(script) {
+ var matchChar = '\0';
+ var beg = 0;
+ var i = 0;
+ var tokens = [];
+
+ for (i=0; i < script.length; i++) {
+ if (script[i] == '\\') {
+ i++;
+ continue;
+ }
+ switch (matchChar) {
+ case '\0':
+ if (script[i] == '/' && script[i+1] == '/') {
+ while (script[i] != '\n' && i < script.length - 1)
+ i++;
+ break;
+ }
+ if (script[i] == '\'' || script[i] == '"') {
+ var token = script.substr(beg, i - beg);
+ //CSPdebug("token = " + token);
+ if (beg != i) // can happen at the beginning?
+ tokens.push([beg, i, TokenType.OTHER]);
+ matchChar = script[i];
+ beg = i;
+ }
+ break;
+ case '\'':
+ case '"':
+ //case '/':
+ if (script[i] == matchChar) {
+ var token = script.substr(beg, i - beg + 1);
+ //CSPdebug("token = " + token);
+ tokens.push([beg, i+1, TokenType.STRING]);
+ matchChar = '\0';
+ beg = i + 1;
+ }
+ break;
+ }
+ }
+ if (beg != i) {
+ var token = script.substr(beg, i - beg);
+ //CSPdebug("token = " + token);
+ if (matchChar == '\0')
+ tokens.push([beg, i, TokenType.OTHER]);
+ else
+ tokens.push([beg, i, TokenType.STRING]);
+ }
+
+ return tokens;
+}
+
+/* policy: tokenize the script and do not allow tainted data to break out
+ * of strings or be at the beginning of the script.
+ */
+function findInlineXSS(p, s, tokens, matchFun, uri) {
+ ProfDumpP1Fast(p, s, uri.spec);
+ var matches = matchFun(p, s, THRESHOLD);
+ if (!matches)
+ return false;
+ for (var i = 0; i < matches.length; i++) {
+ var match = matches[i];
+ //CSPdebug("Match in findPinS: " + s.substr(match[0], match[1] - match[0]));
+ // 1st check: tainted string must not be at the beginning of the script
+ if (match[0] == 0) {
+ CSPdebug("Match is at the beginning of script!");
+ return true;
+ }
+ // 2nd check: taint must not break out of strings
+ for(var k = 0; k < tokens.length; k++) {
+ var token = tokens[k];
+ //CSPdebug("token " + token[0] + "-" + token[1] + ", type = " + token[2]);
+ if (
+ ((match[0] < token[0] && match[1] > token[0]) ||
+ (match[0] < token[1] && match[1] > token[1])) &&
+ token[2] == TokenType.STRING
+ ) {
+ CSPdebug("Attack Found!!");
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function getHostLimit(uri) {
+ CSPdebug("Find limit for url: " + uri.asciiSpec);
+ CSPdebug(uri.asciiSpec.substr(uri.prePath.length));
+ return uri.prePath.length;
+}
+
+/* policy: first check if the domain of the url is almost intact in the
+ * parameter. if it is, then try to match the whole url and check if the
+ * attacker can control the host.
+ */
+function findUrlXSS(p, s, matchFun, uri) {
+ CSPdebug("-----------> findUrlXSS");
+ try {
+ var matches = null;
+ // can we really prune everything under this condition?
+ if (s.host.length <= p.length) {
+ ProfDumpP1Fast(p, s.spec, uri.spec);
+ matches = p1FastMatch(s.host, p, DOM_THRES);
+ } else {
+ CSPdebug("Skip check, host '" + s.host + "' is bigger than parameter '" +
+ p + "'");
+ }
+ if (matches) {
+ // TODO: loop over results only for debugging.
+ for (var i = 0; i < matches.length; i++) {
+ var match = matches[0];
+ CSPdebug("Match in findPinS for domain: " +
+ s.host.substr(match[0], match[1] - match[0]));
+ }
+ }
+ else
+ return false;
+ } catch (e) {
+ if (s.scheme != "data") {
+ CSPdebug("###\n###\n###\n Missing Host!");
+ CSPdebug("Url: " + s.asciiSpec);
+ CSPdebug("Scheme: " + s.scheme);
+ }
+ }
+ // check: attacker can only control url path
+ var safeIndex = getHostLimit(s);
+ CSPdebug("Host limit is " + safeIndex);
+ ProfDumpP1Fast(p, s.spec, uri.spec);
+ var matches = matchFun(p, s.spec, THRESHOLD);
+ if (!matches) {
+ // this should not happen, we should at least have a match in the
+ // domain.
+ CSPdebug("Error in findUrlXSS");
+ return false;
+ }
+ for (var i = 0; i < matches.length; i++) {
+ var match = matches[0];
+ CSPdebug("URLXSS Match. Start: " + match[0] + ", End: " + match[1] +
+ ", SafeIndex: " + safeIndex);
+ if (match[0] < safeIndex)
+ return true;
+ }
+ return false;
+}
+
+
+function checkInline(params, script, uri) {
+ CSPdebug("-----------> checkInline");
+
+ if (params.length == 0)
+ return null;
+
+ if (script.length < MIN_SCRIPT_LEN)
+ return null;
+
+ var attacks = [];
+
+ // TODO: tokenize lazily
+ var tokens = tokenize(script);
+ for (i=0; i < params.length; i++) {
+ var param = params[i];
+
+ if (param[1].length < MIN_INL_LEN)
+ continue;
+
+ CSPdebug("Checking for parameter " + param[0]);
+ // should we execute both in some condtions?
+ if (param[1].length < script.length) {
+ // cost of eating chars from script is 0)
+ //CSPdebug("regular match");
+ var isXSS = findInlineXSS(param[1], script, tokens, p1FastMatch, uri);
+ if (isXSS)
+ attacks.push(param[0]);
+ }
+ else {
+ // cost of eating chars from param is 0
+ // CSPdebug("inverse match");
+ var isXSS = findInlineXSS(param[1], script, tokens,
+ p1FastMatchReverse, uri);
+ if (isXSS)
+ attacks.push(param[0]);
+ }
+ }
+ if (attacks.length > 0)
+ return attacks;
+ else
+ return null;
+}
+
+function getDomain(uri) {
+ var p = uri.host.split(".");
+ var rv = p[p.length-2] + "." + p[p.length-1];
+ CSPdebug("Created " + rv);
+ //TODO: need 2-level tlds
+ return rv;
+}
+
+function checkExternal(params, uri, pageUri) {
+ CSPdebug("----------> checkExternal");
+
+ if (params.length == 0)
+ return null;
+
+ if (uri.spec.length < MIN_URI_LEN)
+ return null;
+
+ var attacks = [];
+
+ for (i=0; i < params.length; i++) {
+ var param = params[i];
+
+ if (param[1].length < MIN_EXT_LEN)
+ continue;
+
+ CSPdebug("Checking for parameter " + param[0]);
+ // should we execute both in some condtions?
+ if (param[1].length <= uri.spec.length) {
+ // cost of eating chars from script is 0)
+ //CSPdebug("regular match");
+ //TODO: this should be only for inside injections
+ if (getDomain(uri) == getDomain(pageUri)) {
+ CSPdebug("Lax SOP check returned true, no attack");
+ return null;
+ }
+ var isXSS = findUrlXSS(param[1], uri, p1FastMatch, pageUri);
+ if (isXSS)
+ attacks.push(param[0]);
+ }
+ else {
+ // cost of eating chars from param is 0
+ //CSPdebug("inverse match");
+ if (getDomain(uri) == getDomain(pageUri)) {
+ CSPdebug("Lax SOP check returned true, no attack");
+ return null;
+ }
+ var isXSS = findUrlXSS(param[1], uri, p1FastMatchReverse, pageUri);
+ if (isXSS)
+ attacks.push(param[0]);
+ }
+ }
+ if (attacks.length > 0)
+ return attacks;
+ else
+ return null;
+
+}
+
+function sanitize_param(param) {
+ return param;
+ //return param.replace(/\x00/gm, "");
+}
+
+/* decodes entities */
+function html_decode(input){
+ //TODO: working version of html decode
+ return input;
+}
+
+/* recursive unescape */
+// apparently unescape is not friendly with unicode. test...
+function r_unescape(param) {
+ var prev = null;
+ var cur = param;
+ while (cur != prev) {
+ prev = cur;
+ cur = html_decode(unescape(cur));
+ }
+ return cur;
+}
+
+/* parses a URL into a list of parameters */
+/* uses parseParams to take care of the querystring */
+function parseURL(uri, params) {
+ CSPdebug("----------> parseURL");
+
+ if (params == undefined)
+ params = [];
+
+ try {
+ // happens when you view sources, where else?
+ var url = uri.QueryInterface(Ci.nsIURL);
+ } catch (x) {
+ CSPdebug("Not a nsIURL");
+ return params;
+ }
+
+
+ if (url.query)
+ parseParams(url.query, params);
+ else if (regex_result) {
+ // if there is no param, at least try to match
+ // the part of the request with special html
+ // char. this is already unescaped
+ /* regex picks the biggest substring with html chars */
+ var s_url = sanitize_param(unescape(url.spec));
+ var regex = new RegExp("[\"'<>].*[\"'<>]");
+ var regex_result = regex.exec(s_url);
+ var param = regex_result[0];
+
+ params.push(["HTML", param]);
+
+ }
+
+ // add the fragment
+ if (url.ref) {
+ var a_param_name = "FRAGMENT";
+ var a_param_value = sanitize_param(r_unescape(url.ref));
+ params.push([a_param_name, a_param_value]);
+ }
+
+ // we also need the path! otherwise, DOM-based XSS attacks using the URL
+ // would be able to split the content in two parameters
+ if (uri.path && uri.path.length > 2 && uri.path != "/")
+ params.push(["PATH", r_unescape(uri.path.substr(1))]);
+
+ //CSPdebug("parseURL params: " + params);
+ return params;
+};
+
+
+/* parses a querystring into a list of parameters */
+function parseParams(url_params, params) {
+ CSPdebug("--------> parseParam");
+
+ if (params == undefined)
+ params = [];
+
+ var url_parts;
+ var delimiter;
+
+ /* careful: we need to print the urlencoded version, but
+ * splitting should happen on the original url. otherwise, we
+ * risk splitting on escaped & and ; UPDATE: asciiSpec
+ * sometimes contains & and ; anyway :-/ Seems to be against the HTTP
+ * specs AND mozilla's specs */
+ var unknownCount = 1;
+
+
+ /* heuristic to choose param delimiter */
+ if (url_params.indexOf('&') == -1 && url_params.indexOf(';') != -1 &&
+ (url_params.length - url_params.replace(/=/g, "").length) > 1)
+ delimiter = ';';
+ else
+ delimiter = '&';
+
+ //CSPdebug("url_params = " + url_params);
+ // if we used the ; heuristic (unsafe), also add the parameter
+ // with the & heuristic,
+ // by simply considering the rest of the URI as one param.
+ if (delimiter == ';') {
+
+ var equal_index = url_params.indexOf('=');
+ if(equal_index != -1) {
+ // split on the first equal sign
+ var a_param_name = "alt_" + sanitize_param(unescape(
+ url_params.substr(0, equal_index)));
+ var a_param_value = sanitize_param(r_unescape(
+ url_params.substr(equal_index + 1)));
+
+ params.push([a_param_name, a_param_value]);
+
+ } else {
+
+ // consider the whole string as a parameter
+ var a_param_name = "unknown" + String(unknownCount++);
+ var a_param_value = sanitize_param(r_unescape(url_params));
+
+ params.push([a_param_name, a_param_value]);
+ }
+
+ }
+
+ var params_array = url_params.split(delimiter);
+
+ for (var i=0; i < params_array.length; i++) {
+ var equal_index = params_array[i].indexOf('=');
+ //CSPdebug("params_array[i] = " + params_array[i]);
+ if(equal_index != -1) {
+ // split on the first equal sign
+ var a_param_name = sanitize_param(unescape(
+ params_array[i].substr(0, equal_index)));
+ //CSPdebug("param name = " + a_param_name);
+ var a_param_value = sanitize_param(r_unescape(
+ params_array[i].substr(equal_index + 1)));
+ //CSPdebug("param value = " + a_param_value);
+ params.push([a_param_name, a_param_value]);
+ } else {
+
+ // consider the whole string as a parameter
+ if (params_array[i] != "") {
+ var a_param_name = "unknown" + String(unknownCount++);
+ var a_param_value = sanitize_param(r_unescape(params_array[i]));
+ params.push([a_param_name, a_param_value]);
+ }
+ }
+
+ }
+
+ //CSPdebug("parseParams params: " + params);
+ return params;
+
+};
+
+/* checks if two urls have the same origin */
+function isSameOrigin(a, b) {
+ if (a == undefined || b == undefined) {
+ CSPdebug("one of the urls is invalid");
+ return false;
+ }
+ return (a.scheme == b.scheme &&
+ a.host == b.host &&
+ a.port == b.port);
+}
+
+/* checks if the filter contains any character other than [a-zA-Z0-9],
+ * spaces, underscore, dashes and forward slashes */
+// \w includes the underscore.
+function isDangerous(param) {
+ return param[1].length > MIN_PARAM_LEN &&
+ !/^[\w\t\.\s-]+$/.test(param[1]);
+}
+
+function isPathDangerous(param) {
+ return param[1].length > MIN_PARAM_LEN &&
+ !/^[\w\t\.\s-\/]+$/.test(param[1]);
+}
+
+
+
+// heuristic: if there are no params, check for special chars in path
+// [/ is safe].
+// if there are params, filter the params which do not contain
+// special characters.
+// if two or more params are left, keep path:
+// check the PATH: if special chars are only after the
+// querystring, keep only that part.
+//
+
+function filterParams(params) {
+ if (params.length == 1 && params[0][0] == "PATH")
+ return params.filter(
+ function (p) { return isPathDangerous(p); }
+ );
+
+ var result = params.filter(
+ function (p) {
+ return isDangerous(p) && p[0] != "PATH";
+ }
+ );
+ if (result.length < 2)
+ return result;
+ else {
+ var pathArray = params.filter(
+ function (p) { return p[0] == "PATH"; }
+ );
+ if (pathArray.length == 0)
+ return result;
+ var path = pathArray[0][1];
+ var leftPath = path.substr(0, path.indexOf("?"));
+ var rightPath = path.substr(path.indexOf("?") + 1);
+ if (isPathDangerous([null, leftPath]))
+ result.push(["PATH", path]);
+ else
+ // the right part has to be dangerous...
+ result.push(["QSTRING", rightPath]);
+ return result;
+ }
+}
+
+function parseMimeParams(data, params) {
+ var regex = /boundary=(-+.*)/;
+ var boundary = regex.exec(data)[1];
+ if (boundary == "") {
+ CSPdebug("Cannot parse MIME post params");
+ return;
+ }
+ data = data.substr(data.indexOf("\r\n\r\n") + 4);
+ var fields = data.split(boundary + "\r\n");
+ for (var i = 0; i < fields.length; i++) {
+ fields[i] = fields[i].replace(boundary + "--", "");
+ CSPdebug("Field: " + fields[i] + "\n-+-");
+ regex = /Content-Disposition:.+; name="(.*?)"(?:; filename="(.*?)")?/;
+ var result = regex.exec(fields[i]);
+ if (result == null) {
+ CSPdebug("Cannot parse field");
+ continue;
+ }
+ var name = result[1];
+ var filename = result[2];
+ if (filename == undefined) {
+ var content = fields[i].split("\r\n\r\n")[1];
+ CSPdebug(name + ": " + content);
+ params.push([name, content]);
+ } else {
+ CSPdebug(name + ": " + filename);
+ params.push([name, filename]);
+ }
+ }
+};
\ No newline at end of file
diff --git a/content/base/src/contentSecurityPolicy.js b/content/base/src/contentSecurityPolicy.js
--- a/content/base/src/contentSecurityPolicy.js
+++ b/content/base/src/contentSecurityPolicy.js
@@ -51,10 +51,14 @@
const Cu = Components.utils;
const CSP_VIOLATION_TOPIC = "csp-on-violate-policy";
+const XSS_VIOLATION_TOPIC = "xss-on-violate-policy";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/CSPUtils.jsm");
+Cu.import("resource://gre/modules/XSSUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/CSPProfile.jsm");
/* ::::: Policy Parsing & Data structures :::::: */
@@ -70,6 +74,23 @@
this._request = "";
this._docRequest = null;
+
+ // this is useful for two things:
+ // 1) check domains only once when they get hit twice
+ // because of speculative parsing.
+ // 2) policy: if you accepted scripting content from one domain, you
+ // implicitly trust it.
+ this._shouldLoadCache = {};
+
+ // no reason to observe value... switching off the protection in the
+ // middle of a page load does not make much sense
+ var prefSvc = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService);
+ this._branch = prefSvc.getBranch("security.csp.");
+ this._policy._blockXSS = this._branch.getBoolPref("xss");
+ this._policy._blockModeXSS = this._branch.getBoolPref("xssblock");
+ this._policy._debug = this._branch.getBoolPref("debug");
+
CSPdebug("CSP POLICY INITED TO 'default-src *'");
}
@@ -210,6 +231,106 @@
var reqVersion = internalChannel.getRequestVersion(reqMaj, reqMin);
this._request += " HTTP/" + reqMaj.value + "." + reqMin.value;
}
+
+ try {
+ var header = aChannel.getResponseHeader("X-XSS-Protection");
+ CSPdebug("Header: " + header);
+ var pos = 0;
+ var len = header.length;
+ var skipWhiteSpace = function(s) {
+ while (pos != len && s[pos] <= ' ')
+ ++pos;
+ return pos != len;
+ };
+ var skipToken = function(str, token) {
+ var tokenPos = 0;
+ var tokenLen = token.length;
+
+ while (pos != len && tokenPos < tokenLen) {
+ if (str[pos].toLowerCase() != token[tokenPos++])
+ return false;
+ ++pos;
+ }
+ return true;
+ };
+ if (header.length == 0) {
+ CSPdebug("Header enables XSS");
+ this._policy._blockXSS &= true;
+ }
+ else if (header[0] == '0') {
+ CSPdebug("Header disables XSS");
+ this._policy._blockXSS = false;
+ }
+ else if (header[pos++] == '1'
+ && skipWhiteSpace(header)
+ && header[pos++] == ';'
+ && skipWhiteSpace(header)
+ && skipToken(header, "mode")
+ && skipWhiteSpace(header)
+ && header[pos++] == '='
+ && skipWhiteSpace(header)
+ && skipToken(header, "block")
+ && pos == len) {
+ CSPdebug("Header enables block mode");
+ this._policy._blockXSS &= true;
+ this._policy._blockModeXSS = true;
+ }
+ } catch (e) {
+ //CSPdebug("No XSS header, or exception: " + e);
+ }
+
+ // no need to parse url if we will not use it (still parse it for debug)
+ if (!this._policy._blockXSS && !this._policy._debug)
+ return;
+
+ ProfDumpInit(aChannel.URI.spec);
+
+ // save the URL for origin checks
+ this._policy._uri = aChannel.URI;
+
+ // save the referrer for xss notification
+ this._policy._referrer = aChannel.referrer;
+
+ // grab HTTP GET parameters
+ this._fullParams = [];
+ parseURL(aChannel.URI, this._fullParams);
+ // grab POST parameters
+ // is there anything in mozilla to do this parsing for us? my mime
+ // parser is rather fragile.
+ if (aChannel.requestMethod == "POST") {
+ //TODO: catch the exception?
+ var uploadChannel = aChannel.QueryInterface(Ci.nsIUploadChannel);
+ var uploadStream = uploadChannel.uploadStream;
+ // rewind the stream;
+ var seekStream = uploadStream.QueryInterface(Ci.nsISeekableStream);
+ CSPdebug(seekStream);
+ seekStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+ CSPdebug(uploadStream.available());
+ var data = NetUtil.readInputStreamToString(uploadStream,
+ uploadStream.available());
+ CSPdebug(data);
+ var separator = data.indexOf("\r\n\r\n");
+ var urlencoded = data.indexOf("application/x-www-form-urlencoded");
+ var multipart = data.indexOf("multipart/form-data");
+
+ if (urlencoded != -1 && urlencoded < separator ) {
+ var uri_params = data.substr(separator+4);
+ //CSPdebug("POST uri_params = " + uri_params);
+ parseParams(uri_params.replace(/\+/g, ' '), this._fullParams);
+ } else if (multipart != -1 && multipart < separator) {
+ parseMimeParams(data, this._fullParams);
+ }
+ //var stream = uploadChannel.getUploadStream();
+ } else {
+ //CSPdebug("This is not a POST request");
+ }
+
+ // filter params if they can't be malicious
+ CSPdebug("final param list before filtering: " + this._fullParams);
+ this._params = filterParams(this._fullParams);
+ CSPdebug("final param list after filtering: " + this._params);
+
+
},
/* ........ Methods .............. */
@@ -273,7 +394,7 @@
blockedUri.asciiSpec : blockedUri),
'violated-directive': violatedDirective
}
- }
+ };
// extra report fields for script errors (if available)
if (aSourceFile)
report["csp-report"]["source-file"] = aSourceFile;
@@ -323,6 +444,21 @@
}
},
+ /* shorthand method to notify observers */
+ notifyViolation:
+ function(violation, details) {
+
+ // gotta wrap the violation string, since it's sent out to observers as
+ // an nsISupports.
+ let wrapper = Cc["@mozilla.org/supports-cstring;1"]
+ .createInstance(Ci.nsISupportsCString);
+ wrapper.data = details;
+ this._observerService.notifyObservers(
+ wrapper,
+ CSP_VIOLATION_TOPIC,
+ violation);
+ },
+
/**
* Exposed Method to analyze docShell for approved frame ancestry.
* Also sends violation reports if necessary.
@@ -377,6 +513,250 @@
},
/**
+ * Uses taint inference to check for inline XSS attacks
+ */
+ permitsInlineScript:
+ function(script) {
+ /* this seems to be necessary to catch CSP calls on some chrome
+ * pages which i am not able to avoid otherwise. uri is undefined
+ * and throws an exception... there is one check for each permits*
+ * function */
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("inline", this._policy._uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ if (script.length < 50)
+ CSPdebug("script:\n---\n" + script + "\n---\n");
+
+ var attacks = checkInline(this._params, script, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+ CSPdebug("--------> SUSPECT ATTACK!");
+ CSPdebug("offending script:\n---\n" + script + "\n---");
+ CSPdebug("attack Parameters: " + attacks);
+
+ this._reportXSSViolation("Inline Script", script, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+ },
+
+ permitsExternalScript:
+ function(uri) {
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("external", this._policy._uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ CSPdebug("Url: " + uri.spec);
+
+ var attacks = checkExternal(this._params, uri, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+
+ this._reportXSSViolation("External Script", uri.spec, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+ },
+
+ /* unescape the url and then use the policy for inline scripts */
+ permitsJSUrl:
+ function(uri) {
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("jsurl", this._policy._uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ script = r_unescape(uri);
+
+ if (script.length < 50)
+ CSPdebug("js url:\n---\n" + script + "\n---");
+
+
+ var attacks = checkInline(this._params, script, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+
+ CSPdebug("--------> SUSPECT ATTACK!");
+
+ this._reportXSSViolation("JS URL", uri.spec, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+
+ },
+
+ /* use the policy for inline scripts */
+ permitsEventListener:
+ function(script) {
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("event", this._policy._uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ CSPdebug("script:\n---\n" + script + "\n---");
+
+
+ //CSPdebug("params passed to checkInline: " + this._params);
+ var attacks = checkInline(this._params, script, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+ CSPdebug("--------> SUSPECT ATTACK!");
+ CSPdebug("offending event:\n---\n" + script + "\n---");
+
+ this._reportXSSViolation("Event Listener", script, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+
+ },
+
+ /* uses the policy for external scripts */
+ permitsBaseElement:
+ function(uri) {
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("base", this._policy._uri.spec);
+ CSPdebug("Url: " + uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ var attacks = checkExternal(this._params, uri, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+
+ CSPdebug("--------> SUSPECT ATTACK!");
+
+ this._reportXSSViolation("Base Element", uri.spec, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+ },
+
+ /* uses policy for external scripts */
+ permitsObject:
+ function(uri) {
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("object", this._policy._uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ CSPdebug("Url: " + uri.spec);
+
+ var attacks = checkExternal(this._params, uri, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+
+ CSPdebug("--------> SUSPECT ATTACK!");
+
+ this._reportXSSViolation("Object", uri.spec, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+
+ },
+
+ /* we do not try to decode base64, we use the url as it is. it would
+ * be very rare for a web application to encode a parameter in base64.
+ * uses policy from checkInline. the decoded text surfaces as an inline
+ * script later anyway */
+ // do we need to parse dataURLs and check if attacker has control
+ // of the body? i think it is only needed if the webapp lets the user
+ // control the body alone, to increase the string similarity.
+ permitsDataUrl:
+ function(uri) {
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("dataurl", this._policy._uri);
+ CSPdebug("Url: " + uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ var attacks = checkInline(this._params, uri.spec, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+
+ CSPdebug("--------> SUSPECT ATTACK!");
+
+ this._reportXSSViolation("Data URL", uri.spec, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+ },
+
+ permitsJSAction:
+ function(code) {
+ CSPdebug("PERMITJSACTION!!!!");
+ if (this._policy._uri == undefined)
+ return true;
+
+ ProfDumpFunc("jsaction", this._policy._uri.spec);
+
+ if (!this._policy._blockXSS)
+ return true;
+
+ if (code.length < 50)
+ CSPdebug("js code:\n---\n" + code + "\n---\n");
+
+ var attacks = checkInline(this._params, code, this._policy._uri);
+
+ if (attacks == null) {
+ ProfDumpResult("Safe", this._policy._uri.spec);
+ return true;
+ }
+ CSPdebug("--------> SUSPECT ATTACK!");
+ CSPdebug("offending code:\n---\n" + code + "\n---");
+ CSPdebug("attack Parameters: " + attacks);
+
+ this._reportXSSViolation("JSAction", code, attacks,
+ this._params);
+ ProfDumpResult("Attack", this._policy._uri.spec);
+ return this._reportOnlyMode;
+ },
+
+ /**
* Delegate method called by the service when sub-elements of the protected
* document are being loaded. Given a bit of information about the request,
* decides whether or not the policy is satisfied.
@@ -391,13 +771,31 @@
// don't filter chrome stuff
if (aContentLocation.scheme === 'chrome' ||
- aContentLocation.scheme === 'resource') {
+ aContentLocation.scheme === 'resource' ||
+ aContentLocation.scheme === 'moz-icon' ||
+ aContentLocation.scheme === 'about' ||
+ aContentLocation.scheme === 'data') {
return Ci.nsIContentPolicy.ACCEPT;
}
+ // even after filtering about urls, i still get errors
+ try {
+ var exc = aContentLocation.host;
+ } catch (x) {
+ CSPdebug("Giving up on URL " + aContentLocation.spec);
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ // keep a cache of checks, because xss checks are expensive.
+ // use also contentType as key
+ if (aContentType + aContentLocation.host in this._shouldLoadCache) {
+ CSPdebug("Host: " + aContentLocation.host + " has been checked already");
+ return this._shouldLoadCache[aContentType + aContentLocation.host];
+ }
+
// interpret the context, and then pass off to the decision structure
- CSPdebug("shouldLoad location = " + aContentLocation.asciiSpec);
- CSPdebug("shouldLoad content type = " + aContentType);
+ //CSPdebug("shouldLoad location = " + aContentLocation.asciiSpec);
+ //CSPdebug("shouldLoad content type = " + aContentType);
var cspContext = ContentSecurityPolicy._MAPPINGS[aContentType];
// if the mapping is null, there's no policy, let it through.
@@ -411,6 +809,52 @@
? Ci.nsIContentPolicy.ACCEPT
: Ci.nsIContentPolicy.REJECT_SERVER;
+ // allow all same origin loads
+ // but only for xss (respect CSP's reject)
+ if (res == Ci.nsIContentPolicy.ACCEPT &&
+ isSameOrigin(aContentLocation, this._policy._uri)) {
+ CSPdebug("Url: " + aContentLocation.asciiSpec);
+ CSPdebug("is same origin, skip");
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ // only call permitsExternal if CSP accepts the url
+ if (res == Ci.nsIContentPolicy.ACCEPT &&
+ this._policy._blockXSS &&
+ aContentType == Ci.nsIContentPolicy.TYPE_SCRIPT) {
+ //CSPdebug("Type = Script, checking URL");
+ //CSPdebug(aContentLocation.asciiSpec);
+
+ var safe = this.permitsExternalScript(aContentLocation);
+ if (!safe) {
+ CSPdebug("External XSS detected!!!");
+ // don't fall through, permitsExternal handles reporting
+ // currently, both positive and negative responses are cached.
+ this._shouldLoadCache[aContentType + aContentLocation.host] =
+ Ci.nsIContentPolicy.REJECT_SERVER;
+ return Ci.nsIContentPolicy.REJECT_SERVER;
+ }
+
+ }
+
+ /* object policy */
+ if (res == Ci.nsIContentPolicy.ACCEPT &&
+ this._policy._blockXSS &&
+ aContentType == Ci.nsIContentPolicy.TYPE_OBJECT) {
+ //CSPdebug("Type = Object, checking URL");
+ //CSPdebug(aContentLocation.asciiSpec);
+
+ var safe = this.permitsObject(aContentLocation);
+ if (!safe) {
+ CSPdebug("Object XSS detected!!!");
+ // useless to let it go through because the report
+ // would be wrong
+ this._shouldLoadCache[aContentType + aContentLocation.host] =
+ Ci.nsIContentPolicy.REJECT_SERVER;
+ return Ci.nsIContentPolicy.REJECT_SERVER;
+ }
+ }
+
// frame-ancestors is taken care of early on (as this document is loaded)
// If the result is *NOT* ACCEPT, then send report
@@ -427,7 +871,12 @@
}
}
- return (this._reportOnlyMode ? Ci.nsIContentPolicy.ACCEPT : res);
+ // save the result of the check for later use
+ // TODO: this might screw with CSPs, change later to only save xss stuff
+ var result = this._reportOnlyMode ? Ci.nsIContentPolicy.ACCEPT : res;
+ this._shouldLoadCache[aContentType + aContentLocation.host] =
+ result;
+ return result;
},
shouldProcess:
@@ -443,6 +892,45 @@
return res;
},
+ /* wraps an array of arguments for the csp observer */
+ _reportXSSViolation:
+ function(policy, script, attacks, params) {
+
+ // dump it to console as well, with more info
+ CSPdebug("\n\n\n\n\n%^%%^^%^%%^%%^%^ Policy Violation: " + policy);
+ CSPdebug("page: " + this._policy._uri.spec);
+ CSPdebug("script/url: " + script.substr(0, 1000))
+ CSPdebug("involved params: " + attacks);
+ CSPdebug("Parameters: ");
+ for (var i = 0; i < params.length; i++) {
+ CSPdebug(params[i][0] + ": " + params[i][1]);
+ CSPdebug("--------");
+ }
+
+ var data = {
+ block: this._policy._blockModeXSS,
+ url: this._policy._uri.spec,
+ policy: policy,
+ script: script,
+ params: params,
+ filter: "xssfilter",
+ referrer: this._policy._referrer && this._policy._referrer.spec
+ }
+ data.wrappedJSObject = data;
+
+ Services.tm.mainThread.dispatch(
+ function() {
+ Services.obs.notifyObservers(data, XSS_VIOLATION_TOPIC, null);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+
+ // block the page is block mode is enabled
+ if (this._policy._blockModeXSS) {
+ CSPdebug("canceling request " + this._docRequest);
+ // NS_ERROR_XSS_BLOCK is not exposed to js, so we enter its value
+ this._docRequest.cancel(2153381988);
+ }
+ },
+
/**
* Asynchronously notifies any nsIObservers listening to the CSP violation
* topic that a violation occurred. Also triggers report sending. All
diff --git a/content/base/src/nsCSPService.cpp b/content/base/src/nsCSPService.cpp
--- a/content/base/src/nsCSPService.cpp
+++ b/content/base/src/nsCSPService.cpp
@@ -60,6 +60,8 @@
/* Keeps track of whether or not CSP is enabled */
bool CSPService::sCSPEnabled = true;
+bool CSPService::sXSSEnabled = true;;
+bool CSPService::sDebugEnabled = false;
#ifdef PR_LOGGING
static PRLogModuleInfo* gCspPRLog;
@@ -68,6 +70,8 @@
CSPService::CSPService()
{
Preferences::AddBoolVarCache(&sCSPEnabled, "security.csp.enable");
+ Preferences::AddBoolVarCache(&sXSSEnabled, "security.csp.xss");
+ Preferences::AddBoolVarCache(&sDebugEnabled, "security.csp.debug");
#ifdef PR_LOGGING
if (!gCspPRLog)
diff --git a/content/base/src/nsCSPService.h b/content/base/src/nsCSPService.h
--- a/content/base/src/nsCSPService.h
+++ b/content/base/src/nsCSPService.h
@@ -55,5 +55,5 @@
CSPService();
virtual ~CSPService();
- static bool sCSPEnabled;
+ static bool sCSPEnabled, sXSSEnabled, sDebugEnabled;
};
diff --git a/content/base/src/nsDocument.cpp b/content/base/src/nsDocument.cpp
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -196,6 +196,7 @@
#include "nsCSPService.h"
#include "nsHTMLStyleSheet.h"
#include "nsHTMLCSSStyleSheet.h"
+#include "nsIContentPrefService.h" //TODO: not sure if useful
#include "mozilla/dom/Link.h"
#include "nsIHTMLDocument.h"
@@ -2403,13 +2404,16 @@
return NS_OK;
}
- if (cspHeaderValue.IsEmpty() && cspROHeaderValue.IsEmpty()) {
- // no CSP header present, stop processing
+ if (cspHeaderValue.IsEmpty() && cspROHeaderValue.IsEmpty() &&
+ !CSPService::sXSSEnabled && !CSPService::sDebugEnabled) {
+ // no CSP neither XSS protection present, stop processing.
+ // but in csp debug mode we apply an empty policy anyway.
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("No CSP for this doc"));
return NS_OK;
}
#ifdef PR_LOGGING
- PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP header specified for document %p", this));
+ //PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP header specified for document %p", this));
#endif
nsresult rv;
@@ -2427,44 +2431,48 @@
nsCOMPtr httpChannel = do_QueryInterface(mChannel);
mCSP->ScanRequestData(httpChannel);
- // Start parsing the policy
- nsCOMPtr chanURI;
- mChannel->GetURI(getter_AddRefs(chanURI));
+ // only attempt to parse if there is actually a header. otherwise,
+ // we just keep the defaults: block XSS, allow eval and inline
+ // scripts.
+ if (!cspHeaderValue.IsEmpty() || !cspROHeaderValue.IsEmpty()) {
+ // Start parsing the policy
+ nsCOMPtr chanURI;
+ mChannel->GetURI(getter_AddRefs(chanURI));
#ifdef PR_LOGGING
- PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP Loaded"));
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP Loaded"));
#endif
- // ReportOnly mode is enabled *only* if there are no regular-strength CSP
- // headers present. If there are, then we ignore the ReportOnly mode and
- // toss a warning into the error console, proceeding with enforcing the
- // regular-strength CSP.
- if (cspHeaderValue.IsEmpty()) {
- mCSP->SetReportOnlyMode(true);
- mCSP->RefinePolicy(cspROHeaderValue, chanURI);
+ // ReportOnly mode is enabled *only* if there are no regular-strength CSP
+ // headers present. If there are, then we ignore the ReportOnly mode and
+ // toss a warning into the error console, proceeding with enforcing the
+ // regular-strength CSP.
+ if (cspHeaderValue.IsEmpty()) {
+ mCSP->SetReportOnlyMode(true);
+ mCSP->RefinePolicy(cspROHeaderValue, chanURI);
#ifdef PR_LOGGING
- {
- PR_LOG(gCspPRLog, PR_LOG_DEBUG,
- ("CSP (report only) refined, policy: \"%s\"",
+ {
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG,
+ ("CSP (report only) refined, policy: \"%s\"",
NS_ConvertUTF16toUTF8(cspROHeaderValue).get()));
+ }
+#endif
+ } else {
+ //XXX(sstamm): maybe we should post a warning when both read only and regular
+ // CSP headers are present.
+ mCSP->RefinePolicy(cspHeaderValue, chanURI);
+#ifdef PR_LOGGING
+ {
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG,
+ ("CSP refined, policy: \"%s\"",
+ NS_ConvertUTF16toUTF8(cspHeaderValue).get()));
+ }
+#endif
}
-#endif
- } else {
- //XXX(sstamm): maybe we should post a warning when both read only and regular
- // CSP headers are present.
- mCSP->RefinePolicy(cspHeaderValue, chanURI);
-#ifdef PR_LOGGING
- {
- PR_LOG(gCspPRLog, PR_LOG_DEBUG,
- ("CSP refined, policy: \"%s\"",
- NS_ConvertUTF16toUTF8(cspHeaderValue).get()));
- }
-#endif
- }
-
- // Check for frame-ancestor violation
- nsCOMPtr docShell = do_QueryReferent(mDocumentContainer);
- if (docShell) {
+
+ // Check for frame-ancestor violation
+ nsCOMPtr docShell = do_QueryReferent(mDocumentContainer);
+ if (docShell) {
bool safeAncestry = false;
// PermitsAncestry sends violation reports when necessary
@@ -2473,13 +2481,18 @@
if (!safeAncestry) {
#ifdef PR_LOGGING
- PR_LOG(gCspPRLog, PR_LOG_DEBUG,
- ("CSP doesn't like frame's ancestry, not loading."));
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG,
+ ("CSP doesn't like frame's ancestry, not loading."));
#endif
- // stop! ERROR page!
- mChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION);
+ // stop! ERROR page!
+ mChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION);
}
- }
+ }
+
+ } // closes "if headers are provided"
+ // we also skipped the frame ancestry test for empty policy.. but
+ // with the default policy, it can never fail
+
//Copy into principal
nsIPrincipal* principal = GetPrincipal();
@@ -2949,6 +2962,22 @@
}
}
+ // csp check
+ nsCOMPtr csp;
+ nsresult rv = NodePrincipal()->GetCsp(getter_AddRefs(csp));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (csp) {
+ nsresult rv;
+ bool safe;
+ rv = csp->PermitsBaseElement(aURI, &safe);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!safe) {
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP blocked XSS attack"));
+ return NS_OK;
+ }
+ }
+
+
if (aURI) {
mDocumentBaseURI = NS_TryToMakeImmutable(aURI);
} else {
diff --git a/content/base/src/nsScriptLoader.cpp b/content/base/src/nsScriptLoader.cpp
--- a/content/base/src/nsScriptLoader.cpp
+++ b/content/base/src/nsScriptLoader.cpp
@@ -76,13 +76,14 @@
#include "nsChannelPolicy.h"
#include "nsCRT.h"
#include "nsContentCreatorFunctions.h"
-
+#include
#include "mozilla/FunctionTimer.h"
#ifdef PR_LOGGING
static PRLogModuleInfo* gCspPRLog;
#endif
+using namespace std;
using namespace mozilla::dom;
//////////////////////////////////////////////////////////////
@@ -698,6 +699,29 @@
aElement->GetScriptLineNumber());
return NS_ERROR_FAILURE;
}
+
+ nsAutoString textData;
+ bool safe;
+ // is this making a copy of the string? our function is constant, we
+ // should use a reference
+ aElement->GetScriptText(textData);
+ // TODO: ensure success failed?
+ // it's not p1matchc++ fault, and not triggered by a special string.
+ rv = csp->PermitsInlineScript(textData, &safe);
+ if (NS_FAILED(rv)) {
+ cout << "PermitsInlineScript failed for string:\n";
+ cout << NS_ConvertUTF16toUTF8(textData).get() << endl;
+ cout << "@@@ End of String\n";
+ rv = csp->PermitsInlineScript(textData, &safe);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!safe) {
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP blocked XSS attack"));
+ return NS_ERROR_FAILURE;
+ }
+
+
}
request = new nsScriptLoadRequest(aElement, version);
@@ -740,6 +764,11 @@
// there's no document.write currently on the call stack. However,
// this way matches IE more closely than checking if document.write
// is on the call stack.
+ // TODO: fix the race condition and remove this!
+ // useful to place a breakpoint
+ if (!nsContentUtils::IsSafeToRunScript())
+ PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Oops"));
+
NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),
"Not safe to run a parser-inserted script?");
return ProcessRequest(request);
diff --git a/content/base/test/Makefile.in b/content/base/test/Makefile.in
--- a/content/base/test/Makefile.in
+++ b/content/base/test/Makefile.in
@@ -520,6 +520,12 @@
test_bug692434.html \
file_bug692434.xml \
test_bug693875.html \
+ test_XSS.html \
+ file_XSS.js \
+ file_XSS.sjs \
+ file_XSS_title.js \
+ file_XSS_title.jpg \
+ file_XSS_binding.xml \
$(NULL)
_CHROME_FILES = \
diff --git a/content/base/test/file_XSS.js b/content/base/test/file_XSS.js
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_XSS.js
@@ -0,0 +1,305 @@
+netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const env = Cc["@mozilla.org/process/environment;1"].
+ getService(Ci.nsIEnvironment);
+// we use this for clarity
+var GET = "GET", POST = "POST";
+var ATTACK = "ATTACK", ATTACK_BLOCK = "ATTACK_BLOCK", SAFE = "SAFE";
+var TODO = "TODO", DONE = "DONE";
+var NONE = "none";
+// format:
+// method, description, server context, content, header, encoding
+// (form only), attack, todo
+
+var TESTS = [
+ [GET, "partial injection inline", "inside",
+ 'abc\'; parent.postMessage("xss", "*");//', NONE, NONE, ATTACK, DONE],
+
+ [GET, "whole script injection", "outside",
+ '', NONE, NONE, ATTACK, DONE],
+ [GET, "whole script with HTML quirk", "outside",
+ '">',
+ NONE, NONE, ATTACK, DONE],
+
+ [GET, "external script w/ quirk", "outside",
+ '', NONE, NONE, ATTACK, DONE],
+ [GET, "script tag w/ quirk", "outside",
+ '<', NONE, NONE, ATTACK, DONE],
+ // '<' does not work on windows
+ // ["external_outside5_bad", "outside_script",
+ // '',
+ NONE, NONE, ATTACK, DONE],
+ /*[GET, "iframe src w/ jsurl", "outside",
+ '', NONE, NONE, ATTACK, DONE],*/
+ [GET, "svg object", "outside",
+ '', NONE, NONE, ATTACK, DONE],
+ [GET, "external w/ fake jpg", "outside",
+ '', NONE, NONE, ATTACK, DONE],
+ [GET, "external script w/ quirk", "outside",
+ '', NONE, NONE, ATTACK, DONE],
+ [GET, "whole script w/ doc.write", "outside",
+ 'PT SRC="http://example.org/tests/content/base/test/file_XSS_title.js">', NONE, NONE, ATTACK, DONE],
+ [GET, "inline_outside_esc_bad", "outside_esc",
+ '', NONE, NONE, ATTACK, DONE],
+ [GET, "handler_plain_bad", "handler_plain",
+ 'parent.postMessage("xss", "*");', NONE, NONE, ATTACK, DONE],
+ [GET, "handler_inside_bad", "handler_inside",
+ '= 3; parent.postMessage("xss", "*"); //', NONE, NONE, ATTACK, DONE],
+ [GET, "external_outside_bad", "outside",
+'', NONE, NONE, ATTACK, DONE],
+ [GET, "benign external script", "external_path",
+ "file_XSS_title.js", NONE, NONE, SAFE, DONE],
+ // p larger than s?
+ [GET, "external w/o http", "external_domain",
+ "example.org/tests/content/base/test/file_XSS_title.js",
+ NONE, NONE, ATTACK, DONE],
+ /* [GET, "jsurl in frame", "html_nobody",
+ '', NONE, NONE, ATTACK, DONE],
+ [GET, "jsurl with html entities/utf8", "outside",
+ '',
+ NONE, NONE, ATTACK, DONE],
+ [GET, "jsurl with html entities 2", "outside",
+ '', NONE, NONE, ATTACK, DONE],
+ // TODO: create a flash file?
+ //["inline_outside10_bad", "outside",
+ // ''],
+ [GET, "inside js url", "js_url",
+ "javascript:void(parent.parent.postMessage(\'xss\', \'*\'));",
+ NONE, NONE, ATTACK, DONE],*/
+ [GET, "body onload event", "html_nobody",
+ 'Hello',
+ NONE, NONE, ATTACK, DONE],
+ // does not seem to be a valid vector for firefox
+ // ["meta1_bad", "meta",
+ // ''],
+ // document domain has not been set
+ // ["meta2_bad", "meta",
+ // ''],
+ // does not work. maybe server sends content type?
+ // ["utf7_bad", "meta",
+ // ' +ADw-SCRIPT+AD4-alert(\'XSS\');+ADw-/SCRIPT+AD4-
+
+
+
+
+
+
+
+
+
+
+