# 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- + + + + + + + +

+ + +
+  
+ +Filter on +Filter off +
+ + + diff --git a/content/base/test/unit/test_xssutils.js b/content/base/test/unit/test_xssutils.js new file mode 100644 --- /dev/null +++ b/content/base/test/unit/test_xssutils.js @@ -0,0 +1,358 @@ +/* ***** 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 tests. + * + * 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 ***** */ + +// cannot use import because we want to ignore visibility +//Components.utils.import('resource://gre/modules/XSSUtils.jsm'); + +var filter = Components.classes["@sunysb.edu/filter;1"].getService(); + + +var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Components.interfaces.mozIJSSubScriptLoader); +loader.loadSubScript('resource://gre/modules/XSSUtils.jsm'); + +var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + +var JSON = Components.classes["@mozilla.org/dom/json;1"] + .createInstance(Components.interfaces.nsIJSON); + + +var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); +prefs.setBoolPref("security.csp.debug", true); + +/* is there a smarter way to perform deep comparison? */ +// TODO: it does not work. do_check_* can only be called directly from +// the functions. what kind of magic is this? +function do_array_eq(a1, a2) { + //CSPdebug("EQ?: " + a1 + "---" + a2); + CSPdebug("Array EQ"); + do_check_eq(String(a1), String(a2)); +} + +function s(st) { + return String(st); +} + +// steal some stuff from test_CSPUtils +var tests = []; +function test(fcn) { + tests.push(fcn); +} + +test( + function test_parseParams() { + //Test 1: (Simplest one) + url = "varA=533"; + params = parseParams(url); + result = [["varA", "533"]]; + do_check_eq(s(params), s(result)); + + //Test 2: (Missing params) + url = "varA=533&&varB=1"; + params = parseParams(url); + result = [["varA", "533"],["varB","1"]]; + do_check_eq(s(params), s(result)); + + //Test 3: (Incomplete params) + url = "varA=533&&varB&varC=1"; + params = parseParams(url); + result = [["varA", "533"],["unknown1","varB"],["varC","1"]]; + do_check_eq(s(params), s(result)); + + //Test 4: (Detecting values with special chars) + url = "a=3&b=aaa&c=bb" + escape("&") + "bb"; + params = parseParams(url); + result = [["a", "3"], ["b", "aaa"], ["c", "bb&bb"]]; + do_check_eq(s(params), s(result)); + } +); + +test( + function test_parseURL_filter() { + uri = ios.newURI("http://abc.com/index.php?a=3&b=aaa&c=aaabb" + + escape("&") + "bbccc#hello", null, null); + params = parseURL(uri); + result = [["a", "3"], ["b", "aaa"], ["c", "aaabb&bbccc"], + ["FRAGMENT", "hello"], + ["PATH", "index.php?a=3&b=aaa&c=aaabb&bbccc#hello"]]; + do_check_eq(s(params), s(result)); + result = [["c", "aaabb&bbccc"]]; + do_check_eq(s(filterParams(params)), s(result)); + + uri = ios.newURI("http://www.hello.it/", null, null); + params = parseURL(uri); + result = []; + do_check_eq(s(params), s(result)); + do_check_eq(s(filterParams(params)), s(result)); + + uri = ios.newURI( + "http://www.abc.it/get?id=4567&name=" + + "&value=helloworld#", null, null); + params = parseURL(uri); + result = [ + ["id", "4567"], ["name", ""], + ["value", "helloworld"], ["FRAGMENT", ""], + ["PATH", "get?id=4567&name=&value=helloworld" + + "#"] + ]; + do_check_eq(s(params), s(result)); + result = [ + ["name", ""], + ["FRAGMENT", ""], + ["QSTRING", "id=4567&name=&value=helloworld" + + "#"] + ]; + do_check_eq(s(filterParams(params)), s(result)); + + + } +); + +test( + function test_isSameOrigin() { + uri1 = ios.newURI("http://abc.com/index.php?a=3&b=aaa&c=bb", null, null); + + //Test 1: Different domains. + uri2 = ios.newURI("http://w.abc.com/index.php?a=3&b=aaa&c=bb" + + escape("&") + "bb#hello", null, null); + do_check_false(isSameOrigin(uri1, uri2)); + + //Test 2: Same domain. + uri2 = ios.newURI("http://abc.com/hello/index.php?b=aaa&c=bb" + + escape("&") + "bb#hello", null, null); + do_check_true(isSameOrigin(uri1, uri2)); + + //Test 3: Different Protocol + uri2 = ios.newURI("ftp://abc.com/hello/index.php?b=aaa&c=bb" + + escape("&") + "bb#hello", null, null); + do_check_false(isSameOrigin(uri1, uri2)); + + //Test 4: Different port + uri2 = ios.newURI("http://abc.com:81/hello/index.php?b=aaa&c=bb" + + escape("&") + "bb#hello", null, null); + do_check_false(isSameOrigin(uri1, uri2)); + + //Test 5: Different domain + uri2 = ios.newURI("http://www.abc.com:81/hello/index.php?b=aaa&c=bb" + + escape("&") + "bb#hello", null, null); + do_check_false(isSameOrigin(uri1, uri2)); + } +); + +test( + function test_r_unescape() { + url = "aaa" + escape(escape("bb")) + "zzz"; + do_check_eq(r_unescape(url), "aaabbzzz"); + + url = "aaa&a"; + do_check_eq(r_unescape(url), "aaa&a"); + + url = "this%252520is%252520it"; + do_check_eq(r_unescape(url), "this is it"); + + url ="this%25253C%25253Eis%25253C%25253Eit"; + do_check_eq(r_unescape(url), "this<>is<>it"); + + url ="this%25253C%25253Eis%25253C%25253Eit"; + do_check_neq(r_unescape(url), "thisit"); + + url = escape(escape(escape(escape("lets see##how,,;,,this<>works_out@")))); + do_check_eq(r_unescape(url), "lets see##how,,;,,this<>works_out@"); + } +); + +test( + function test_getHostLimit() { + //Test one. + uri = ios.newURI("http://abc.com/index.php", null, null); + limit = getHostLimit(uri); + do_check_eq(limit, 14); + } +); + +test( + function test_getMatch() { + //Note: Taken care internally by other test cases. + + } +); + +test( + function test_p1Match() { + s = "thisisthefirststring"; + p = "thefirst"; + //Test 1 + distThreshold = 0.8; + do_check_neq( p1Match(p,s,distThreshold), null); + + //Test 2 + p = "thefarst"; + distThreshold = 0.05; + do_check_eq( p1Match(p,s,distThreshold), null); + + } +); + +test( + function test_p1MatchReverse() { + p = "thisisthefirststring"; + s = "thefarst"; + //Test 1 + distThreshold = 0.8; + // does not exist in JS anymore + //do_check_neq( p1MatchReverse(s,p,distThreshold), null); + + //Test 2 + distThreshold = 0.05; + //do_check_eq( p1MatchReverse(s,p,distThreshold), null); + + } +); + +test( + function test_getPMatch() { + //TODO: Pending implementation in XSSUtils.jsm + } +); + +test( + function test_tokenize() { + tokens = tokenize("aaa'aa'aaa"); + do_check_eq(tokens.length, 3); + do_check_eq(tokens[1][2], TokenType.STRING); + + tokens = tokenize("2010/10/7"); + do_check_eq(tokens.length, 3); + + tokens = tokenize("2010\"10\"7"); + do_check_eq(tokens.length, 3); + + tokens = tokenize("2010;10;7"); + do_check_eq(tokens.length, 1); + + tokens = tokenize( + '"externalURL":"http:\\/\\/www.facebook.com\\/ilFattoQuotidiano"'); + do_check_eq(tokens.length, 3); + + } +); + +test( + function test_findInlineXSS() { + + var script='var var1="value"; attack();'; + do_check_eq(String(checkInline([['a','attack();']], script)), String(null)); + + script='onloadRegister(function (){new ExternalPageLikeWidget({"isAmbiguousText":true,"viewer":0,"channelURL":null,"nodeType":"page","externalURL":"http:\\/\\/www.facebook.com\\/ilFattoQuotidiano","pageId":132707500076838,"widgetID":"connect_widget_4d42e5288ee171a42961445","alreadyConnected":false,"viewerIsAdmin":false,"adminUrl":null,"showFaces":true,"useUnlikeLink":true,"layout":"standard","commentWidgetMarkup":"","error":null,"autoResize":true,"connectText":0,"socialbar":false,"ref":null,"userOptedOut":false,"showCaptcha":false,"usingInlineCommenting":false,"isBlocked":false,"forceCommentHooks":"","nux":true,"referer":"http:\\/\\/www.ilfattoquotidiano.it\\/2011\\/01\\/27\\/eccome-come-il-presidente-del-consiglio-pago-il-silenzio-di-ruby-rubacuori\\/88843\\/"})});'; + var params = [['a', 'http://www.facebook.com/IlFattoQUotidiano']]; + do_check_eq(String(checkInline(params, script)), String(null)); + + script = "var var1='value_'; do_xss_attack(); //_fromuser';"; + params = [['a', "'; do_xss_attack(); //"]]; + do_check_eq(String(checkInline(params, script)), String(['a'])); + + + } +); + +test( + function test_findUrlXSS() { + //Test 1 + uri = ios.newURI("http://www.abc.com/index.php?var1=Something&var2=&var3=smiley", null, null); + params= [["var1", "diffvalue"], ["var2", "not_a_hack"], ["var3", "smiley"], ["var4", "discarded"], ["var5","lost"]]; + //do_check_false(checkExternal(params, uri)); + + //Test 2 + uri = ios.newURI("http://www.abc.com/index.php?var1=Something&var2&var3=smiley", null, null); + params= [["var1", "Something"]]; + //do_check_true(checkExternal(params, uri)); + + //Test 3 + uri = ios.newURI("http://www.abc.com/index.php?var1=Something&var2&var3=smiley", null, null); + params= [["var1", "Something"], ["var2",""],["var3", "smiley"]]; + //do_check_true(checkExternal(params, uri)); + + //Test 4 + uri = ios.newURI("http://www.abc.com/index.php?var1=Something&var2&var3=smiley", null, null); + params= [["var1", "randomhere"], ["var2","valuehere"], ["var3", "smiley"], ["var4", "discarded"], ["var5","lost"]]; + //do_check_false(checkExternal(params, uri)); + + } +); + +test( + function test_isDangerous() { + do_check_false(isDangerous(["name", "kjflg kf-jl_kjl"])); + do_check_true(isDangerous(["name", "adffdf;sss()ffgfgff"])); + do_check_true(isDangerous(["name", "fs\"ckkjdlslkjslskjs"])); + do_check_true(isDangerous(["name", "fs<>ckfgdfgdfss"])); + } +); + +test( + function test_changeParam() { + do_check_eq(changeParam(""), + "aaa();"); + do_check_eq(changeParam( + "document.location = http://evil.com + document.cookie"), + "aaaaaaaa.aaaaaaaa = aaaa://aaaa.aaa + aaaaaaaa.aaaaaa"); + } +); + +test( + function test_buildUrl() { + var attackParams = ["evil", "vuln"]; + var url = ios.newURI( + "http://vuln.com/index.php?evil=&" + + "safe=do_something()&vuln=pippopippo#ciao", null, null); + var params = parseURL(url); + do_check_eq(buildUrl(url.asciiSpec, params, attackParams), + "http://vuln.com/index.php?evil=aaaa()&" + + "safe=do_something()&vuln=aaaaaaaaaa#ciao"); + url = ios.newURI("http://abc.com/gino?evil=b&cvc", null, null); + params = parseURL(url); + do_check_false(buildUrl(url.asciiSpec, params, attackParams)); + } +); + +function run_test() { + for(let i in tests) { + tests[i](); + } + + do_test_finished(); +} diff --git a/content/base/test/unit/xpcshell.ini b/content/base/test/unit/xpcshell.ini --- a/content/base/test/unit/xpcshell.ini +++ b/content/base/test/unit/xpcshell.ini @@ -8,3 +8,4 @@ [test_error_codes.js] [test_thirdpartyutil.js] [test_xmlserializer.js] +[test_xssutils.js] diff --git a/content/events/src/nsEventListenerManager.cpp b/content/events/src/nsEventListenerManager.cpp --- a/content/events/src/nsEventListenerManager.cpp +++ b/content/events/src/nsEventListenerManager.cpp @@ -82,9 +82,14 @@ #include "nsDOMEvent.h" #include "nsIContentSecurityPolicy.h" #include "nsJSEnvironment.h" +#include "prlog.h" using namespace mozilla::dom; +#ifdef PR_LOGGING +static PRLogModuleInfo* gCspPRLog; +#endif + #define EVENT_TYPE_EQUALS( ls, type, userType ) \ (ls->mEventType == type && \ (ls->mEventType != NS_USER_DEFINED_EVENT || ls->mTypeAtom == userType)) @@ -141,6 +146,14 @@ NS_ASSERTION(aTarget, "unexpected null pointer"); ++sCreatedCount; + mXssResult = UNKNOWN; + + // enable logging for CSP +#ifdef PR_LOGGING + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); +#endif + } nsEventListenerManager::~nsEventListenerManager() @@ -526,6 +539,17 @@ nsnull); return NS_OK; } + + // this is only for event attributes. we don't really need it for + // DOM2 handlers because the CSP would stop JS execution of + // the whole script. + // TODO: reuse the csp reference for performance + // TODO: is mXssResult per-event or per-page? + nsresult rv = CheckXSSEvent(doc, aBody); + if (rv != NS_OK) + return NS_OK; + + } } @@ -1015,3 +1039,53 @@ } return size; } + +nsresult +nsEventListenerManager::CheckXSSEvent(nsIDocument *doc, + const nsAString &script) { + // mXssResult contains the result if the event has been checked + // before. also, the interface is not very "xpcomey". the returned + // value should be an out arg. or since it is not part of the xpcom + // interface, it is not necessary? + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CheckXSSEvent for string:\n")); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, (NS_ConvertUTF16toUTF8(script).get())); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("@@@ End of String")); + + + + switch (mXssResult) { + case UNKNOWN: + if (doc) { + nsresult rv; + nsCOMPtr csp; + rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if (csp) { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Got CSP for compilation")); + bool safe; + rv = csp->PermitsEventListener(script, &safe); + NS_ENSURE_SUCCESS(rv, rv); + + if (!safe) { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("XSS in HTML attr handler")); + mXssResult = UNSAFE; + return NS_ERROR_FAILURE; + } else { + mXssResult = SAFE; + } + } else + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("No CSP for event check")); + } else + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("No DOC for event check")); + + break; + case SAFE: + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Handler already checked, safe")); + break; + case UNSAFE: + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Handler already checked, unsafe")); + return NS_ERROR_FAILURE; + break; + } + return NS_OK; +} diff --git a/content/events/src/nsEventListenerManager.h b/content/events/src/nsEventListenerManager.h --- a/content/events/src/nsEventListenerManager.h +++ b/content/events/src/nsEventListenerManager.h @@ -76,6 +76,8 @@ } } nsListenerStruct; +enum XSSResult { UNKNOWN, SAFE, UNSAFE }; + /* * Event listener manager */ @@ -307,6 +309,8 @@ PRUint32 mMayHaveMouseEnterLeaveEventListener : 1; PRUint32 mNoListenerForEvent : 25; + nsresult CheckXSSEvent(nsIDocument *doc, const nsAString &script); + nsAutoTObserverArray mListeners; nsISupports* mTarget; //WEAK nsCOMPtr mNoListenerForEventAtom; @@ -316,6 +320,10 @@ friend class nsEventTargetChainItem; static PRUint32 sCreatedCount; + +private: + XSSResult mXssResult; + }; #endif // nsEventListenerManager_h__ diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -3868,6 +3868,11 @@ cssClass.AssignLiteral("neterror"); error.AssignLiteral("cspFrameAncestorBlocked"); } + else if (NS_ERROR_XSS_BLOCK == aError) { + // XSS violation in block mode + cssClass.AssignLiteral("neterror"); + error.AssignLiteral("xssBlockMode"); + } else if (NS_ERROR_GET_MODULE(aError) == NS_ERROR_MODULE_SECURITY) { nsCOMPtr nsserr = do_GetService(NS_NSS_ERRORS_SERVICE_CONTRACTID); diff --git a/docshell/resources/content/netError.xhtml b/docshell/resources/content/netError.xhtml --- a/docshell/resources/content/netError.xhtml +++ b/docshell/resources/content/netError.xhtml @@ -204,6 +204,12 @@ document.getElementById("errorTryAgain").style.display = "none"; } + if (err == "xssBlockMode") { + // Remove the "Try again" button for XSS violations. We might instead consider + // putting a "Load Anyway" button. + document.getElementById("errorTryAgain").style.display = "none"; + } + if (err == "nssBadCert") { // Remove the "Try again" button for security exceptions, since it's // almost certainly useless. @@ -335,6 +341,7 @@

&nssBadCert.title;

&malwareBlocked.title;

&cspFrameAncestorBlocked.title;

+

&xssBlockMode.title;

&remoteXUL.title;

&corruptedContentError.title;

@@ -361,6 +368,8 @@
&nssBadCert.longDesc2;
&malwareBlocked.longDesc;
&cspFrameAncestorBlocked.longDesc;
+
&xssBlockMode.longDesc;
+
&remoteXUL.longDesc;
&corruptedContentError.longDesc;
diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -249,6 +249,10 @@ #include "nsLocation.h" #include "nsWrapperCacheInlines.h" +// for awful alert hack +#include +#include + #ifdef PR_LOGGING static PRLogModuleInfo* gDOMLeakPRLog; #endif @@ -4739,6 +4743,13 @@ { FORWARD_TO_OUTER(Alert, (aString), NS_ERROR_NOT_INITIALIZED); + // awful hack to suppress alert dialogs + if (getenv("NOALERT") != NULL) { + printf("Alert: %s\n", NS_LossyConvertUTF16toASCII(aString).get()); + printf("%s\n\n","###ALERT POPPED###"); + return NS_OK; + } + if (AreDialogsBlocked()) return NS_ERROR_NOT_AVAILABLE; diff --git a/dom/base/nsJSTimeoutHandler.cpp b/dom/base/nsJSTimeoutHandler.cpp --- a/dom/base/nsJSTimeoutHandler.cpp +++ b/dom/base/nsJSTimeoutHandler.cpp @@ -305,6 +305,27 @@ // Note: Our only caller knows to turn NS_ERROR_DOM_TYPE_ERR into NS_OK. return NS_ERROR_DOM_TYPE_ERR; } + + //xss settimeout + size_t len = 0; + const jschar *jschrs = JS_GetStringCharsAndLength(cx, expr, &len); + nsAutoString nsStr(jschrs); + XSSJSAction t = xss_timeout; + rv = csp->PermitsJSAction(nsStr, &allowsEval); + if (NS_FAILED(rv)) { + NS_WARNING("CCSP: PermitsJSAction failed"); + return JS_TRUE; // fail open to not break sites. + } + + if (!allowsEval) { + ::JS_ReportError(cx, "call to %s blocked by CSP", + *aIsInterval ? kSetIntervalStr : kSetTimeoutStr); + // Note: Our only caller knows to turn NS_ERROR_DOM_TYPE_ERR + // into NS_OK. + return NS_ERROR_DOM_TYPE_ERR; + } + + } } // if there's no document, we don't have to do anything. diff --git a/dom/locales/en-US/chrome/appstrings.properties b/dom/locales/en-US/chrome/appstrings.properties --- a/dom/locales/en-US/chrome/appstrings.properties +++ b/dom/locales/en-US/chrome/appstrings.properties @@ -63,5 +63,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. diff --git a/dom/locales/en-US/chrome/netError.dtd b/dom/locales/en-US/chrome/netError.dtd --- a/dom/locales/en-US/chrome/netError.dtd +++ b/dom/locales/en-US/chrome/netError.dtd @@ -86,6 +86,9 @@ The browser prevented this page from loading in this way because the page has a content security policy that disallows it.

"> + +The browser 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/dom/src/jsurl/nsJSProtocolHandler.cpp b/dom/src/jsurl/nsJSProtocolHandler.cpp --- a/dom/src/jsurl/nsJSProtocolHandler.cpp +++ b/dom/src/jsurl/nsJSProtocolHandler.cpp @@ -79,9 +79,14 @@ #include "nsIObjectOutputStream.h" #include "nsIWritablePropertyBag2.h" #include "nsIContentSecurityPolicy.h" +#include "prlog.h" static NS_DEFINE_CID(kJSURICID, NS_JSURI_CID); +#ifdef PR_LOGGING +static PRLogModuleInfo* gCspPRLog; +#endif + class nsJSThunk : public nsIInputStream { public: @@ -130,6 +135,12 @@ rv = uri->GetSpec(mURL); if (NS_FAILED(rv)) return rv; + // enable logging for CSP +#ifdef PR_LOGGING + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); +#endif + return NS_OK; } @@ -215,6 +226,19 @@ nsnull); return NS_ERROR_DOM_RETVAL_UNDEFINED; } + + nsCAutoString script(mScript); + NS_UnescapeURL(script); + bool safe; + + rv = csp->PermitsJSUrl(NS_ConvertUTF8toUTF16(mURL), &safe); + NS_ENSURE_SUCCESS(rv, rv); + + if (!safe) { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("XSS in JS URI")); + return NS_ERROR_DOM_RETVAL_UNDEFINED; + } + } // Get the global object we should be running on. diff --git a/js/src/js.msg b/js/src/js.msg --- a/js/src/js.msg +++ b/js/src/js.msg @@ -370,3 +370,4 @@ MSG_DEF(JSMSG_DEBUG_NOT_DEBUGGING, 284, 0, JSEXN_ERR, "can't set breakpoint: script global is not a debuggee") MSG_DEF(JSMSG_DEBUG_COMPARTMENT_MISMATCH, 285, 2, JSEXN_TYPEERR, "{0}: descriptor .{1} property is an object in a different compartment than the target object") MSG_DEF(JSMSG_DEBUG_NOT_SCRIPT_FRAME, 286, 0, JSEXN_ERR, "stack frame is not running JavaScript code") +MSG_DEF(JSMSG_XSS_BLOCKED_FUNCTION, 287, 0, JSEXN_ERR, "call to Function() blocked by XSS Filter") diff --git a/js/src/jsapi.h b/js/src/jsapi.h --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -1205,6 +1205,15 @@ (* JSCSPEvalChecker)(JSContext *cx); /* + * Used to check if an xss filter wants to disable eval() and friends. + * See js_CheckXSSPermitsJSAction() in jsobj. + */ +enum XSSJSAction { xss_eval, xss_timeout, xss_function }; +typedef JSBool +(* JSXSSFilterChecker)(JSContext *cx, JSString *str, enum XSSJSAction action); + + +/* * Callback used to ask the embedding for the cross compartment wrapper handler * that implements the desired prolicy for this kind of object in the * destination compartment. @@ -3635,6 +3644,7 @@ JSPrincipalsTranscoder principalsTranscoder; JSObjectPrincipalsFinder findObjectPrincipals; JSCSPEvalChecker contentSecurityPolicyAllows; + JSXSSFilterChecker xssFilterAllows; }; extern JS_PUBLIC_API(JSSecurityCallbacks *) diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -72,6 +72,7 @@ #include "jstracer.h" #include "vm/CallObject.h" #include "vm/Debugger.h" +#include "jspubtd.h" #if JS_HAS_GENERATORS # include "jsiter.h" @@ -2262,6 +2263,18 @@ strAnchor.set(str); chars = str->getChars(cx); length = str->length(); + + JSSecurityCallbacks *callbacks = JS_GetSecurityCallbacks(cx); + // this callback is not always defined + if (callbacks && callbacks->xssFilterAllows) { + XSSJSAction f = xss_function; + if (!callbacks->xssFilterAllows(cx, str, f)) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, + JSMSG_XSS_BLOCKED_FUNCTION); + return JS_FALSE; + } + } + } else { chars = cx->runtime->emptyString->chars(); length = 0; diff --git a/js/src/jsobj.cpp b/js/src/jsobj.cpp --- a/js/src/jsobj.cpp +++ b/js/src/jsobj.cpp @@ -1170,6 +1170,17 @@ } JSString *str = args[0].toString(); + // xss filter + JSSecurityCallbacks *callbacks = JS_GetSecurityCallbacks(cx); + if (callbacks && callbacks->xssFilterAllows) { + XSSJSAction e = xss_eval; + if (!callbacks->xssFilterAllows(cx, str, e)) { + JS_ReportError(cx, "call to eval() is an xss attack"); + return false; + } + } + + /* ES5 15.1.2.1 steps 2-8. */ /* diff --git a/mobile/chrome/content/netError.xhtml b/mobile/chrome/content/netError.xhtml --- a/mobile/chrome/content/netError.xhtml +++ b/mobile/chrome/content/netError.xhtml @@ -199,6 +199,12 @@ document.getElementById("errorTryAgain").style.display = "none"; } + if (err == "xssBlockMode") { + // Remove the "Try again" button for XSS violations. We might instead consider + // putting a "Load Anyway" button. + document.getElementById("errorTryAgain").style.display = "none"; + } + if (err == "nssBadCert") { // Remove the "Try again" button for security exceptions, since it's // almost certainly useless. @@ -328,6 +334,7 @@

&nssFailure2.title;

&nssBadCert.title;

&cspFrameAncestorBlocked.title;

+

&xssBlockMode.title;

&remoteXUL.title;

&corruptedContentError.title;

@@ -352,6 +359,7 @@
&nssFailure2.longDesc;
&nssBadCert.longDesc2;
&cspFrameAncestorBlocked.longDesc;
+
&xssBlockMode.longDesc;
&remoteXUL.longDesc;
&corruptedContentError.longDesc;
diff --git a/mobile/locales/en-US/chrome/overrides/appstrings.properties b/mobile/locales/en-US/chrome/overrides/appstrings.properties --- a/mobile/locales/en-US/chrome/overrides/appstrings.properties +++ b/mobile/locales/en-US/chrome/overrides/appstrings.properties @@ -63,5 +63,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/mobile/locales/en-US/chrome/overrides/netError.dtd b/mobile/locales/en-US/chrome/overrides/netError.dtd --- a/mobile/locales/en-US/chrome/overrides/netError.dtd +++ b/mobile/locales/en-US/chrome/overrides/netError.dtd @@ -145,6 +145,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/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -1198,6 +1198,10 @@ pref("security.csp.enable", true); pref("security.csp.debug", false); +pref("security.csp.profile", false); +pref("security.csp.xss", true); +pref("security.csp.xssblock", false); +pref("security.csp.xsswarning", false); // Modifier key prefs: default to Windows settings, // menu access key = alt, accelerator key = control. diff --git a/netwerk/protocol/data/nsDataChannel.cpp b/netwerk/protocol/data/nsDataChannel.cpp --- a/netwerk/protocol/data/nsDataChannel.cpp +++ b/netwerk/protocol/data/nsDataChannel.cpp @@ -49,6 +49,13 @@ #include "plbase64.h" #include "plstr.h" #include "prmem.h" +#include "prlog.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIPrincipal.h" + +#ifdef PR_LOGGING +static PRLogModuleInfo* gCspPRLog; +#endif nsresult nsDataChannel::OpenContentStream(bool async, nsIInputStream **result, @@ -57,6 +64,30 @@ NS_ENSURE_TRUE(URI(), NS_ERROR_NOT_INITIALIZED); nsresult rv; +#ifdef PR_LOGGING + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); +#endif + + nsISupports* owner; + GetOwner(&owner); + nsCOMPtr principal = do_QueryInterface(owner); + nsCOMPtr csp; + if (principal) { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("data url: Got principal")); + rv = principal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + } + if (csp) { + // TODO: some of these might not be scripts! + bool safe; + rv = csp->PermitsDataUrl(URI(), &safe); + NS_ENSURE_SUCCESS(rv, rv); + if (!safe) { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("XSS in data URL")); + return NS_ERROR_NOT_INITIALIZED; + } + } nsCAutoString spec; rv = URI()->GetAsciiSpec(spec); diff --git a/testing/xpcshell/xpcshell.ini b/testing/xpcshell/xpcshell.ini --- a/testing/xpcshell/xpcshell.ini +++ b/testing/xpcshell/xpcshell.ini @@ -25,6 +25,7 @@ [include:toolkit/components/places/tests/unit/xpcshell.ini] [include:toolkit/components/places/tests/network/xpcshell.ini] [include:toolkit/components/urlformatter/tests/unit/xpcshell.ini] +[include:toolkit/components/taintinference/tests/unit/xpcshell.ini] [include:toolkit/components/ctypes/tests/unit/xpcshell.ini] [include:toolkit/components/autocomplete/tests/unit/xpcshell.ini] [include:toolkit/components/satchel/test/unit/xpcshell.ini] diff --git a/toolkit/components/Makefile.in b/toolkit/components/Makefile.in --- a/toolkit/components/Makefile.in +++ b/toolkit/components/Makefile.in @@ -73,6 +73,7 @@ viewconfig \ viewsource \ telemetry \ + taintinference \ $(NULL) ifdef BUILD_CTYPES diff --git a/toolkit/components/taintinference/DistMetric.cpp b/toolkit/components/taintinference/DistMetric.cpp new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/DistMetric.cpp @@ -0,0 +1,102 @@ +#include +#include "DistMetric.h" +#include +#include +//#include + +using namespace std; +#define DEFAULT_COST 20 + +#define IS_ASCII(u) ((u) < 0x80) +// #define IS_ASCII_UPPER(u) (('A' <= (u)) && ( (u) <= 'Z' )) +// #define IS_ASCII_LOWER(u) (('a' <= (u)) && ( (u) <= 'z')) +// #define IS_ASCII_ALPHA(u) (IS_ASCII_UPPER(u) || IS_ASCII_LOWER(u)) +// #define IS_ASCII_SPACE(u) ( ' ' == (u) ) + + +// We map x -> x, except for upper-case letters, + // which we map to their lower-case equivalents. +const PRUint8 gASCIIToLower [128] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, +}; + +// this version does not put non-ascii chars to lowercase +NS_ALWAYS_INLINE PRUnichar +ToLowerCase(PRUnichar aChar) +{ + if (IS_ASCII(aChar)) + return gASCIIToLower[aChar]; + else + return aChar; +} + +PRUint8 DistMetric::geticost(PRUnichar sc) { + return DEFAULT_COST; +} + +PRUint8 DistMetric::getdcost(PRUnichar pc) { + return DEFAULT_COST; +} + +PRUint8 DistMetric::getscost(PRUnichar pc, PRUnichar sc) { + // in this case, normalization might not be very useful... + if (ToLowerCase(pc) == ToLowerCase(sc)) + return 0; + else // copy BinMetric costs + return ( (geticost(sc) + getdcost(pc)) * 3) / 4; +} + +PRUnichar DistMetric::getnorm(PRUnichar c) { + return ToLowerCase(c); +} + +const PRUnichar DistVector::NOT_ASCII = PRUnichar(~0x007F); + +inline bool DistVector::isASCII(PRUnichar c) { + return false; + return (c & DistVector::NOT_ASCII); +} + +inline PRUint8 DistVector::getASCII(PRUnichar c) { + return c; +} + +DistVector::DistVector() { + +} + +PRInt32 DistVector::getDiff(PRUnichar c) { + if (isASCII(c)) + return mArray[getASCII(c)]; + else + return mMap[c]; +} + +void DistVector::setDiff(PRUnichar c, PRInt32 val) { + if (isASCII(c)) + mArray[getASCII(c)] = val; + else + mMap[c] = val; +} + +PRInt32& DistVector::operator[] (PRUnichar c) { + if (isASCII(c)) + return mArray[getASCII(c)]; + else + return mMap[c]; +} + +void DistVector::print() { + cout << "DIFFVEC\n"; + map::iterator it; + for ( it=mMap.begin() ; it != mMap.end(); it++ ) + cout << (*it).first << " => " << (*it).second << endl; + +} diff --git a/toolkit/components/taintinference/DistMetric.h b/toolkit/components/taintinference/DistMetric.h new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/DistMetric.h @@ -0,0 +1,33 @@ +#ifndef DIST_METRIC_H +#define DIST_METRIC_H + +#include +#include "nsXPCOM.h" + + +using namespace std; + +class DistMetric { + public: + static PRUint8 geticost(PRUnichar sc); + static PRUint8 getdcost(PRUnichar pc); + static PRUint8 getscost(PRUnichar pc, PRUnichar sc); + static PRUnichar getnorm(PRUnichar c); +}; + +class DistVector { + public: + DistVector(); + PRInt32 getDiff(PRUnichar c); + void setDiff(PRUnichar c, PRInt32 val); + PRInt32& operator[] (PRUnichar c); + void print(); + private: + map mMap; + PRInt32 mArray[128]; + static const PRUnichar NOT_ASCII; + inline bool isASCII(PRUnichar c); + inline PRUint8 getASCII(PRUnichar c); +}; + +#endif diff --git a/toolkit/components/taintinference/Makefile.in b/toolkit/components/taintinference/Makefile.in new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/Makefile.in @@ -0,0 +1,70 @@ +# ***** 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 +# The Mozilla Foundation . +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Zack Weinberg (original author) +# +# 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 = taintinference +MODULE_NAME = taintinference +GRE_MODULE = 1 + +LIBRARY_NAME = taintinference +LIBXUL_LIBRARY = 1 +EXPORT_LIBRARY = 1 +IS_COMPONENT = 1 + +XPIDLSRCS = \ + nsITaintInference.idl \ + $(NULL) + +CPPSRCS = \ + nsTaintInference.cpp \ + $(NULL) + +EXTRA_JS_MODULES = \ + TaintInference.jsm \ + $(NULL) + +ifdef ENABLE_TESTS +DIRS += tests +endif + +include $(topsrcdir)/config/rules.mk diff --git a/toolkit/components/taintinference/TaintInference.jsm b/toolkit/components/taintinference/TaintInference.jsm new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/TaintInference.jsm @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* ***** 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 + * The Mozilla Foundation . + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Zack Weinberg + * + * 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 Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; + + +let EXPORTED_SYMBOLS = [ "p1FastMatch", "p1FastMatchReverse", "setFast"]; +var ti = Components.classes["@mozilla.org/taintinference;1"].getService() + .QueryInterface(Components.interfaces.nsITaintInference); + + +/* converts the nsIArray into a js array */ +// TODO: is there any better way to pass js objects through xpcom? +function to_array(xpcomarray) { + if (xpcomarray.length == 0) + return null; + var results = []; + for (var i = 0; i < xpcomarray.length; ++i) { + var value1 = xpcomarray.queryElementAt(i, Ci.nsISupportsPRUint32); + ++i; + var value2 = xpcomarray.queryElementAt(i, Ci.nsISupportsPRUint32); + results.push([value1.data, value2.data]); + } + //CSPdebug("to_array results: " + results); + return results; +} + +/* an older version in svn contains a javascript implementation of + * p1Match */ + +/* interfaces to the xpcom functions */ +function p1FastMatch(p, s, threshold) { + var match = to_array(ti.p1FastMatch(p, s, threshold)); + return match; +} +function p1FastMatchReverse(s, p, threshold) { + var match = to_array(ti.p1FastMatchReverse(s, p, threshold)); + return match; +} + +function setFast(b) { + ti.noFast = b; +} diff --git a/toolkit/components/taintinference/nsITaintInference.idl b/toolkit/components/taintinference/nsITaintInference.idl new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/nsITaintInference.idl @@ -0,0 +1,54 @@ +/* -*- 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" +#include "nsIArray.idl" + +[scriptable, uuid(03702bc6-0a02-48b5-ba7b-69e696574f03)] +interface nsITaintInference : nsISupports +{ + // does efficient approximate substring matching and return the match on s + nsIArray p1FastMatch(in AString p, in AString s, in float distThreshold); + + // same as above, but the match is on p. + nsIArray p1FastMatchReverse(in AString s, in AString p, + in float distThreshold); + + attribute boolean noFast; + +}; diff --git a/toolkit/components/taintinference/nsTaintInference.cpp b/toolkit/components/taintinference/nsTaintInference.cpp new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/nsTaintInference.cpp @@ -0,0 +1,769 @@ +/* -*- 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 +#include +#include +#include + +#include "mozilla/ModuleUtils.h" +#include "nsITaintInference.h" +#include "nsTaintInference.h" +#include "nsMemory.h" +#include "plstr.h" +#include "nsArrayUtils.h" +#include "nsIMutableArray.h" +#include "nsString.h" +#include "nsIClassInfoImpl.h" +#include "nsIArray.h" +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsIComponentRegistrar.h" +#include "nsXPCOM.h" +#include "nsISupportsPrimitives.h" +#include "nsComponentManagerUtils.h" +#include "nsTArray.h" +#include "DistMetric.h" + +using namespace std; + + +#define NS_TAINTINFERENCE_CID \ +{ 0x03702bc5, 0xa02, 0x48b5, { 0xba, 0x7b, 0x69, 0xe6, 0x96, 0x57, 0x4f, 0x03 } } +#define NS_TAINTINFERENCE_CONTRACTID "@mozilla.org/taintinference;1" + +namespace mozilla { +namespace taintinference { + +#define min2(a, b) ( (a) < (b) ? (a) : (b) ) +#define min3(a, b, c) ( (a) < (b) ? min2(a, c) : min2(b, c) ) +#define MAX_DIST 100000000 +#define dist(a, b) dist[(a)*(slen+1)+(b)] +#define NONE 4 +#define SUBST 1 +#define DELETE 2 +#define INSERT 3 + +//TODO: disable them after debug +bool printMatch = PR_TRUE; +bool printDistMatrix = PR_FALSE; +bool printApproxMatches = PR_TRUE; +bool printAllMatchSummary = PR_TRUE; + +NS_GENERIC_FACTORY_CONSTRUCTOR(Module) +NS_IMPL_ISUPPORTS1(Module, nsITaintInference) + + +Module::Module() +{ + noFast = false; +} + +Module::~Module() +{ + +} + +NS_IMETHODIMP Module::GetNoFast(bool *b) +{ + if (!b) + return NS_ERROR_NULL_POINTER; + *b = noFast; + return NS_OK; +} + +NS_IMETHODIMP Module::SetNoFast(bool b) +{ + noFast = b; + return NS_OK; +} + +inline void +printDist(PRInt32 *dist, PRInt32 slen, PRInt32 plen) { + if (printDistMatrix) { + PRInt32 i, j; + printf(" *"); + for (j = 0; j <= slen; j++) + printf("%5d", j); + printf("\n"); + for (i = 0; i <= plen; i++) { + printf("%3d", i); + for (j = 0; j <= slen; j++) { + if (dist(i, j) > 300 || dist(i, j) < -300) + printf("%5d", -1); + else + printf("%5d", dist(i, j)); + } + printf("\n"); + } + } +} + + +inline int getMatch(PRInt32 *dist, const nsAString &p, const nsAString &s, + int sEnd, float thresh, PRUint8 *& opmap) { + // If we need to print the match, then we need to create the + // string s1 and p1 that are obtained by aligning the matching + // portion of s with p, and inserting '-' characters where + // insertions/deletions take place. + // + // If we dont need to print, then we need only compute the + // beginning of the match, which is in "j" at the end of the loop below + + PRInt32 plen = p.Length(); + PRInt32 slen = s.Length(); + + int i = plen, j = sEnd; + int k = i+sEnd+1; + int rv; + + PRUint8 *op = (PRUint8 *) nsMemory::Alloc(k+1); + op[k--] = 0; + + while ((i > 0) && (j > 0)) { + PRUnichar sc = s[j-1], pc = p[i-1]; + PRInt32 curDist = dist(i, j); + if (curDist == dist(i-1, j) + DistMetric::GetDCost(pc)) { + op[k] = DELETE; + i--; + } + else if (curDist == dist(i, j-1) + DistMetric::GetICost(sc)) { + op[k] = INSERT; + j--; + } + else { + if (DistMetric::GetSCost(pc, sc) == 0) + op[k] = NONE; + else op[k] = SUBST; + i--; j--; + } + k--; + } + + k++; + rv = j; + opmap = (PRUint8 *) strdup((const char *) &op[k]); + nsMemory::Free(op); + return rv; +} + +inline void +prtMatch(PRInt32 b, PRInt32 e, float d, const nsAString &p, const nsAString &s, + const PRUint8 *op, bool printMatch) { + int i, j; bool done; + if (printMatch) { + printf("distance(s[%d-%d]): %g\n\tp: ", b, e, d); + for (i=0, j=0, done=false; (!done); j++) { + switch (op[j]) { + case NONE: + case SUBST: + putwchar(p[i]); i++; continue; + case DELETE: + putwchar(p[i]); i++; continue; + case INSERT: + putchar('-'); continue; + default: + done=true; break; + } + } + printf("\n\ts: "); + for (i=b, j=0, done=false; (!done); j++) { + switch (op[j]) { + case NONE: + case SUBST: + putwchar(s[i]); i++; continue; + case INSERT: + putwchar(s[i]); i++; continue; + case DELETE: + putchar('-'); continue; + default: + done=true; break; + } + } + printf("\n\to: "); + for (i=b, j=0, done=false; (!done); j++) { + switch (op[j]) { + case NONE: + putchar('N'); continue; + case SUBST: + putchar('S'); continue; + case INSERT: + putchar('I'); continue; + case DELETE: + putchar('D'); continue; + default: + done=true; break; + } + } + printf("\n"); + } +} + + +nsresult P1Match(const nsAString & p, const nsAString & s, PRInt32 sOffset, + float distThreshold, MatchRes &mres) { + + PRInt32 i, j; + PRInt32 sBeg, sEnd; + PRInt32 bestDist=MAX_DIST; + PRInt32 prevBest; + PRInt32 distDn=0, distRt=0, distDiag=0; + PRInt32* dist; + PRUint8 *opmap; + + PRInt32 plen = p.Length(); + PRInt32 slen = s.Length(); + + dist = (PRInt32 *) nsMemory::Alloc((plen+1)*(slen+1)*sizeof(PRInt32 *)); + if (dist == NULL) { + NS_WARNING("dist is NULL!\n"); + return NS_ERROR_OUT_OF_MEMORY; + } + + PRInt32 costThresh; + +#ifdef DEBUG + if (printAllMatchSummary) + printf("p1Match(p, s[%d-%d])\n", sOffset ,sOffset+slen); +#endif + // Lazy Initialization of costThresh_ + if (mres.costThresh_ < 0) { + PRInt32 maxCost = 0; + prevBest = MAX_DIST; + for (i = 0; i < plen; i++) + maxCost += DistMetric::GetDCost(p[i]); + costThresh = (PRInt32)floorf((float) distThreshold * maxCost); + mres.costThresh_ = costThresh; + } + else { + prevBest = mres.bestDist_; + costThresh = mres.costThresh_; + } + + // Initialize distance matrix + + dist(0, 0) = 0; + for (j=1; j <= slen; j++) + dist(0, j) = 0; // this is in the for loop... + for (i=1; i <= plen; i++) + dist(i, 0) = dist(i-1, 0) + DistMetric::GetDCost(p[i-1]); + +#ifdef DEBUG + printDist(dist, slen, plen); +#endif + + // Main loop: compute the minimum distance matrix + + for (i = 1; i <= plen; i++) { + for (j = 1; j <= slen; j++) { + PRUnichar sc = s[j-1], pc = p[i-1]; + distDn = dist(i-1, j) + DistMetric::GetDCost(pc); + distRt = dist(i, j-1) + DistMetric::GetICost(sc); + distDiag = dist(i-1, j-1) + DistMetric::GetSCost(pc, sc); + dist(i, j) = min3(distDn, distRt, distDiag); + } + } + +#ifdef DEBUG + printDist(dist, slen, plen); +#endif + /* Now, look for j such that dist(plen, j) is minimum. This gives + the lowest cost substring match between p and s */ + + sEnd = 1; + for (j=1; j <= slen; j++) { + if (dist(plen, j) < bestDist) { + bestDist = dist(plen, j); + sEnd = j; + } + } + + // Compare the best with previous best matches to see if there is any + // sense in continuing further. We retain results that are below costThresh + // that are within 50% of costThresh from the best result so far. + + // TODO: what do we need "bad" matches for in the first place? + // we already know they won't be used by the caller, do we need them + // for overlaps? Perhaps we can simply keep valid matches only, and + // eventually merge them at the end if necessary. + + if (bestDist <= prevBest + (costThresh/2)) { + PRInt32 bestSoFar = min(bestDist, prevBest); + mres.bestDist_ = bestSoFar; + + if (bestDist < prevBest) { + for (PRUint32 i = 0; i < mres.elem_.Length(); i++) { + if ((mres[i].dist_ > costThresh) && + (mres[i].dist_ > bestDist+(costThresh/2))) { +#ifdef DEBUG + printf("Removing worse match: [%d, %d]\n", mres[i].matchBeg_, + mres[i].matchEnd_); +#endif + mres.elem_.RemoveElementAt(i); + i--; // next loop goes back to i; + } + } + } + + sBeg = getMatch(dist, p, s, sEnd, distThreshold, opmap); +#ifdef DEBUG + prtMatch(sBeg, sEnd, bestDist, p, s, opmap, printMatch); +#endif + // previously sEnd was decreased by 1. why? + MatchResElem mre(bestDist, sBeg + sOffset, sEnd + sOffset, opmap); + mres.elem_.AppendElement(mre); + + // Now, compare the best match with other possible matches + // identified in this invocation of this function + +#ifdef FIND_OVERLAPS + PRInt32 l; + for (l=1; l <= slen; l++) { + PRInt32 currDist = dist(plen, l); + if (((currDist <= costThresh) || + (currDist <= bestSoFar + (costThresh/2))) && + // The first two tests below eliminate consideration + // of distances that do not correspond to local minima + (currDist < dist(plen, l-1)) && + ((l == slen) || currDist < dist(plen, l+1)) && + (l != sEnd)) /* Dont consider the global minima, either */ { + + j = getMatch(dist, p, s, l, distThreshold, opmap); + + /* s[j]...s[l-1] are included in the match with p */ + + // Eliminate matches that have large overlap with the + // main match. This is necessary since the "non-local + // minima" test earlier misses situations where the + // distance increases briefly but then decreases after + // a few more characters. In that case, you seem to + // have a local minima, but the corresponding match is + // subsumed in the ultimate match that is discovered. + + PRInt32 uniqLen=0; + if (j < sBeg) + uniqLen += sBeg-j; + if (l > sEnd) + uniqLen += l-sEnd; + if (uniqLen > (plen*distThreshold)) { + // TODO: why are we only adding a match and not deleting the + // previous matches? + MatchResElem mre(bestDist, j + sOffset, l-1 + sOffset, opmap); + mres.elem_.InsertElementAt(0, mre); + printf("prtMatch, find overlap\n"); + prtMatch(j, l, dist(plen, l), p, s, opmap, printAllMatches); + } + } // if ((currDist <= costThresh) ... + } // for (l=1; ... +#endif + } // if (bestDist <= ... + nsMemory::Free(dist); + return NS_OK; + +} + + +inline void +adjustDiffIns(PRUnichar c, DistVector &vec, int& idiff, int& ddiff) { + if (vec.GetDiff(c) > 0) // an excess of c in p, now reduced when c is added to s + ddiff -= DistMetric::GetDCost(c); + else // an excess of c in s, now excess further increased + idiff += DistMetric::GetICost(c); + vec.SetDiff(c, vec.GetDiff(c) - 1); +} + +inline void +adjustDiffDel(PRUnichar c, DistVector &vec, int& idiff, int& ddiff) { + if (vec.GetDiff(c) >= 0) // an excess of c in p, further increased now + ddiff += DistMetric::GetDCost(c); + else // an excess of c in s, excess being reduced now, so decrease + idiff -= DistMetric::GetICost(c); // insertion cost correspondingly. + vec.SetDiff(c, vec.GetDiff(c) + 1); +} + +nsresult +Module::P1FastMatch(const nsAString & p, const nsAString & s, + float distThreshold, MatchRes &mr, nsIArray ** outArray) { + NS_ENSURE_ARG_POINTER(outArray); + *outArray = nsnull; + + PRInt32 plen = p.Length(); + PRInt32 slen = s.Length(); + + if (plen > slen) { + NS_ERROR("p should be smaller than s"); + printf("p (%d): %s\n", plen, NS_LossyConvertUTF16toASCII(p).get()); + printf("s (%d): %s\n", slen, NS_LossyConvertUTF16toASCII(s).get()); + return NS_ERROR_INVALID_ARG; + } + + + + PRInt32 idiff=0, ddiff=0, diff; + PRInt32 diffThresh; + PRInt32 start=-1, end=-1; + DistVector diffVec; + const PRUnichar* cur = p.BeginReading(); + const PRUnichar* finish = p.EndReading(); + + mr.costThresh_ = -1; + mr.bestDist_ = MAX_DIST; + + if (noFast) { + nsresult rv = P1Match(p, s, 0, distThreshold, mr); + NS_ENSURE_SUCCESS(rv, rv); + goto afterP1; + } + + for (; cur < finish; ++cur) { + PRUnichar c = DistMetric::GetNorm(*cur); + adjustDiffDel(c, diffVec, idiff, ddiff); + } + + diffThresh = (PRInt32)floorf(ddiff*distThreshold); + + + for (PRInt32 i = 0; i < plen; ++i) { + PRUnichar d = DistMetric::GetNorm(s[i]); + adjustDiffIns(d, diffVec, idiff, ddiff); + } + + //diffVec.print(); + + diff = min(idiff, ddiff); + + if (diff <= diffThresh) + start = 0; + + // move the sliding window, and when the difference is low enough, + // call the actual P1Match on the candidate. + for (PRInt32 j=plen; j < slen; j++) { + char c = DistMetric::GetNorm(s[j-plen]); + char d = DistMetric::GetNorm(s[j]); + + adjustDiffDel(c, diffVec, idiff, ddiff); + adjustDiffIns(d, diffVec, idiff, ddiff); + //cout << "idiff = " << idiff << ", ddiff = " << ddiff << endl; + diff = min(idiff, ddiff); + + if (start == -1) { + if (diff <= diffThresh) + start = j-plen+1; + } else if (diff > diffThresh) { + end = j+1;//-1+diffThresh; + if (end >= slen) + end = slen; + start -= 1;//diffThresh; + if (start < 0) + start = 0; + + const nsAString &newS = Substring(s, start, end-start+1); + nsresult rv = P1Match(p, newS, start, distThreshold, mr); + NS_ENSURE_SUCCESS(rv, rv); + start = -1; + //j = end-1; + } + } + + // this is for the remaining of the string if you started seeing a + // match and you get to the end of the string before the end of + // the match. + if (start != -1) { + end = slen-1; + + // increasing 1 seems to be ok here... it is a bit confusing + // because we print the values and they don't always have the + // same semantic meaning. sometimes they are "last character + // in the match" and sometimes "first char out of the match" + const nsAString &newS = Substring(s, start, end-start+1); + nsresult rv = P1Match(p, newS, start, distThreshold, mr); + NS_ENSURE_SUCCESS(rv, rv); + } + afterP1: +#ifdef DEBUG + if (printApproxMatches || printAllMatchSummary) { + if (!mr.elem_.IsEmpty() && (mr.bestDist_ <= mr.costThresh_) && + (printAllMatchSummary + || ((mr.bestDist_ > 0) || (mr.elem_.Length() != 1)))) { + for (PRUint32 i = 0; i < mr.elem_.Length(); i++) { + printf("prtMatch, printMatchsummary\n"); + prtMatch(mr[i].matchBeg_, mr[i].matchEnd_, + mr[i].dist_, p, s, mr[i].opmap_, true); + } + + for (PRUint32 i = 0; i < mr.elem_.Length(); i++) { + PRInt32 smin = mr[i].matchBeg_ - 6; + PRInt32 smax = mr[i].matchEnd_ + 6; + printf("MATCH:\n"); + if (smin > 0) + printf("..."); + else printf(" "); + + for (PRInt32 k=smin; k <= min(smax, slen-1); k++) { + if (k >= 0) + if ((s[k] == PRUnichar('\n')) || + (s[k] == PRUnichar('\t'))) + printf(" "); + else + putwchar(s[k]); + else printf(" "); + } + if (smax < slen-1) + printf("..."); + printf("[%d,%d]", mr[i].matchBeg_, mr[i].matchEnd_); + if (mr[i].dist_ != 0) + printf(": %f", (mr[i].dist_*distThreshold)/mr.costThresh_); + printf("\n"); + if (mr.costThresh_ != 0) { + printf("Relative Distance: %f%%\n", + (mr[i].dist_*distThreshold*100)/mr.costThresh_); + printf("dist: %d\n", mr[i].dist_); + printf("distThreshold: %f\n", distThreshold); + printf("costThresh: %d\n", mr.costThresh_); + } + } + } + if (mr.elem_.IsEmpty() || (mr.bestDist_ > mr.costThresh_)) { + printf("p1FastMatch found no good matches.\n"); + } + } +#endif // DEBUG + + // return them in line, a js function will turn them into proper + // arrays. + nsresult rv = NS_OK; + nsCOMPtr retValues = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mr.elem_.IsEmpty() && (mr.bestDist_ <= mr.costThresh_)) { + for (PRUint32 i = 0; i < mr.elem_.Length(); i++) { + if (mr.elem_[i].dist_ > mr.costThresh_) + continue; + nsCOMPtr beg = + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr end = + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + beg->SetData(mr.elem_[i].matchBeg_); + end->SetData(mr.elem_[i].matchEnd_); +#ifdef DEBUG + cout << "Appending 2 elements\n"; +#endif + retValues->AppendElement(beg, PR_FALSE); + retValues->AppendElement(end, PR_FALSE); + } + } + + // elements are not appended but length is 2! + NS_ADDREF(*outArray = retValues); + return NS_OK; + +} + +NS_IMETHODIMP +Module::P1FastMatch(const nsAString & p, const nsAString & s, + float distThreshold, nsIArray ** outArray) { + MatchRes mr; + return P1FastMatch(p, s, distThreshold, mr, outArray); +} + +NS_IMETHODIMP +Module::P1FastMatchReverse(const nsAString & s, const nsAString & p, + float distThreshold, nsIArray ** outArray) { + + MatchRes mr; + nsresult rv = P1FastMatch(p, s, distThreshold, mr, outArray); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr retValues = do_QueryInterface(*outArray); + PRUint32 length; + rv = retValues->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + + + for (PRUint32 i = 0; i < length/2; ++i) { + nsCOMPtr beg = + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + nsCOMPtr end = + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + PRUint32 len = strlen((char*) mr[i].opmap_); + PRUint32 begi, endi; + for (begi = 0; begi < len; begi++) + if (mr[i].opmap_[begi] != SUBST && + mr[i].opmap_[begi] != DELETE) + break; + for (endi = len; endi > 0; endi--) + if (mr[i].opmap_[endi-1] != SUBST && + mr[i].opmap_[endi-1] != DELETE) + break; + + beg->SetData(begi); + end->SetData(endi); + +#ifdef DEBUG + cout << "Appending 2 elements to ReverseMatch\n"; +#endif + retValues->ReplaceElementAt(beg, i*2, PR_FALSE); + retValues->ReplaceElementAt(end, i*2+1, PR_FALSE); + } + + return NS_OK; +} + +#define DEFAULT_COST 20 + +#define IS_ASCII(u) ((u) < 0x80) + + +// We map x -> x, except for upper-case letters, + // which we map to their lower-case equivalents. +const PRUint8 gASCIIToLower [128] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, +}; + +// this version does not put non-ascii chars to lowercase +NS_ALWAYS_INLINE PRUnichar +ASCIIToLowerCase(PRUnichar aChar) +{ + if (IS_ASCII(aChar)) + return gASCIIToLower[aChar]; + else + return aChar; +} + +PRUint8 DistMetric::GetICost(PRUnichar sc) { + return DEFAULT_COST; +} + +PRUint8 DistMetric::GetDCost(PRUnichar pc) { + return DEFAULT_COST; +} + +PRUint8 DistMetric::GetSCost(PRUnichar pc, PRUnichar sc) { + // in this case, normalization might not be very useful... + if (ASCIIToLowerCase(pc) == ASCIIToLowerCase(sc)) + return 0; + else + return ( (GetICost(sc) + GetDCost(pc)) * 3) / 4; +} + +PRUnichar DistMetric::GetNorm(PRUnichar c) { + return ASCIIToLowerCase(c); +} + +const PRUnichar DistVector::NOT_ASCII = PRUnichar(~0x007F); + +bool DistVector::IsASCII(PRUnichar c) { + //return false; + return !(c & DistVector::NOT_ASCII); +} + +PRUint8 DistVector::GetASCII(PRUnichar c) { + return c; +} + +DistVector::DistVector() { + mMap.Init(); + memset(mArray, 0, sizeof(PRInt32)*128); +} + +PRInt32 DistVector::GetDiff(PRUnichar c) { + if (IsASCII(c)) + return mArray[GetASCII(c)]; + else + return mMap.Get(c); +} + +void DistVector::SetDiff(PRUnichar c, PRInt32 val) { + if (IsASCII(c)) + mArray[GetASCII(c)] = val; + else + mMap.Put(c, val); +} + +static PLDHashOperator +PrintVector(const PRUnichar& aKey, PRInt32 aData, void *aUserArg) { + printf("%lc: %d\n", aKey, aData); + return PL_DHASH_NEXT; +} + + +void DistVector::Print() { + printf("DIFFVEC\n"); + for (int i = 0; i < 128; i++) + if (mArray[i] > 0) + printf("arr -- %c: %d\n", i, mArray[i]); + mMap.EnumerateRead(PrintVector, this); +} + + +} +} + +NS_DEFINE_NAMED_CID(NS_TAINTINFERENCE_CID); + +static const mozilla::Module::CIDEntry kTaintCIDs[] = { + { &kNS_TAINTINFERENCE_CID, false, NULL, mozilla::taintinference::ModuleConstructor }, + { NULL } +}; + +static const mozilla::Module::ContractIDEntry kTaintContracts[] = { + { NS_TAINTINFERENCE_CONTRACTID, &kNS_TAINTINFERENCE_CID }, + { NULL } +}; + +static const mozilla::Module kTaintModule = { + mozilla::Module::kVersion, + kTaintCIDs, + kTaintContracts +}; + +NSMODULE_DEFN(taintinference) = &kTaintModule; diff --git a/toolkit/components/taintinference/nsTaintInference.h b/toolkit/components/taintinference/nsTaintInference.h new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/nsTaintInference.h @@ -0,0 +1,160 @@ +/* -*- 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): + * S Sarath Kumar + * + * 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 ***** */ + +#ifndef COMPONENTS_TAINTINFERENCE_H +#define COMPONENTS_TAINTINFERENCE_H + +#include "nsITaintInference.h" +#include "nsTArray.h" +#include "nsDataHashtable.h" + +class nsIArray; +class nsAString; + +namespace mozilla { +namespace taintinference { + + +/** + * MatchRes, DistMetric and DistVector are used by the approximate + * substring matching algorithm, which is implemented in + * p1FastMatch/p1Match. + */ +class MatchResElem { + public: + PRInt32 dist_; // edit distance + PRUint32 matchBeg_; // start and + PRUint32 matchEnd_; // end of match + PRUint8 *opmap_; // table of edit operator costs + + public: + MatchResElem(PRUint32 d, PRUint32 b, PRUint32 e, PRUint8 *opmap) + {dist_ = d; matchBeg_ = b; matchEnd_ = e; opmap_ = opmap; }; + PRBool operator==(const MatchResElem& other) const + { return dist_ == other.dist_ && matchBeg_ == other.matchBeg_ && + matchEnd_ == other.matchEnd_; }; // don't compare opmap for testing + // are we leaking mem with opmap? +}; + + +typedef nsTArray Matches; + +/** + * Represents a Match Result as returned by p1FastMach. + */ +class MatchRes { + public: + PRInt32 costThresh_; // minimum edit distance for matches + PRInt32 bestDist_; // best obseved edit distance in the matches found + Matches elem_; // list of matches found + PRBool HasMatches() { return !elem_.IsEmpty(); }; + MatchResElem& operator[] (PRUint32 i) { return elem_.ElementAt(i); }; + PRUint32 Length() const { return elem_.Length(); }; + void ClearInvalidMatches(double threshold); +}; + +class DistMetric { + public: + static PRUint8 GetICost(PRUnichar sc); + static PRUint8 GetDCost(PRUnichar pc); + static PRUint8 GetSCost(PRUnichar pc, PRUnichar sc); + static PRUnichar GetNorm(PRUnichar c); +}; + +class nsUnicharHashKey : public PLDHashEntryHdr +{ +public: + typedef const PRUnichar& KeyType; + typedef const PRUnichar* KeyTypePointer; + + nsUnicharHashKey(KeyTypePointer aKey) : mValue(*aKey) { } + nsUnicharHashKey(const nsUnicharHashKey& toCopy) : mValue(toCopy.mValue) { } + ~nsUnicharHashKey() { } + + KeyType GetKey() const { return mValue; } + PRBool KeyEquals(KeyTypePointer aKey) const { return *aKey == mValue; } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { return *aKey; } + enum { ALLOW_MEMMOVE = PR_TRUE }; + +private: + const PRUnichar mValue; +}; +// why can't we simply template the entryHeader as PLDHashEntryHdr? +typedef nsDataHashtable UnicharMap; +typedef nsDataHashtableMT DomainMap; + + +class DistVector { +public: + DistVector(); + PRInt32 GetDiff(PRUnichar c); + void SetDiff(PRUnichar c, PRInt32 val); + //PRInt32& operator[] (PRUnichar c); can't get a reference to nsDataHashtable! + void Print(); + static bool IsASCII(PRUnichar c); + static PRUint8 GetASCII(PRUnichar c); +private: + UnicharMap mMap; + PRInt32 mArray[128]; // fast path for ASCII chars + static const PRUnichar NOT_ASCII; +}; + + +class Module : public nsITaintInference +{ +public: + Module(); + + NS_DECL_ISUPPORTS + NS_DECL_NSITAINTINFERENCE + +private: + ~Module(); + nsresult P1FastMatch(const nsAString & p, const nsAString & s, + float distThreshold, MatchRes &mr, nsIArray ** outArray); + + bool noFast; +}; + + + +} +} +#endif diff --git a/toolkit/components/taintinference/tests/Makefile.in b/toolkit/components/taintinference/tests/Makefile.in new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/tests/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 Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2007 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Ryan Flint (Original author) +# +# 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 ***** + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = toolkit/components/taintinference/tests + +include $(DEPTH)/config/autoconf.mk + +MODULE = test_taintinference + +XPCSHELL_TESTS = unit + +include $(topsrcdir)/config/rules.mk diff --git a/toolkit/components/taintinference/tests/test_taintinference.js b/toolkit/components/taintinference/tests/test_taintinference.js new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/tests/test_taintinference.js @@ -0,0 +1,153 @@ +var Cu = Components.utils; +var Cc = Components.classes; +Cu.import("resource://gre/modules/TaintInference.jsm"); + +var JSON = Cc["@mozilla.org/dom/json;1"] + .createInstance(Components.interfaces.nsIJSON); + +// should not work +function do_array_eq(a1, a2) { + do_check_eq(JSON.encode(a1), JSON.encode(a2)); +} + +var prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); +prefs.setBoolPref("security.csp.debug", true); + + +// steal some stuff from test_CSPUtils +var tests = []; +function test(fcn) { + tests.push(fcn); +} + +test( + // check whether noFast yields equal results + function test_noFast() { + var str1 = "this string shold be found"; + var str2 = "hey, this string should be found here"; + var val = p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], to_array(val)); + setFast(true); + var str1 = "this string shold be found"; + var str2 = "hey, this string should be found here"; + var val = p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], to_array(val)); + setFast(false); + } +); + +test( + // basic functionality + function test_P1Match() { + var str1 = "this string should be found"; + var str2 = "hey, this string should be found here"; + var val = p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], to_array(val)); + // var alt = p1Match(str1, str2, 0.2); + // do_array_eq(alt, to_array(val)); + // val = p1FastMatch("zollo", "zilllo", 0.1); + // do_array_eq(null, to_array(val)); + // alt = p1Match("zilllo", "zollo", 0.1); + // do_array_eq(null, alt); + val = p1FastMatch("zillo", "zollozillozollo", 0.3); + do_array_eq([[5, 10]], to_array(val)); + str1 = "simple string"; + str2 = "look into the simple string"; + val = p1FastMatch(str1, str2, 0.3); + do_array_eq([[14, 27]], to_array(val)); + } +); + +test( + // internal scripts + function test_P1Match2() { + var str1 = "'; do_XSS() //"; + var str2 = "var i = 3; query = 'abc'; do_XSS() //';"; + var val = p1FastMatch(str1, str2, 0.3); + do_array_eq([[23, 37]], to_array(val)); + } +); + +test( + // urls + function test_P1Match3() { + var str1 = "index.php/../../script.js"; + var str2 = "http://abc.com/index.php/../../script.js"; + var val = p1FastMatch(str1, str2, 0.1); + do_array_eq([[15, 40]], to_array(val)); + } +); + +test( + // unicode chars + function test_P1Match4() { + var str1 = "John lát egy almát"; + var str2 = "John lat egy almat"; + // dump(str1.length + " " + str2.length + "\n"); + // var val = p1FastMatch(str1, str1, 0.1); + // do_array_eq([[0, 20]], to_array(val)); + // val = p1FastMatch(str1, str2, 0.3); + // do_array_eq([[0, 20]], to_array(val)); + // TODO: is it possible that this is utf-8? + str1 = "Says 施氏食狮史!"; + str2 = "The poet says 施氏獅獅史"; + val = p1FastMatch(str1, str1, 0.1); + //do_array_eq([0, 11], to_array(val)); + val = p1FastMatch(str1, str2, 0.5); + //do_array_eq([9, 28], to_array(val)); + } +); + +// multiple matches + +// edit distance (escaping) + +// reverse +test( + function test_P1Match4() { + var str1 = "dHello!f"; + var str2 = "ffvsffHello!This is the string"; + val = p1FastMatchReverse(str2, str1, 0.3); + do_array_eq([[0, 8]], to_array(val)); + str1 = "document.title = 'Yes';"; + str2 = ''; + val = p1FastMatchReverse(str2, str1, 0.3); + do_array_eq([[0, 23]], to_array(val)); + } +); + +test( + function test_p1fast() { + var p = ""; + var s = "Today's winner is Philip. The title is " + p + + ". Philip has got 45345 votes."; + var val = p1FastMatch(p, s, 0.3); + do_array_eq([[39, 39 + p.length]], to_array(val)); + p = ""; + // what the hell is pruning? + s = "Hello! My task is to test whether P1Match pruning works properly. It is important to do () pruning properly for performance reasons."; + val = p1FastMatch(p, s, 0.3); + do_array_eq([[89, 114]], to_array(val)); + p = "ginopino"; + s = "GINOPINO"; + val = p1FastMatch(p, s, 0.1); + do_array_eq([[0, 8]], to_array(val)); + } +); + +test( + function test_jsapibug() { + var p = "http://www.google.com/jsapi"; + var s = "url=http://www.google.com/friendconnect/gadgets/wall.xml&container=peoplesense&parent=http://www.rgagnon.com/&mid=0&view=profile&d=0.558.7&lang=en&communityId=09315574661186500794&caller=http://www.rgagnon.com/jsdetails/js-0083.html#st=e=AOG8GaDOHd+OR+IfkOXJfiA5wmeC49lo4lc+rdD1LN3H9NdfF+r9ioRXuraCflYcOzzJ+Rdc/qYl+ZHMP3xonNPe4uNzSCz+EKJVYFa+VXYkw1g1gWcFGd1jEAPYQsXGLmWrZ92lpjOwLp2EASc1awxliiP9XcZSSpyfJSlikfeovvtL0WCuJVxJ0sh0cfYobDLKHY2+BNgqGd50RjPhgVES0W8s4O2ug5oVCJhDwFYlB1nACS3mEhlEtOl0zpr7Fx6z9E45IMrf&c=peoplesense&rpctoken=1221214634&"; + + } +); + +function run_test() { + for(let i in tests) { + tests[i](); + } + + do_test_finished(); +} diff --git a/toolkit/components/taintinference/tests/unit/test_taintinference.js b/toolkit/components/taintinference/tests/unit/test_taintinference.js new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/tests/unit/test_taintinference.js @@ -0,0 +1,154 @@ +var Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; + +Cu.import("resource://gre/modules/TaintInference.jsm"); + +var JSON = Cc["@mozilla.org/dom/json;1"] + .createInstance(Components.interfaces.nsIJSON); + +// should not work +function do_array_eq(a1, a2) { + do_check_eq(JSON.encode(a1), JSON.encode(a2)); +} + +var prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); +prefs.setBoolPref("security.csp.debug", true); + + +// steal some stuff from test_CSPUtils +var tests = []; +function test(fcn) { + tests.push(fcn); +} + +test( + // check whether noFast yields equal results + function test_noFast() { + var str1 = "this string shold be found"; + var str2 = "hey, this string should be found here"; + var val = p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], val); + setFast(true); + var str1 = "this string shold be found"; + var str2 = "hey, this string should be found here"; + var val = p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], val); + setFast(false); + } +); + +test( + // basic functionality + function test_P1Match() { + var str1 = "this string should be found"; + var str2 = "hey, this string should be found here"; + var val = p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], val); + // var alt = p1Match(str1, str2, 0.2); + // do_array_eq(alt, val); + // val = p1FastMatch("zollo", "zilllo", 0.1); + // do_array_eq(null, val); + // alt = p1Match("zilllo", "zollo", 0.1); + // do_array_eq(null, alt); + val = p1FastMatch("zillo", "zollozillozollo", 0.3); + do_array_eq([[5, 10]], val); + str1 = "simple string"; + str2 = "look into the simple string"; + val = p1FastMatch(str1, str2, 0.3); + do_array_eq([[14, 27]], val); + } +); + +test( + // internal scripts + function test_P1Match2() { + var str1 = "'; do_XSS() //"; + var str2 = "var i = 3; query = 'abc'; do_XSS() //';"; + var val = p1FastMatch(str1, str2, 0.3); + do_array_eq([[23, 37]], val); + } +); + +test( + // urls + function test_P1Match3() { + var str1 = "index.php/../../script.js"; + var str2 = "http://abc.com/index.php/../../script.js"; + var val = p1FastMatch(str1, str2, 0.1); + do_array_eq([[15, 40]], val); + } +); + +test( + // unicode chars + function test_P1Match4() { + var str1 = "John lát egy almát"; + var str2 = "John lat egy almat"; + // dump(str1.length + " " + str2.length + "\n"); + // var val = p1FastMatch(str1, str1, 0.1); + // do_array_eq([[0, 20]], val); + // val = p1FastMatch(str1, str2, 0.3); + // do_array_eq([[0, 20]], val); + // TODO: is it possible that this is utf-8? + str1 = "Says 施氏食狮史!"; + str2 = "The poet says 施氏獅獅史"; + val = p1FastMatch(str1, str1, 0.1); + //do_array_eq([0, 11], val); + val = p1FastMatch(str1, str2, 0.5); + //do_array_eq([9, 28], val); + } +); + +// multiple matches + +// edit distance (escaping) + +// reverse +test( + function test_P1Match4() { + var str1 = "dHello!f"; + var str2 = "ffvsffHello!This is the string"; + val = p1FastMatchReverse(str2, str1, 0.3); + do_array_eq([[1, 7]], val); + str1 = "document.title = 'Yes';"; + str2 = ''; + val = p1FastMatchReverse(str2, str1, 0.3); + do_array_eq([[0, 23]], val); + } +); + +test( + function test_p1fast() { + var p = ""; + var s = "Today's winner is Philip. The title is " + p + + ". Philip has got 45345 votes."; + var val = p1FastMatch(p, s, 0.3); + do_array_eq([[39, 39 + p.length]], val); + p = ""; + // what the hell is pruning? + s = "Hello! My task is to test whether P1Match pruning works properly. It is important to do () pruning properly for performance reasons."; + val = p1FastMatch(p, s, 0.3); + do_array_eq([[89, 114]], val); + p = "ginopino"; + s = "GINOPINO"; + val = p1FastMatch(p, s, 0.1); + do_array_eq([[0, 8]], val); + } +); + +test( + function test_jsapibug() { + var p = "http://www.google.com/jsapi"; + var s = "url=http://www.google.com/friendconnect/gadgets/wall.xml&container=peoplesense&parent=http://www.rgagnon.com/&mid=0&view=profile&d=0.558.7&lang=en&communityId=09315574661186500794&caller=http://www.rgagnon.com/jsdetails/js-0083.html#st=e=AOG8GaDOHd+OR+IfkOXJfiA5wmeC49lo4lc+rdD1LN3H9NdfF+r9ioRXuraCflYcOzzJ+Rdc/qYl+ZHMP3xonNPe4uNzSCz+EKJVYFa+VXYkw1g1gWcFGd1jEAPYQsXGLmWrZ92lpjOwLp2EASc1awxliiP9XcZSSpyfJSlikfeovvtL0WCuJVxJ0sh0cfYobDLKHY2+BNgqGd50RjPhgVES0W8s4O2ug5oVCJhDwFYlB1nACS3mEhlEtOl0zpr7Fx6z9E45IMrf&c=peoplesense&rpctoken=1221214634&"; + } +); + +function run_test() { + for(let i in tests) { + tests[i](); + } + + do_test_finished(); +} diff --git a/toolkit/components/taintinference/tests/unit/xpcshell.ini b/toolkit/components/taintinference/tests/unit/xpcshell.ini new file mode 100644 --- /dev/null +++ b/toolkit/components/taintinference/tests/unit/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = +tail = + +[test_taintinference.js] diff --git a/toolkit/library/libxul-config.mk b/toolkit/library/libxul-config.mk --- a/toolkit/library/libxul-config.mk +++ b/toolkit/library/libxul-config.mk @@ -156,6 +156,7 @@ COMPONENT_LIBS += \ jsperf \ gkplugin \ + taintinference \ $(NULL) ifdef MOZ_XUL diff --git a/toolkit/library/nsStaticXULComponents.cpp b/toolkit/library/nsStaticXULComponents.cpp --- a/toolkit/library/nsStaticXULComponents.cpp +++ b/toolkit/library/nsStaticXULComponents.cpp @@ -274,6 +274,7 @@ MOZ_APP_COMPONENT_MODULES \ MODULE(nsTelemetryModule) \ MODULE(jsdebugger) \ + MODULE(taintinference) \ /* end of list */ #define MODULE(_name) \ diff --git a/xpcom/tests/unit/test_filter.js b/xpcom/tests/unit/test_filter.js new file mode 100644 --- /dev/null +++ b/xpcom/tests/unit/test_filter.js @@ -0,0 +1,176 @@ +var Ci = Components.interfaces; + +var Cu = Components.utils; +Cu.import("resource://gre/modules/XSSUtils.jsm"); + +//netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); +var filter = Components.classes["@sunysb.edu/filter;1"].getService(); + + +var JSON = Components.classes["@mozilla.org/dom/json;1"] + .createInstance(Components.interfaces.nsIJSON); + +// should not work +function do_array_eq(a1, a2) { + do_check_eq(JSON.encode(a1), JSON.encode(a2)); +} + +var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); +prefs.setBoolPref("security.csp.debug", true); + + +// function to_array(xpcomarray) { +// var results = []; +// for (var i = 0; i < xpcomarray.length; ++i) { +// var value1 = xpcomarray.queryElementAt(i, Ci.nsISupportsPRUint32); +// ++i; +// var value2 = xpcomarray.queryElementAt(i, Ci.nsISupportsPRUint32); +// results.push([value1.data, value2.data]); +// } +// return results; +// } + +function convertjs(jsresult) { + if (jsresult != null) + return [[jsresult[0], jsresult[1]]]; + else + return []; +} + +// steal some stuff from test_CSPUtils +var tests = []; +function test(fcn) { + tests.push(fcn); +} + +test( + // check whether noFast yields equal results + function test_noFast() { + var str1 = "this string shold be found"; + var str2 = "hey, this string should be found here"; + var val = filter.p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], to_array(val)); + filter.noFast = true; + var str1 = "this string shold be found"; + var str2 = "hey, this string should be found here"; + var val = filter.p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], to_array(val)); + filter.noFast = false; + } +); + +test( + // basic functionality + function test_P1Match() { + var str1 = "this string should be found"; + var str2 = "hey, this string should be found here"; + var val = filter.p1FastMatch(str1, str2, 0.2); + do_array_eq([[5, 32]], to_array(val)); + // var alt = p1Match(str1, str2, 0.2); + // do_array_eq(alt, to_array(val)); + // val = filter.p1FastMatch("zollo", "zilllo", 0.1); + // do_array_eq(null, to_array(val)); + // alt = p1Match("zilllo", "zollo", 0.1); + // do_array_eq(null, alt); + val = filter.p1FastMatch("zillo", "zollozillozollo", 0.3); + do_array_eq([[5, 10]], to_array(val)); + str1 = "simple string"; + str2 = "look into the simple string"; + val = filter.p1FastMatch(str1, str2, 0.3); + do_array_eq([[14, 27]], to_array(val)); + } +); + +test( + // internal scripts + function test_P1Match2() { + var str1 = "'; do_XSS() //"; + var str2 = "var i = 3; query = 'abc'; do_XSS() //';"; + var val = filter.p1FastMatch(str1, str2, 0.3); + do_array_eq([[23, 37]], to_array(val)); + } +); + +test( + // urls + function test_P1Match3() { + var str1 = "index.php/../../script.js"; + var str2 = "http://abc.com/index.php/../../script.js"; + var val = filter.p1FastMatch(str1, str2, 0.1); + do_array_eq([[15, 40]], to_array(val)); + } +); + +test( + // unicode chars + function test_P1Match4() { + var str1 = "John lát egy almát"; + var str2 = "John lat egy almat"; + // dump(str1.length + " " + str2.length + "\n"); + // var val = filter.p1FastMatch(str1, str1, 0.1); + // do_array_eq([[0, 20]], to_array(val)); + // val = filter.p1FastMatch(str1, str2, 0.3); + // do_array_eq([[0, 20]], to_array(val)); + // TODO: is it possible that this is utf-8? + str1 = "Says 施氏食狮史!"; + str2 = "The poet says 施氏獅獅史"; + val = filter.p1FastMatch(str1, str1, 0.1); + //do_array_eq([0, 11], to_array(val)); + val = filter.p1FastMatch(str1, str2, 0.5); + //do_array_eq([9, 28], to_array(val)); + } +); + +// multiple matches + +// edit distance (escaping) + +// reverse +test( + function test_P1Match4() { + var str1 = "dHello!f"; + var str2 = "ffvsffHello!This is the string"; + val = filter.p1FastMatchReverse(str2, str1, 0.3); + do_array_eq([[0, 8]], to_array(val)); + str1 = "document.title = 'Yes';"; + str2 = ''; + val = filter.p1FastMatchReverse(str2, str1, 0.3); + do_array_eq([[0, 23]], to_array(val)); + } +); + +test( + function test_p1fast() { + var p = ""; + var s = "Today's winner is Philip. The title is " + p + + ". Philip has got 45345 votes."; + var val = filter.p1FastMatch(p, s, 0.3); + do_array_eq([[39, 39 + p.length]], to_array(val)); + p = ""; + // what the hell is pruning? + s = "Hello! My task is to test whether P1Match pruning works properly. It is important to do () pruning properly for performance reasons."; + val = filter.p1FastMatch(p, s, 0.3); + do_array_eq([[89, 114]], to_array(val)); + p = "ginopino"; + s = "GINOPINO"; + val = filter.p1FastMatch(p, s, 0.1); + do_array_eq([[0, 8]], to_array(val)); + } +); + +test( + function test_jsapibug() { + var p = "http://www.google.com/jsapi"; + var s = "url=http://www.google.com/friendconnect/gadgets/wall.xml&container=peoplesense&parent=http://www.rgagnon.com/&mid=0&view=profile&d=0.558.7&lang=en&communityId=09315574661186500794&caller=http://www.rgagnon.com/jsdetails/js-0083.html#st=e=AOG8GaDOHd+OR+IfkOXJfiA5wmeC49lo4lc+rdD1LN3H9NdfF+r9ioRXuraCflYcOzzJ+Rdc/qYl+ZHMP3xonNPe4uNzSCz+EKJVYFa+VXYkw1g1gWcFGd1jEAPYQsXGLmWrZ92lpjOwLp2EASc1awxliiP9XcZSSpyfJSlikfeovvtL0WCuJVxJ0sh0cfYobDLKHY2+BNgqGd50RjPhgVES0W8s4O2ug5oVCJhDwFYlB1nACS3mEhlEtOl0zpr7Fx6z9E45IMrf&c=peoplesense&rpctoken=1221214634&"; + + var s = + } + +function run_test() { + for(let i in tests) { + tests[i](); + } + + do_test_finished(); +} diff --git a/xpcom/tests/unit/xpcshell.ini b/xpcom/tests/unit/xpcshell.ini --- a/xpcom/tests/unit/xpcshell.ini +++ b/xpcom/tests/unit/xpcshell.ini @@ -40,3 +40,4 @@ # Bug 676998: test fails consistently on Android fail-if = os == "android" [test_versioncomparator.js] +[test_filter.js]