diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -309,6 +309,7 @@ @BINPATH@/components/nsWebHandlerApp.js @BINPATH@/components/nsBadCertHandler.js @BINPATH@/components/nsFormAutoComplete.js +@BINPATH@/components/contentSecurityPolicy.js #ifdef XP_MACOSX @BINPATH@/components/libalerts_s.dylib #endif diff --git a/caps/idl/nsIPrincipal.idl b/caps/idl/nsIPrincipal.idl --- a/caps/idl/nsIPrincipal.idl +++ b/caps/idl/nsIPrincipal.idl @@ -47,6 +47,7 @@ struct JSPrincipals; %} interface nsIURI; +interface IContentSecurityPolicy; [ptr] native JSContext(JSContext); [ptr] native JSPrincipals(JSPrincipals); @@ -241,4 +242,9 @@ interface nsIPrincipal : nsISerializable * one, this will return null. Getting this attribute never throws. */ readonly attribute nsISupports certificate; + + /** + * A Content Security Policy associated with this principal. + */ + [noscript] attribute IContentSecurityPolicy csp; }; diff --git a/caps/include/nsPrincipal.h b/caps/include/nsPrincipal.h --- a/caps/include/nsPrincipal.h +++ b/caps/include/nsPrincipal.h @@ -138,6 +138,7 @@ protected: DomainPolicy* mSecurityPolicy; + nsCOMPtr mCSP; nsCOMPtr mCodebase; nsCOMPtr mDomain; PRPackedBool mTrusted; 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,10 @@ private: jsval id, JSAccessMode mode, jsval *vp); + // Decides, based on CSP, whether or not eval() and stuff can be executed. + static JSBool + ContentSecurityPolicyPermitsJSAction(JSContext *cx); + // Returns null if a principal cannot be found; generally callers // should error out at that point. static nsIPrincipal* diff --git a/caps/src/nsNullPrincipal.cpp b/caps/src/nsNullPrincipal.cpp --- a/caps/src/nsNullPrincipal.cpp +++ b/caps/src/nsNullPrincipal.cpp @@ -252,6 +252,20 @@ nsNullPrincipal::GetURI(nsIURI** aURI) } NS_IMETHODIMP +nsNullPrincipal::GetCsp(IContentSecurityPolicy** aCsp) +{ + *aCsp = nsnull; + return NS_OK; +} + +NS_IMETHODIMP +nsNullPrincipal::SetCsp(IContentSecurityPolicy* aCsp) +{ + // CSP on a null principal makes no sense + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsNullPrincipal::GetDomain(nsIURI** aDomain) { return NS_EnsureSafeToReturn(mURI, aDomain); diff --git a/caps/src/nsPrincipal.cpp b/caps/src/nsPrincipal.cpp --- a/caps/src/nsPrincipal.cpp +++ b/caps/src/nsPrincipal.cpp @@ -57,6 +57,7 @@ #include "nsIPrefService.h" #include "nsIClassInfoImpl.h" #include "nsDOMError.h" +#include "IContentSecurityPolicy.h" #include "nsPrincipal.h" @@ -775,6 +776,25 @@ nsPrincipal::GetCertificate(nsISupports* } NS_IMETHODIMP +nsPrincipal::GetCsp(IContentSecurityPolicy** aCsp) +{ + if (mCSP) { + NS_IF_ADDREF(*aCsp = mCSP); + } + else { + *aCsp = nsnull; + } + return NS_OK; +} + +NS_IMETHODIMP +nsPrincipal::SetCsp(IContentSecurityPolicy* aCsp) +{ + mCSP = aCsp; + return NS_OK; +} + +NS_IMETHODIMP nsPrincipal::GetHashValue(PRUint32* aValue) { NS_PRECONDITION(mCert || mCodebase, "Need a cert or codebase"); diff --git a/caps/src/nsScriptSecurityManager.cpp b/caps/src/nsScriptSecurityManager.cpp --- a/caps/src/nsScriptSecurityManager.cpp +++ b/caps/src/nsScriptSecurityManager.cpp @@ -94,6 +94,7 @@ #include "nsCDefaultURIFixup.h" #include "nsIChromeRegistry.h" #include "nsPrintfCString.h" +#include "IContentSecurityPolicy.h" static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID); @@ -514,6 +515,61 @@ NS_IMPL_ISUPPORTS5(nsScriptSecurityManag /////////////////////////////////////////////////// ///////////////// Security Checks ///////////////// +JSBool +nsScriptSecurityManager::ContentSecurityPolicyPermitsJSAction(JSContext *cx) +{ + // 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* sourcePrincipal = ssm->GetSubjectPrincipal(cx, &rv); + + if (NS_FAILED(rv)) + { + NS_WARNING("CSP: Failed to get nsIPrincipal from js context\n"); + return JS_FALSE; // Not just absence of principal, but failure. + } + + if (!sourcePrincipal) + { + NS_WARNING("Checking for CSP on eval w/o principals; should this happen?"); + return JS_TRUE; //no principal? might as well allow eval + } + + nsCOMPtr csp; + rv = sourcePrincipal->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; + + ////XXX: This stack push and associated pop went away once the security check + //// was removed from JS (Bug 515475, rev 4448e1b581b7) + //nsCxPusher pusher; + //pusher.PushNull(); + + PRBool evalOK = PR_TRUE; + // this call will send violation reports as warranted (and return true if + // reportOnly is set). + rv = csp->GetAllowsEval(&evalOK); + + if (NS_FAILED(rv)) + { + NS_WARNING("CSP: failed to get allowsEval"); + return JS_TRUE; // fail open to not break sites. + } + + //pusher.Pop(); //-removed as of rev 4448e1b581b7 + + return evalOK; +} + + JSBool nsScriptSecurityManager::CheckObjectAccess(JSContext *cx, JSObject *obj, jsval id, JSAccessMode mode, @@ -3396,7 +3452,8 @@ nsresult nsScriptSecurityManager::Init() static JSSecurityCallbacks securityCallbacks = { CheckObjectAccess, NULL, - NULL + NULL, + ContentSecurityPolicyPermitsJSAction }; #ifdef DEBUG diff --git a/caps/src/nsSystemPrincipal.cpp b/caps/src/nsSystemPrincipal.cpp --- a/caps/src/nsSystemPrincipal.cpp +++ b/caps/src/nsSystemPrincipal.cpp @@ -224,6 +224,21 @@ nsSystemPrincipal::GetHasCertificate(PRB } NS_IMETHODIMP +nsSystemPrincipal::GetCsp(IContentSecurityPolicy** aCsp) +{ + *aCsp = nsnull; + return NS_OK; +} + +NS_IMETHODIMP +nsSystemPrincipal::SetCsp(IContentSecurityPolicy* aCsp) +{ + // CSP on a null principal makes no sense + return NS_OK; +} + + +NS_IMETHODIMP nsSystemPrincipal::GetDomain(nsIURI** aDomain) { *aDomain = nsnull; diff --git a/content/base/public/IContentSecurityPolicy.idl b/content/base/public/IContentSecurityPolicy.idl new file mode 100644 --- /dev/null +++ b/content/base/public/IContentSecurityPolicy.idl @@ -0,0 +1,151 @@ +/* ***** 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 IDL definition. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * + * Contributor(s): + * Sid Stamm + * Brandon Sterne + * Daniel Veditz + * + * 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 ***** */ + +#include "nsISupports.idl" + +interface nsIURI; +interface nsIHttpChannel; +interface nsIDocShell; + +/** + * IContentSecurityPolicy + * Describes an XPCOM component used to model an enforce CSPs. + */ +[scriptable, uuid(AB36A2BF-CB32-4AA6-AB41-6B4E4444A221)] +interface IContentSecurityPolicy : nsISupports +{ + + /** + * Set to true when the CSP has been read in and parsed and is ready to + * enforce. This is a barrier for the nsDocument so it doesn't load any + * sub-content until either it knows that a CSP is ready or will not be used. + */ + attribute boolean isInitialized; + + /** + * When set to true, content load-blocking and fail-closed are disabled: CSP + * will ONLY send reports, and not modify behavior. + */ + attribute boolean reportOnlyMode; + + /** + * A read-only string version of the policy for debugging. + */ + readonly attribute AString policy; + + /** + * Whether this policy allows in-page script. + * + * Calls to this may trigger violation reports when queried, so + * this value should not be cached. + */ + readonly attribute boolean allowsInlineScript; + + /** + * whether this policy allows eval and eval-like functions + * such as setTimeout("code string", time). + * + * Calls to this may trigger violation reports when queried, so + * this value should not be cached. + */ + readonly attribute boolean allowsEval; + + /** + * Manually triggers violation report sending given a URI and reason. + * The URI may be null, in which case "self" is sent. + * @param blockedURI + * the URI that violated the policy + * @param violatedDirective + * the directive that was violated. + * @return + * nothing. + */ + void sendReports(in AString blockedURI, in AString violatedDirective); + + /** + * Called after the CSP object is created to fill in the appropriate request + * and request header information needed in case a report needs to be sent. + */ + void scanRequestData(in nsIHttpChannel aChannel); + + /** + * Updates the policy currently stored in the CSP to be "refined" or + * tightened by the one specified in the string policyString. + */ + void refinePolicy(in AString policyString, in nsIURI selfURI); + + /** + * Verifies ancestry as permitted by the policy. + * + * Calls to this may trigger violation reports when queried, so + * this value should not be cached. + * + * @param docShell + * containing the protected resource + * @return + * true if the frame's ancestors are all permitted by policy + */ + boolean permitsAncestry(in nsIDocShell docShell); + + /** + * 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. + * + * Calls to this may trigger violation reports when queried, so + * this value should not be cached. + */ + short shouldLoad(in unsigned long aContentType, + in nsIURI aContentLocation, + in nsIURI aRequestOrigin, + in nsISupports aContext, + in ACString aMimeTypeGuess, + in nsISupports aExtra); + + /** + * Delegate method called by the service when sub-elements of the protected + * document are being processed. Given a bit of information about the request, + * decides whether or not the policy is satisfied. + */ + short shouldProcess(in unsigned long aContentType, + in nsIURI aContentLocation, + in nsIURI aRequestOrigin, + in nsISupports aContext, + in ACString aMimeType, + in nsISupports aExtra); + +}; diff --git a/content/base/public/Makefile.in b/content/base/public/Makefile.in --- a/content/base/public/Makefile.in +++ b/content/base/public/Makefile.in @@ -114,6 +114,7 @@ XPIDLSRCS = \ nsIObjectLoadingContent.idl \ nsIFrameLoader.idl \ nsIXMLHttpRequest.idl \ + IContentSecurityPolicy.idl \ $(NULL) include $(topsrcdir)/config/rules.mk diff --git a/content/base/src/CSPUtils.jsm b/content/base/src/CSPUtils.jsm new file mode 100644 --- /dev/null +++ b/content/base/src/CSPUtils.jsm @@ -0,0 +1,1292 @@ +/* ***** 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): + * Sid Stamm + * + * 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 ***** */ + +/** + * Content Security Policy Utilities + * + * Overview + * This contains a set of classes and utilities for CSP. It is in this + * separate file for testing purposes. + */ + +// Module stuff +var EXPORTED_SYMBOLS = ["CSPRep", "CSPSourceList", "CSPSource", + "CSPHost", "CSPWarning", "CSPError", "CSPdebug"]; + + +//these are not exported +var gIoService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + +var gETLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"] + .getService(Components.interfaces.nsIEffectiveTLDService); + + +function CSPWarning(aMsg) { + //customize this to redirect output. + aMsg = 'CSP WARN: ' + aMsg + "\n"; + dump(aMsg); + Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService) + .logStringMessage(aMsg); +} +function CSPError(aMsg) { + aMsg = 'CSP ERROR: ' + aMsg + "\n"; + dump(aMsg); + Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService) + .logStringMessage(aMsg); +} +function CSPdebug(aMsg) { + aMsg = 'CSP debug: ' + aMsg + "\n"; + dump(aMsg); + Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService) + .logStringMessage(aMsg); +} + + +//:::::::::::::::::::::::: CLASSES ::::::::::::::::::::::::::// + +/** + * Class that represents a parsed policy structure. + */ +function CSPRep() { + // this gets set to true when the policy is done parsing, or when a + // URI-borne policy has finished loading. + this._isInitialized = false; + + this._allowEval = false; + this._allowInlineScripts = false; + + // don't auto-populate _directives, so it is easier to find bugs + this._directives = {}; + +} +CSPRep.SRC_DIRECTIVES = { + ALLOW: "allow", + SCRIPT_SRC: "script-src", + STYLE_SRC: "style-src", + MEDIA_SRC: "media-src", + IMG_SRC: "img-src", + OBJECT_SRC: "object-src", + FRAME_SRC: "frame-src", + FRAME_ANCESTORS: "frame-ancestors", + FONT_SRC: "font-src", + XHR_SRC: "xhr-src" +}; +CSPRep.URI_DIRECTIVES = { + REPORT_URI: "report-uri", /* list of URIs */ + POLICY_URI: "policy-uri" /* single URI */ +}; + +CSPRep.OPTIONS_DIRECTIVE = "options"; + + +/** + * Factory to create a new CSPRep, parsed from a string. + * + * @param aStr + * string rep of a CSP + * @param self (optional) + * string or CSPSource representing the "self" source + * @returns + * an instance of CSPRep + */ +CSPRep.fromString = function(aStr, self) { + var SD = CSPRep.SRC_DIRECTIVES; + var UD = CSPRep.URI_DIRECTIVES; + var aCSPR = new CSPRep(); + aCSPR._originalText = aStr; + + var dirs = aStr.split(";"); + +directive: + for each(var dir in dirs) { + dir = dir.replace(/^\s+/,'').replace(/\s+$/,''); + var dirname = dir.split(/\s+/)[0]; + var dirvalue = dir.substring(dirname.length) + .replace(/^\s+/,'') + .replace(/\s+$/,''); + + + // OPTIONS DIRECTIVE //////////////////////////////////////////////// + if(dirname === CSPRep.OPTIONS_DIRECTIVE) { + // grab value tokens and interpret them + var options = dirvalue.split(/\s+/); + for each (var opt in options) { + if(opt === "inline-script") + aCSPR._allowInlineScripts = true; + else if(opt === "eval-script") + aCSPR._allowEval = true; + else + CSPWarning("don't understand option '" + opt + "'. Ignoring it."); + } + continue directive; + } + + // SOURCE DIRECTIVES //////////////////////////////////////////////// + for each(var sdi in SD) { + if(dirname == sdi) { + // process dirs, and enforce that 'self' is defined. + var dv = CSPSourceList.fromString(dirvalue, self, true); + if(dv) { + aCSPR._directives[sdi] = dv; + continue directive; + } + } + } + + // REPORT URI /////////////////////////////////////////////////////// + if(dirname === UD.REPORT_URI) { + // might be space-separated list of URIs + var uriStrings = dirvalue.split(/\s+/); + var okUriStrings = []; + var selfUri = self ? gIoService.newURI(self.toString(),null,null) : null; + + // Verify that each report URI is in the same etld + 1 + // if "self" is defined, and just that it's valid otherwise. + for(let i in uriStrings) { + try { + var uri = gIoService.newURI(uriStrings[i]); + if(self) { + if( gETLDService.getBaseDomain(uri) === + gETLDService.getBaseDomain(selfUri) ) { + okUriStrings.push(uriStrings[i]); + } else { + CSPWarning("can't use report URI from non-matching eTLD+1: " + + gETLDService.getBaseDomain(uri)); + } + } + } catch(e) { + CSPWarning("couldn't parse report URI: " + dirvalue); + } + } + aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' '); + continue directive; + } + + // POLICY URI ////////////////////////////////////////////////////////// + if(dirname == UD.POLICY_URI) { + // POLICY_URI can only be alone + if(aCSPR._directives.length > 0 || dirs.length > 1) { + CSPError("policy-uri directive can only appear alone"); + return CSPRep.fromString("allow 'none'"); + } + + var uri = ''; + try { + uri = gIoService.newURI(dirvalue, null, null); + } catch(e) { + CSPError("could not parse URI in policy URI: " + dirvalue); + return CSPRep.fromString("allow 'none'"); + } + + // Verify that policy URI comes from the same origin + if(self) { + var selfUri = gIoService.newURI(self.toString(), null, null); + if(selfUri.host !== uri.host){ + CSPError("can't fetch policy uri from non-matching hostname: " + uri.host); + return CSPRep.fromString("allow 'none'"); + } + if(selfUri.port !== uri.port){ + CSPError("can't fetch policy uri from non-matching port: " + uri.port); + return CSPRep.fromString("allow 'none'"); + } + if(selfUri.scheme !== uri.scheme){ + CSPError("can't fetch policy uri from non-matching scheme: " + uri.scheme); + return CSPRep.fromString("allow 'none'"); + } + } + + var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Components.interfaces.nsIXMLHttpRequest); + + // insert error hook + req.onerror = CSPError; + + // synchronous -- otherwise we need to architect a callback into the + // xpcom component so that whomever creates the policy object gets + // notified when it's loaded and ready to go. + req.open("GET", dirvalue, false); + + // make request anonymous + // This prevents sending cookies with the request, in case the policy URI + // is injected, it can't be abused for CSRF. + req.channel.loadFlags |= Components.interfaces.nsIChannel.LOAD_ANONYMOUS; + + //CSPdebug("Fetching URI " + dirvalue); + req.send(null); + if(req.status == 200) { + //CSPdebug("XHR returned: " + req.responseText); + aCSPR = CSPRep.fromString(req.responseText, self); + //remember where we got the policy + aCSPR._directives[UD.POLICY_URI] = dirvalue; + return aCSPR; + } + CSPError("Error fetching policy URI: server response was " + req.status); + return CSPRep.fromString("allow 'none'"); + } + + // UNIDENTIFIED DIRECTIVE ///////////////////////////////////////////// + CSPWarning("Couldn't process unknown directive '" + dirname + "'"); + + } // end directive: loop + + aCSPR.makeExplicit(); + return aCSPR; +}; + +CSPRep.prototype = { + /** + * Returns array of all report uris defined, or 'none' if there are none. + */ + getReportURIs: + function() { + if(!this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI]) + return []; + return this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI]; + }, + + /** + * Compares this CSPRep instance to another. + */ + equals: + function(that) { + if(this._directives.length != that._directives.length) { + return false; + } + for(var i in this._directives) { + if(!that._directives[i] || !this._directives[i].equals(that._directives[i])) { + return false; + } + } + return (this.allowsInlineScripts == that.allowsInlineScripts) + && (this.allowsEvalInScripts == that.allowsEvalInScripts); + }, + + /** + * Generates string representation of the policy. Should be fairly similar + * to the original. + */ + toString: + function csp_toString() { + var dirs = []; + + if(this._allowEval || this._allowInlineScripts) { + dirs.push("options " + (this._allowEval ? "eval-script" : "") + + (this._allowInlineScripts ? "inline-script" : "")); + } + for(var i in this._directives) { + if(this._directives[i]) { + dirs.push(i + " " + this._directives[i].toString()); + } + } + return dirs.join("; "); + }, + + + /** + * Determines if this policy accepts a URI. + * @param aContext + * one of the SRC_DIRECTIVES defined above + * @returns + * true if the policy permits the URI in given context. + */ + permits: + function csp_permits(aURI, aContext) { + if(!aURI) return false; + + // GLOBALLY ALLOW "about:" SCHEME + if(aURI instanceof String && aURI.substring(0,6) == "about:") + return true; + if(aURI instanceof Components.interfaces.nsIURI && aURI.scheme == "about") + return true; + + //make sure the context is valid + for(var i in CSPRep.SRC_DIRECTIVES) { + if(CSPRep.SRC_DIRECTIVES[i] == aContext) { + return this._directives[aContext].permits(aURI); + } + } + return false; + }, + + /** + * Intersects with another CSPRep, deciding the subset policy + * that should be enforced, and returning a new instance. + * @param aCSPRep + * a CSPRep instance to use as "other" CSP + * @returns + * a new CSPRep instance of the intersection + */ + intersectWith: + function cspsd_intersectWith(aCSPRep) { + var newRep = new CSPRep(); + + for(var dir in CSPRep.SRC_DIRECTIVES) { + var dirv = CSPRep.SRC_DIRECTIVES[dir]; + newRep._directives[dirv] = this._directives[dirv] + .intersectWith(aCSPRep._directives[dirv]); + } + + newRep._allowEval = this.allowsEvalInScripts + && aCSPRep.allowsEvalInScripts; + + newRep._allowInlineScripts = this.allowsInlineScripts + && aCSPRep.allowsInlineScripts; + + return newRep; + }, + + + /** + * Copies default source list to each unspecified directive. + * @returns + * true if the makeExplicit succeeds + * false if it fails (for some weird reason) + */ + makeExplicit: + function cspsd_makeExplicit() { + var SD = CSPRep.SRC_DIRECTIVES; + var allowDir = this._directives[SD.ALLOW]; + if(!allowDir) { + return false; + } + + for(var dir in SD) { + var dirv = SD[dir]; + if(dirv == SD.ALLOW) continue; + if(!this._directives[dirv]) { + //implicit directive, make explicit + this._directives[dirv] = allowDir.clone(); + this._directives[dirv]._isImplicit = true; + } + } + this._isInitialized = true; + return true; + }, + + /** + * Returns true if "eval" is enabled through the "eval" keyword. + */ + get allowsEvalInScripts () { + return this._allowEval; + }, + + /** + * Returns true if inline scripts are enabled through the "inline" + * keyword. + */ + get allowsInlineScripts () { + return this._allowInlineScripts; + }, +}; + + +////////////////////////////////////////////////////////////////////// +/** + * Class to represent a list of sources + */ +function CSPSourceList() { + this._sources = []; + this._permitAllSources = false; + + // Set to true when this list is created using "makeExplicit()" + // It's useful to know this when reporting the directive that was violated. + this._isImplicit = false; +} + +/** + * Factory to create a new CSPSourceList, parsed from a string. + * + * @param aStr + * string rep of a CSP Source List + * @param self (optional) + * string or CSPSource representing the "self" source + * @param enforceSelfChecks (optional) + * if present, and "true", will check to be sure "self" has the + * appropriate values to inherit when they are omitted from the source. + * @returns + * an instance of CSPSourceList + */ +CSPSourceList.fromString = function(aStr, self, enforceSelfChecks) { + //Source list is: + // ::= + // | "'none'" + // ::= + // | " " + + var slObj = new CSPSourceList(); + if(aStr == "'none'") + return slObj; + + if(aStr == "*") { + slObj._permitAllSources = true; + return slObj; + } + + var tokens = aStr.split(/\s+/); + for(var i in tokens) { + if(tokens[i] === "") continue; + var src = CSPSource.create(tokens[i], self, enforceSelfChecks); + if(!src) { + CSPWarning("Failed to parse unrecoginzied source " + tokens[i]); + continue; + } + slObj._sources.push(src); + } + + return slObj; +}; + +CSPSourceList.prototype = { + + /** + * Compares one CSPSourceList to another. + * + * @param that + * another CSPSourceList + * @returns + * true if they have the same data + */ + equals: + function(that) { + if(that._sources.length != this._sources.length) { + return false; + } + //sort both arrays and compare like a zipper + //XXX (sid): I think we can make this more efficient + var sortfn = function(a,b) { + return a.toString() > b.toString(); + }; + var a_sorted = this._sources.sort(sortfn); + var b_sorted = that._sources.sort(sortfn); + for(var i in a_sorted) { + if(!a_sorted[i].equals(b_sorted[i])) { + return false; + } + } + return true; + }, + + /** + * Generates string representation of the Source List. + * Should be fairly similar to the original. + */ + toString: + function() { + if(this.isNone()) { + return "'none'"; + } + if(this._permitAllSources) { + return "*"; + } + return this._sources.map(function(x) { return x.toString(); }).join(" "); + }, + + /** + * Returns whether or not this source list represents the "'none'" special + * case. + */ + isNone: + function() { + return (!this._permitAllSources) && (this._sources.length < 1); + }, + + /** + * Returns whether or not this source list permits all sources (*). + */ + isAll: + function() { + return this._permitAllSources; + }, + + + /** + * Makes a new instance that resembles this object. + * @returns + * a new CSPSourceList + */ + clone: + function() { + var aSL = new CSPSourceList(); + aSL._permitAllSources = this._permitAllSources; + for(var i in this._sources) { + aSL._sources[i] = this._sources[i].clone(); + } + return aSL; + }, + + + /** + * Determines if this directive accepts a URI. + * @param aURI + * the URI in question + * @returns + * true if the URI matches a source in this source list. + */ + permits: + function cspsd_permits(aURI) { + if(this.isNone()) return false; + if(this.isAll()) return true; + + for(var i in this._sources) { + if(this._sources[i].permits(aURI)) { + return true; + } + } + return false; + }, + + + /** + * Intersects with another CSPSourceList, deciding the subset directive + * that should be enforced, and returning a new instance. + * @param that + * the other CSPSourceList to intersect "this" with + * @returns + * a new instance of a CSPSourceList representing the intersection + */ + intersectWith: + function cspsd_intersectWith(that) { + + var newCSPSrcList = null; + + if(this.isNone() || that.isNone()) + newCSPSrcList = CSPSourceList.fromString("'none'"); + + if(this.isAll()) newCSPSrcList = that.clone(); + if(that.isAll()) newCSPSrcList = this.clone(); + + + if(!newCSPSrcList) { + // the shortcuts didn't apply, must do intersection the hard way. + // -- find only common sources + + //XXX (sid): figure out a good algorithm for this. + // This is horribly inefficient. O(n^2) + var isrcs = []; + for(var i in this._sources) { + for(var j in that._sources) { + var s = that._sources[j].intersectWith(this._sources[i]); + if(s) { + isrcs.push(s); + } + } + } + //Next, remove duplicates + dup:for(var i = 0; i < isrcs.length; i++) { + for(var j = 0; j < i; j++) { + if(isrcs[i].equals(isrcs[j])) { + isrcs.splice(i, 1); + i--; + continue dup; + } + } + } + newCSPSrcList = new CSPSourceList(); + newCSPSrcList._sources = isrcs; + } + + // if either was explicit, so is this. + newCSPSrcList._isImplicit = this._isImplicit && that._isImplicit; + + return newCSPSrcList; + } +} + + +////////////////////////////////////////////////////////////////////// +/** + * Class to model a source (scheme, host, port) + */ +function CSPSource() { + this._scheme = undefined; + this._port = undefined; + this._host = undefined; + + // when set to true, this source represents 'self' + this._isSelf = false; +} + +/** + * General factory method to create a new source from one of the following + * types: + * - nsURI + * - string + * - CSPSource (clone) + */ +CSPSource.create = function(aData, self, enforceSelfChecks) { + if(typeof aData === 'string') + return CSPSource.fromString(aData, self, enforceSelfChecks); + + if(aData instanceof Components.interfaces.nsIURI) + return CSPSource.fromURI(aData, self, enforceSelfChecks); + + if(aData instanceof CSPSource) { + var ns = aData.clone(); + ns._self = CSPSource.create(self); + } + + return null; +} + +/** + * Factory to create a new CSPSource, from a nsIURI. + * + * Don't use this if you want to wildcard ports! + * + * @param aURI + * nsIURI rep of a URI + * @param self (optional) + * string or CSPSource representing the "self" source + * @param enforceSelfChecks (optional) + * if present, and "true", will check to be sure "self" has the + * appropriate values to inherit when they are omitted from aURI. + * @returns + * an instance of CSPSource + */ +CSPSource.fromURI = function(aURI, self, enforceSelfChecks) { + if( !(aURI instanceof Components.interfaces.nsIURI) ){ + CSPError("Provided argument is not an nsIURI"); + return null; + } + + if(!self && enforceSelfChecks) { + CSPError("Can't use 'self' if self data is not provided"); + return null; + } + + if(self && !(self instanceof CSPSource)) { + self = CSPSource.create(self, undefined, false); + } + + var sObj = new CSPSource(); + sObj._self = self; + + //PARSE + // If 'self' is undefined, then use default port for scheme if there is one. + + //grab scheme (if there is one) + try { + sObj._scheme = aURI.scheme; + } catch(e) { + sObj._scheme = undefined; + CSPError("can't parse a URI without a scheme: " + aURI.asciiSpec); + return null; + } + + //grab host (if there is one) + try { + //if there's no host, an exception will get thrown + //(NS_ERROR_FAILURE) + sObj._host = CSPHost.fromString(aURI.host); + } catch(e) { + sObj._host = undefined; + } + + //grab port (if there is one) + // creating a source from an nsURI is limited in that one cannot specify "*" + // for port. In fact, there's no way to represent "*" differently than + // a blank port in an nsURI, since "*" turns into -1, and so does an + // absence of port declaration. + try { + //if there's no port, an exception will get thrown + //(NS_ERROR_FAILURE) + if(aURI.port > 0) + sObj._port = aURI.port; + else { + // Grab default port for scheme (if a scheme is available, + // which it better be!!) and 'self' is undefined and thus can't be used + // as a "default" port when one's not specified. + if(sObj._scheme && !self) { + sObj._port = gIoService.getProtocolHandler(sObj._scheme).defaultPort; + if(sObj._port < 1) + sObj._port = undefined; + } + } + } catch(e) { + sObj._port = undefined; + } + + return sObj; +}; + +/** + * Factory to create a new CSPSource, parsed from a string. + * + * @param aStr + * string rep of a CSP Source + * @param self (optional) + * string or CSPSource representing the "self" source + * @param enforceSelfChecks (optional) + * if present, and "true", will check to be sure "self" has the + * appropriate values to inherit when they are omitted from aURI. + * @returns + * an instance of CSPSource + */ +CSPSource.fromString = function(aStr, self, enforceSelfChecks) { + if(!aStr) return null; + + if(!(typeof aStr === 'string')) { + CSPError("Provided argument is not a string"); + return null; + } + + if(!self && enforceSelfChecks) { + CSPError("Can't use 'self' if self data is not provided"); + return null; + } + + if(self && !(self instanceof CSPSource)) { + self = CSPSource.create(self, undefined, false); + } + + var sObj = new CSPSource(); + sObj._self = self; + + + //take care of 'self' keyword + if(aStr == "'self'") { + if(!self) { + CSPError("self keyword used, but no self data specified"); + return null; + } + sObj._isSelf = true; + sObj._self = self.clone(); + return sObj; + } + + + // So we could just create a URI and then send this off to fromURI, but + // there's no way to leave out the scheme or wildcard the port in an nsURI. + // That has to be supported here. + + // split it up + var chunks = aStr.split(":"); + + // If there is only one chunk, it's gotta be a host. + if(chunks.length == 1) { + sObj._host = CSPHost.fromString(chunks[0]); + if(!sObj._host) { + CSPError("Couldn't parse invalid source " + aStr); + return null; + } + + //enforce 'self' inheritance + if(enforceSelfChecks) { + // note: the non _scheme accessor checks sObj._self + if(!sObj.scheme || !sObj.port) { + CSPError("Can't create host-only source " + aStr + " without 'self' data"); + return null; + } + } + return sObj; + } + + // If there are two chunks, it's either scheme://host or host:port + // ... but scheme://host can have an empty host. + // ... and host:port can have an empty host + if(chunks.length == 2) { + + //is the last bit a port? + if(chunks[1] === "*" || chunks[1].match(/^\d+$/)) { + sObj._port = chunks[1]; + //then the previous chunk *must* be a host or empty. + if(chunks[0] !== "") { + sObj._host = CSPHost.fromString(chunks[0]); + if(!sObj._host) { + CSPError("Couldn't parse invalid source " + aStr); + return null; + } + } + //enforce 'self' inheritance + //(scheme:host requires port, host:port does too. Wildcard support is + //only available if the scheme and host are wildcarded) + if(enforceSelfChecks) { + // note: the non _scheme accessor checks sObj._self + if(!sObj.scheme || !sObj.host || !sObj.port) { + CSPError("Can't create source " + aStr + " without 'self' data"); + return null; + } + } + } + //is the first bit a scheme? + else if(CSPSource.validSchemeName(chunks[0])) { + sObj._scheme = chunks[0]; + //then the second bit *must* be a host or empty + if(chunks[1] === "") { + // Allow scheme-only sources! These default to wildcard host/port, + // especially since host and port don't always matter. + // Example: "javascript:" and "data:" + if(!sObj._host) sObj._host = "*"; + if(!sObj._port) sObj._port = "*"; + } else { + // some host was defined. + // ... remove <= 3 leading slashes (from the scheme) and parse + var cleanHost = chunks[1].replace(/^\/{0,3}/,""); + // ... and parse + sObj._host = CSPHost.fromString(cleanHost); + if(!sObj._host) { + CSPError("Couldn't parse invalid host " + cleanHost); + return null; + } + } + + //enforce 'self' inheritance (scheme-only should be scheme:*:* now, and + // if there was a host provided it should be scheme:host:selfport + if(enforceSelfChecks) { + // note: the non _scheme accessor checks sObj._self + if(!sObj.scheme || !sObj.host || !sObj.port) { + CSPError("Can't create source " + aStr + " without 'self' data"); + return null; + } + } + } + else { + // AAAH! Don't know what to do! No valid scheme or port! + CSPError("Couldn't parse invalid source " + aStr); + return null; + } + + return sObj; + } + + //If there are three chunks, we got 'em all! + if(!CSPSource.validSchemeName(chunks[0])) { + CSPError("Couldn't parse scheme in " + aStr); + return null; + } + sObj._scheme = chunks[0]; + if(!(chunks[2] === "*" || chunks[2].match(/^\d+$/))) { + CSPError("Couldn't parse port in " + aStr); + return null; + } + + sObj._port = chunks[2]; + + // ... remove <= 3 leading slashes (from the scheme) and parse + var cleanHost = chunks[1].replace(/^\/{0,3}/,""); + sObj._host = CSPHost.fromString(cleanHost); + + return sObj._host ? sObj : null; +}; + + +CSPSource.validSchemeName = function(aStr) { + // ::= + // ::= + // | + // ::= | | "+" | "." | "-" + + return aStr.match(/^[a-zA-Z][a-zA-Z0-9+.-]*$/); +}; + +CSPSource.prototype = { + + + get scheme () { + if(!this._scheme && this._self) + return this._self.scheme; + return this._scheme; + }, + + get host () { + if(!this._host && this._self) + return this._self.host; + return this._host; + }, + + /** + * If 'self' has port hard-defined, and this doesn't have a port + * hard-defined, use the self's port. Otherwise, if both are implicit, + * resolve default port for this scheme. + */ + get port () { + if(this._port) return this._port; + //if no port, get the default port for the scheme. + if(this._scheme) { + try { + var port = gIoService.getProtocolHandler(this._scheme).defaultPort; + if(port > 0) return port; + } catch(e) { + //if any errors happen, fail gracefully. + } + } + //if there was no scheme (and thus no default scheme), return self.port + if(this._self && this._self.port) return this._self.port; + + return undefined; + }, + + /** + * Generates string representation of the Source. + * Should be fairly similar to the original. + */ + toString: + function() { + if(this._isSelf) + return this._self.toString(); + + var s = ""; + if(this._scheme) + s = s + this._scheme + "://"; + if(this._host) + s = s + this._host; + if(this._port) + s = s + ":" + this._port; + return s; + }, + + /** + * Makes a new instance that resembles this object. + * @returns + * a new CSPSource + */ + clone: + function() { + var aClone = new CSPSource(); + aClone._self = this._self ? this._self.clone() : undefined; + aClone._scheme = this._scheme; + aClone._port = this._port; + aClone._host = this._host ? this._host.clone() : undefined; + aClone._isSelf = this._isSelf; + return aClone; + }, + + /** + * Determines if this Source accepts a URI. + * @param aSource + * the URI, or CSPSource in question + * @returns + * true if the URI matches a source in this source list. + */ + permits: + function(aSource) { + if(!aSource) return false; + + if(!(aSource instanceof CSPSource)) + return this.permits(CSPSource.create(aSource)); + + //verify scheme + if(this.scheme != aSource.scheme) + return false; + + //port is defined in 'this' (undefined means it may not be relevant + // to the scheme) AND this port (implicit or explicit) matches + // aSource's port + if(this.port && this.port !== "*" && this.port != aSource.port) + return false; + + //host is defined in 'this' (undefined means it may not be relevant + // to the scheme) AND this host (implicit or explicit) permits + // aSource's host. + if(this.host && !this.host.permits(aSource.host)) + return false; + + //all scheme, host and port matched! + return true; + }, + + /** + * Determines the intersection of two sources. + * Returns a null object if intersection generates no + * hosts that satisfy it. + * @param that + * the other CSPSource to intersect "this" with + * @returns + * a new instance of a CSPSource representing the intersection + */ + intersectWith: + function(that) { + var newSource = new CSPSource(); + + // 'self' is not part of the intersection. Intersect the raw values from + // the source, self must be set by someone creating this source. + // When intersecting, we take the more specific of the two: if one scheme, + // host or port is undefined, the other is taken. (This is contrary to + // when "permits" is called -- there, the value of 'self' is looked at + // when a scheme, host or port is undefined.) + + //port + if(!this._port) + newSource._port = that._port; + else if (!that._port) + newSource._port = this._port; + else if(this._port == "*") + newSource._port = that._port; + else if(that._port == "*") + newSource._port = this._port; + else if(that._port == this._port) + newSource._port = this._port; + else { + CSPError("Could not intersect " + this + " with " + that + + " due to port problems."); + return null; + } + + //scheme + if(!this._scheme) + newSource._scheme = that._scheme; + else if(!that._scheme) + newSource._scheme = this._scheme; + if(this._scheme == "*") + newSource._scheme = that._scheme; + else if(that._scheme == "*") + newSource._scheme = this._scheme; + else if(that._scheme == this._scheme) + newSource._scheme = this._scheme; + else { + CSPError("Could not intersect " + this + " with " + that + + " due to scheme problems."); + return null; + } + + //host + if(!this._host) + newSource._host = that._host; + else if(!that._host) + newSource._host = this._host; + else //both this and that have hosts + newSource._host = this._host.intersectWith(that._host); + + return newSource; + }, + + /** + * Compares one CSPSource to another. + * + * @param that + * another CSPSource + * @param resolveSelf (optional) + * if present, and 'true', implied values are obtained from 'self' + * instead of assumed to be "anything" + * @returns + * true if they have the same data + */ + equals: + function(that, resolveSelf) { + // 1. schemes match + // 2. ports match + // 3. either both hosts are undefined, or one equals the other. + if(resolveSelf) + return this.scheme == that.scheme + && this.port == that.port + && (!(this.host || that.host) || + (this.host && this.host.equals(that.host))); + + //otherwise, compare raw (non-self-resolved values) + return this._scheme == that._scheme + && this._port == that._port + && (!(this._host || that._host) || + (this._host && this._host.equals(that._host))); + }, + +}; + + +////////////////////////////////////////////////////////////////////// +/** + * Class to model a host *.x.y. + */ +function CSPHost() { + this._segments = []; +} + +/** + * Factory to create a new CSPHost, parsed from a string. + * + * @param aStr + * string rep of a CSP Host + * @returns + * an instance of CSPHost + */ +CSPHost.fromString = function(aStr) { + if(!aStr) return null; + + // host string must be LDH with dots and stars. + var invalidChar = aStr.match(/[^a-zA-Z0-9\-\.\*]/); + if(invalidChar) { + CSPdebug("Invalid character '" + invalidChar + "' in host " + aStr); + return null; + } + + var hObj = new CSPHost(); + hObj._segments = aStr.split(/\./); + if(hObj._segments.length < 1 || + hObj._segments.length == 1 && hObj._segments[0] != "*" + && hObj._segments[0] != "localhost") { + //only short hosts allowed are "*" and "localhost" + return null; + } + + // validate data in segments + for(var i in hObj._segments) { + var seg = hObj._segments[i]; + if(seg === "*") { + if(i > 0) { + // Wildcard must be FIRST + CSPdebug("Wildcard char located at invalid position in '" + aStr + "'"); + return null; + } + } + else if(seg.match(/[^a-zA-Z0-9\-]/)) { + // Non-wildcard segment must be LDH string + CSPdebug("Invalid segment '" + seg + "' in host value"); + return null; + } + } + return hObj; +}; + +CSPHost.prototype = { + /** + * Generates string representation of the Source. + * Should be fairly similar to the original. + */ + toString: + function() { + return this._segments.join("."); + }, + + /** + * Makes a new instance that resembles this object. + * @returns + * a new CSPHost + */ + clone: + function() { + var aHost = new CSPHost(); + for(var i in this._segments) { + aHost._segments[i] = this._segments[i]; + } + return aHost; + }, + + /** + * Returns true if this host accepts the provided host (or the other way + * around). + * @param aHost + * the FQDN in question (CSPHost or String) + * @returns + */ + permits: + function(aHost) { + if(!aHost) return false; + + if(!(aHost instanceof CSPHost)) { + // -- compare CSPHost to String + return this.permits(CSPHost.fromString(aHost)); + } + var thislen = this._segments.length; + var thatlen = aHost._segments.length; + + // don't accept a less specific host: + // \--> *.b.a doesn't accept b.a. + if(thatlen < thislen) { return false; } + + // check for more specific host (and wildcard): + // \--> *.b.a accepts d.c.b.a. + // \--> c.b.a doesn't accept d.c.b.a. + if((thatlen > thislen) && this._segments[0] != "*") { + return false; + } + + // Given the wildcard condition (from above), + // only necessary to compare elements that are present + // in this host. Extra tokens in aHost are ok. + // * Compare from right to left. + for(var i=1; i <= thislen; i++) { + if( this._segments[thislen-i] != "*" && + (this._segments[thislen-i] != aHost._segments[thatlen-i]) ) { + return false; + } + } + + //at this point, all conditions are met, so the host is allowed + return true; + }, + + /** + * Determines the intersection of two Hosts. + * Basically, they must be the same, or one must have a wildcard. + * @param that + * the other CSPHost to intersect "this" with + * @returns + * a new instance of a CSPHost representing the intersection + * (or null, if they can't be intersected) + */ + intersectWith: + function(that) { + if(!(this.permits(that) || that.permits(this))) { + // host definitions cannot co-exist without a more general host + // ... one must be a subset of the other, or intersection makes no sense. + return null; + } + + // pick the more specific one, if both are same length. + if(this._segments.length == that._segments.length) { + // *.a vs b.a : b.a + return (this._segments[0] == "*") ? that.clone() : this.clone(); + } + + //different lengths... + // *.b.a vs *.a : *.b.a + // *.b.a vs d.c.b.a : d.c.b.a + return (this._segments.length > that._segments.length) + ? this.clone() : that.clone(); + }, + + /** + * Compares one CSPHost to another. + * + * @param that + * another CSPHost + * @returns + * true if they have the same data + */ + equals: + function(that) { + if(this._segments.length != that._segments.length) + return false; + + for(var i=0; i + * Brandon Sterne + * + * 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 ***** */ + + +/** + * Content Security Policy + * + * Overview + * This is a stub component that will be fleshed out to do all the fancy stuff + * that ContentSecurityPolicy has to do. + */ + +/* :::::::: Constants and Helpers ::::::::::::::: */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +const CSP_VIOLATION_TOPIC = "csp-on-violate-policy"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/CSPUtils.jsm"); + +/* ::::: Policy Parsing & Data structures :::::: */ + +function ContentSecurityPolicy() { + CSPdebug("CSP CREATED"); + this._isInitialized = false; + this._reportOnlyMode = false; + this._policy = CSPRep.fromString("allow *"); + + //default options "wide open" since this policy will be intersected soon + this._policy._allowInlineScripts = true; + this._policy._allowEval = true; + + this._requestHeaders = []; + this._request = ""; + CSPdebug("CSP POLICY INITED TO 'allow *'"); + + this._observerService = Cc['@mozilla.org/observer-service;1'] + .getService(Ci.nsIObserverService); +} + +/* + * Set up mappings from nsIContentPolicy content types to CSP directives. + */ +with(Ci.nsIContentPolicy) { + let csp = ContentSecurityPolicy; + let cspr_sd = CSPRep.SRC_DIRECTIVES; + + csp._MAPPINGS=[]; + + /* default, catch-all case */ + csp._MAPPINGS[TYPE_OTHER] = cspr_sd.ALLOW; + + /* self */ + csp._MAPPINGS[TYPE_DOCUMENT] = null; + + /* shouldn't see this one */ + csp._MAPPINGS[TYPE_REFRESH] = null; + + /* categorized content types */ + csp._MAPPINGS[TYPE_SCRIPT] = cspr_sd.SCRIPT_SRC; + csp._MAPPINGS[TYPE_IMAGE] = cspr_sd.IMG_SRC; + csp._MAPPINGS[TYPE_STYLESHEET] = cspr_sd.STYLE_SRC; + csp._MAPPINGS[TYPE_OBJECT] = cspr_sd.OBJECT_SRC; + csp._MAPPINGS[TYPE_SUBDOCUMENT] = cspr_sd.FRAME_SRC; + csp._MAPPINGS[TYPE_MEDIA] = cspr_sd.MEDIA_SRC; + csp._MAPPINGS[TYPE_FONT] = cspr_sd.FONT_SRC; + csp._MAPPINGS[TYPE_XMLHTTPREQUEST] = cspr_sd.XHR_SRC; + + + /* These must go through the catch-all */ + csp._MAPPINGS[TYPE_XBL] = cspr_sd.ALLOW; + csp._MAPPINGS[TYPE_PING] = cspr_sd.ALLOW; + csp._MAPPINGS[TYPE_OBJECT_SUBREQUEST] = cspr_sd.ALLOW; + csp._MAPPINGS[TYPE_DTD] = cspr_sd.ALLOW; +} + +ContentSecurityPolicy.prototype = { + classDescription: "Content Security Policy Component", + contractID: "@mozilla.org/contentsecuritypolicy;1", + classID: Components.ID("{AB36A2BF-CB32-4AA6-AB41-6B4E4444A221}"), + QueryInterface: XPCOMUtils.generateQI([Ci.IContentSecurityPolicy]), + + // get this contractID registered for certain categories via XPCOMUtils + _xpcom_categories: [ ], + + get isInitialized() { + return this._isInitialized; + }, + + set isInitialized (foo) { + this._isInitialized = foo; + }, + + get policy () { + return this._policy.toString(); + }, + + get allowsInlineScript() { + //trigger automatic report to go out when inline scripts are disabled. + if (!this._policy.allowsInlineScripts) { + var violation = 'violated base restriction: Inline Scripts will not execute'; + // 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 = violation; + this._observerService.notifyObservers( + wrapper, + CSP_VIOLATION_TOPIC, + 'inline script base restriction'); + this.sendReports('self', violation); + } + return this._reportOnlyMode || this._policy.allowsInlineScripts; + }, + + get allowsEval() { + //trigger automatic report to go out when eval and friends are disabled. + if (!this._policy.allowsEvalInScripts) { + var violation = 'violated base restriction: Code will not be created from strings'; + // 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 = violation; + this._observerService.notifyObservers( + wrapper, + CSP_VIOLATION_TOPIC, + 'eval script base restriction'); + this.sendReports('self', violation); + } + return this._reportOnlyMode || this._policy.allowsEvalInScripts; + }, + + set reportOnlyMode(val) { + this._reportOnlyMode = val; + }, + + get reportOnlyMode () { + return this._reportOnlyMode; + }, + + /* + // Having a setter is a bad idea... opens up the policy to "loosening" + // Instead, use "refinePolicy." + set policy (aStr) { + this._policy = CSPRep.fromString(aStr); + }, + */ + + /** + * Given an nsIHttpChannel, fill out the appropriate data. + */ + scanRequestData: + function(aChannel) { + // grab the request line + var internalChannel = aChannel.QueryInterface(Ci.nsIHttpChannelInternal); + var reqMaj = {}; + var reqMin = {}; + var reqVersion = internalChannel.getRequestVersion(reqMaj, reqMin); + this._request = aChannel.requestMethod + " " + + aChannel.URI + + " HTTP/" + reqMaj.value + "." + reqMin.value; + + // grab the request headers + var self = this; + aChannel.visitRequestHeaders({ + visitHeader: function(aHeader, aValue) { + self._requestHeaders.push(aHeader + ": " + aValue); + }}); + }, + +/* ........ Methods .............. */ + + /** + * Given a new policy, intersects the currently enforced policy with the new + * one and stores the result. The effect is a "tightening" or refinement of + * an old policy. This is called any time a new policy is encountered and + * the effective policy has to be refined. + */ + refinePolicy: + function csp_refinePolicy(aPolicy, selfURI) { + CSPdebug("REFINE POLICY: " + aPolicy); + CSPdebug(" SELF: " + selfURI.asciiSpec); + + // stay uninitialized until policy merging is done + this._isInitialized = false; + + // If there is a policy-uri, fetch the policy, then re-call this function. + // (1) parse and create a CSPRep object + var newpolicy = CSPRep.fromString(aPolicy, + selfURI.scheme + "://" + selfURI.hostPort); + + // (2) Intersect the currently installed CSPRep object with the new one + var intersect = this._policy.intersectWith(newpolicy); + //TODO: make sure intersect worked + + // (3) Save the result + this._policy = intersect; + this._isInitialized = true; + }, + + /** + * Generates and sends a violation report to the specified report URIs. + */ + sendReports: + function(blockedUri, violatedDirective) { + var uris = this._policy.getReportURIs(); + if(uris.length > 0) { + // Generate report to send composed of: + // + // GET /index.html HTTP/1.1 + // Host: example.com + // User-Agent: ... + // ... + // + // ... + // ... + // + // + var strHeaders = ""; + for(let i in this._requestHeaders) { + strHeaders += this._requestHeaders[i] + "\n"; + } + + var report = "\n" + + " " + this._request + "\n" + + " \n" + + " " + blockedUri + "\n" + + " " + violatedDirective + "\n" + + "\n"; + + CSPdebug("Constructed violation report:\n" + report); + + // For each URI in the report list, send out a report. + for(let i in uris) { + var failure = function(aEvt) { + if(req.readyState == 4 && req.status != 200) { + CSPError("Failed to send report to " + reportURI); + } + }; + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + + req.open("POST", uris[i], true); + req.setRequestHeader('Content-Type', 'application/xml'); + req.upload.addEventListener("error", failure, false); + req.upload.addEventListener("abort", failure, false); + //req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + + // make request anonymous + // This prevents sending cookies with the request, + // in case the policy URI is injected, it can't be + // abused for CSRF. + req.channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS; + + req.send(report); + CSPdebug("Sent violation report to " + uris[i]); + } + } + }, + + /** + * Exposed Method to analyze docShell for approved frame ancestry. + * Also sends violation reports if necessary. + * @param docShell + * the docShell for this policy's resource. + * @return + * true if the frame ancestry is allowed by this policy. + */ + permitsAncestry: + function(docShell) { + if(!docShell) { return false; } + CSPdebug(" in permitsAncestry(), docShell = " + docShell); + + // walk up this docShell tree until we hit chrome + var dst = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShellTreeItem); + + // collect ancestors and make sure they're allowed. + var ancestors = []; + while(dst.parent) { + dst = dst.parent; + let it = dst.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + if(it.currentURI) { + if(it.currentURI.scheme == "chrome") { + break; + } + let ancestor = it.currentURI; + CSPdebug(" found frame ancestor " + ancestor.asciiSpec); + ancestors.push(ancestor); + } + } + + // scan the discovered ancestors + let cspContext = CSPRep.SRC_DIRECTIVES.FRAME_ANCESTORS; + for(let i in ancestors) { + let ancestor = ancestors[i].prePath; + if(!this._policy.permits(ancestor, cspContext)) { + // report the frame-ancestor violation + let directive = this._policy._directives[cspContext]; + let violatedPolicy = (directive._isImplicit + ? 'allow' : 'frame-ancestors ') + + directive.toString(); + // send an nsIURI object to the observers (more interesting than a string) + this._observerService.notifyObservers( + ancestors[i], + CSP_VIOLATION_TOPIC, + violatedPolicy); + this.sendReports(ancestors[i].asciiSpec, violatedPolicy); + // need to lie if we are testing in report-only mode + return this._reportOnlyMode; + } + } + return true; + }, + + /** + * 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. + */ + shouldLoad: + function csp_shouldLoad(aContentType, + aContentLocation, + aRequestOrigin, + aContext, + aMimeTypeGuess, + aExtra) { + + //don't filter chrome stuff + if(aContentLocation.scheme == 'chrome') { + return Ci.nsIContentPolicy.ACCEPT; + } + + //interpret the context, and then pass off to the decision structure + CSPdebug("shouldLoad location = " + aContentLocation.asciiSpec); + CSPdebug("shouldLoad content type = " + aContentType); + var cspContext = ContentSecurityPolicy._MAPPINGS[aContentType]; + //CSPdebug("shouldLoad CSP directive =" + cspContext); + + // if the mapping is null, there's no policy, let it through. + if(!cspContext) { + return Ci.nsIContentPolicy.ACCEPT; + } + + // otherwise, honor the translation + //var source = aContentLocation.scheme + "://" + aContentLocation.hostPort; + var res = this._policy.permits(aContentLocation, cspContext) + ? Ci.nsIContentPolicy.ACCEPT + : 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 + if(res != Ci.nsIContentPolicy.ACCEPT) { + CSPdebug("blocking request for " + aContentLocation.asciiSpec); + try { + let directive = this._policy._directives[cspContext]; + let violatedPolicy = (directive._isImplicit + ? 'allow' : cspContext) + + ' ' + directive.toString(); + this._observerService.notifyObservers( + aContentLocation, + CSP_VIOLATION_TOPIC, + violatedPolicy); + this.sendReports(aContentLocation, violatedPolicy); + } catch(e) { + CSPdebug('---------------- ERROR: ' + e); + } + } + + return (this._reportOnlyMode ? Ci.nsIContentPolicy.ACCEPT : res); + }, + + shouldProcess: + function csp_shouldProcess(aContentType, + aContentLocation, + aRequestOrigin, + aContext, + aMimeType, + aExtra) { + // frame-ancestors check is done outside the ContentPolicy + var res = Ci.nsIContentPolicy.ACCEPT; + CSPdebug("shouldProcess aContext=" + aContext); + return res; + }, + +}; + + + +function NSGetModule(aComMgr, aFileSpec) + XPCOMUtils.generateModule([ContentSecurityPolicy]); diff --git a/content/base/src/nsCSPService.cpp b/content/base/src/nsCSPService.cpp new file mode 100644 --- /dev/null +++ b/content/base/src/nsCSPService.cpp @@ -0,0 +1,311 @@ +/* -*- 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 + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Brandon Sterne + * + * 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 "prlog.h" +#include "nsString.h" +#include "nsCOMPtr.h" +//#include "nsIServiceManager.h" +//#include "nsMemory.h" +//#include "nsICategoryManager.h" +#include "nsIURI.h" +#include "nsIPrincipal.h" +#include "nsIObserver.h" +#include "nsIDocument.h" +#include "nsIContent.h" +#include "nsContentUtils.h" +#include "nsCSPService.h" +#include "nsIChannelPolicy.h" +#include "IContentSecurityPolicy.h" +#include "nsIChannelEventSink.h" +#include "nsIPropertyBag2.h" +#include "nsIWritablePropertyBag2.h" +#include "nsNetError.h" + +/* Keeps track of whether or not CSP is enabled */ +static PRBool gCSPEnabled = PR_FALSE; + +#ifdef PR_LOGGING +static PRLogModuleInfo* gCspPRLog; +#endif + +CSPService::CSPService() +{ + gCSPEnabled = nsContentUtils::GetBoolPref("security.csp.enable", PR_TRUE); + nsContentUtils::AddBoolPrefVarCache("security.csp.enable", &gCSPEnabled); + +#ifdef PR_LOGGING + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); +#endif +} + +CSPService::~CSPService() +{ +} + +NS_IMPL_ISUPPORTS2(CSPService, nsIContentPolicy, nsIChannelEventSink) + +/* nsIContentPolicy implementation */ +NS_IMETHODIMP +CSPService::ShouldLoad(PRUint32 aContentType, + nsIURI *aContentLocation, + nsIURI *aRequestOrigin, + nsISupports *aRequestContext, + const nsACString &aMimeTypeGuess, + nsISupports *aExtra, + PRInt16 *aDecision) +{ + if (!aContentLocation) + return NS_ERROR_FAILURE; + +#ifdef PR_LOGGING + { + nsCAutoString location("None"); + aContentLocation->GetSpec(location); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSPService::ShouldLoad called for %s", location.get())); + } +#endif + // default decision, CSP can revise it if there's a policy to enforce + *aDecision = nsIContentPolicy::ACCEPT; + + // No need to continue processing if CSP is disabled + if (!gCSPEnabled) + return NS_OK; + + // find the nsDocument that initiated this request and see if it has a + // CSP policy object + nsresult rv; + nsCOMPtr doc; + nsCOMPtr principal; + nsCOMPtr csp; + nsCOMPtr node(do_QueryInterface(aRequestContext)); + if (node) { + doc = node->GetOwnerDoc(); + } + if (!doc) { + doc = do_QueryInterface(aRequestContext); + } + + if (doc) { + principal = doc->NodePrincipal(); + NS_ASSERTION(principal, "Document didn't have a principal."); + + rv = principal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + + if (csp) { +#ifdef PR_LOGGING + nsAutoString policy; + csp->GetPolicy(policy); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("Document has CSP: %s", + NS_ConvertUTF16toUTF8(policy).get())); +#endif + // obtain the enforcement decision + csp->ShouldLoad(aContentType, + aContentLocation, + aRequestOrigin, + aRequestContext, + aMimeTypeGuess, + aExtra, + aDecision); + } + } +#ifdef PR_LOGGING + else { + nsCAutoString uriSpec("None"); + aContentLocation->GetSpec(uriSpec); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("COULD NOT get nsIDocument for location: %s", uriSpec.get())); + } +#endif + + return NS_OK; +} + +NS_IMETHODIMP +CSPService::ShouldProcess(PRUint32 aContentType, + nsIURI *aContentLocation, + nsIURI *aRequestOrigin, + nsISupports *aRequestContext, + const nsACString &aMimeTypeGuess, + nsISupports *aExtra, + PRInt16 *aDecision) +{ + if (!aContentLocation) + return NS_ERROR_FAILURE; + + // default decision is to accept the item + *aDecision = nsIContentPolicy::ACCEPT; + + // No need to continue processing if CSP is disabled + if (!gCSPEnabled) + return NS_OK; + + // find the nsDocument that initiated this request and see if it has a + // CSP policy object + nsresult rv; + nsCOMPtr doc; + nsCOMPtr principal; + nsCOMPtr csp; + nsCOMPtr node(do_QueryInterface(aRequestContext)); + if (node) { + doc = node->GetOwnerDoc(); + } + if (!doc) { + doc = do_QueryInterface(aRequestContext); + } + + if (doc) { + principal = doc->NodePrincipal(); + rv = principal->GetCsp(getter_AddRefs(csp)); + //TODO: check principal and rv + //IContentSecurityPolicy* csp = doc->GetContentSecurityPolicy(); + if (csp) { +#ifdef PR_LOGGING + nsAutoString policy; + csp->GetPolicy(policy); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("shouldProcess - document has policy: %s", + NS_ConvertUTF16toUTF8(policy).get())); +#endif + // obtain the enforcement decision + csp->ShouldProcess(aContentType, + aContentLocation, + aRequestOrigin, + aRequestContext, + aMimeTypeGuess, + aExtra, + aDecision); + } + } +#ifdef PR_LOGGING + else { + nsCAutoString uriSpec("None"); + aContentLocation->GetSpec(uriSpec); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("COULD NOT get nsIDocument for location: %s", uriSpec.get())); + } +#endif + return NS_OK; +} + +/* nsIChannelEventSink implementation */ +NS_IMETHODIMP +CSPService::OnChannelRedirect(nsIChannel *oldChannel, + nsIChannel *newChannel, + PRUint32 flags) +{ + // get the Content Security Policy and load type from the property bag + nsCOMPtr policyContainer; + nsCOMPtr props(do_QueryInterface(oldChannel)); + if (!props) + return NS_OK; + + props->GetPropertyAsInterface(NS_LITERAL_STRING("csp.channelPolicy"), + NS_GET_IID(nsISupports), + getter_AddRefs(policyContainer)); + if (!policyContainer) + return NS_OK; + + // see if we have a valid nsIChannelPolicy containing CSP and load type + nsCOMPtr channelPolicy(do_QueryInterface(policyContainer)); + if (!channelPolicy) + return NS_OK; + + nsCOMPtr csp; + channelPolicy->GetContentSecurityPolicy(getter_AddRefs(csp)); + PRUint32 loadType; + channelPolicy->GetLoadType(&loadType); + + // if no CSP in the channelPolicy, nothing for us to add to the channel + if (!csp) + return NS_OK; + + /* Since redirecting channels don't call into nsIContentPolicy, we call our + * Content Policy implementation directly when redirects occur. When channels + * are created using NewChannelIfPolicyOK() the CSP object and load type, + * which are needed by the Content Policy, are placed in the new channel's + * property bag. These items are propagated forward when channels redirect.*/ + + // Does the CSP permit this host for this type of load? + // If not, cancel the load now. + nsCOMPtr newUri; + newChannel->GetURI(getter_AddRefs(newUri)); + PRInt16 aDecision = nsIContentPolicy::ACCEPT; + csp->ShouldLoad(loadType, // load type per nsIContentPolicy (PRUint32) + newUri, // nsIURI + nsnull, // nsIURI + nsnull, // nsISupports + EmptyCString(), // ACString - MIME guess + nsnull, // nsISupports - extra + &aDecision); + +#ifdef PR_LOGGING + if (newUri) { + nsCAutoString newUriSpec("None"); + newUri->GetSpec(newUriSpec); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSPService::OnChannelRedirect called for %s", newUriSpec.get())); + } + if (aDecision == 1) + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSPService::OnChannelRedirect ALLOWING request.")); + else + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSPService::OnChannelRedirect CANCELLING request.")); +#endif + + // if ShouldLoad doesn't accept the load, cancel the request + if (aDecision != 1) { + newChannel->Cancel(NS_BINDING_FAILED); + } + + // if the redirect is permitted, propagate the Content Security Policy + // and load type to the redirecting channel + else { + nsresult rv; + nsCOMPtr props = do_QueryInterface(newChannel, &rv); + if (props) + props->SetPropertyAsInterface(NS_LITERAL_STRING("csp.channelPolicy"), + channelPolicy); + } + + return NS_OK; +} diff --git a/content/base/src/nsCSPService.h b/content/base/src/nsCSPService.h new file mode 100644 --- /dev/null +++ b/content/base/src/nsCSPService.h @@ -0,0 +1,62 @@ +/* -*- 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 + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Brandon Sterne + * + * 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 "nsIGenericFactory.h" +#include "nsXPCOM.h" +#include "nsIContentPolicy.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" + +#define CSPSERVICE_CONTRACTID "@mozilla.org/cspservice;1" +#define CSPSERVICE_CID \ + { 0x8d2f40b2, 0x4875, 0x4c95, \ + { 0x97, 0xd9, 0x3f, 0x7d, 0xca, 0x2c, 0xb4, 0x60 } } +class CSPService : public nsIContentPolicy, + public nsIChannelEventSink +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPOLICY + NS_DECL_NSICHANNELEVENTSINK + + CSPService(); + virtual ~CSPService(); + +private: + PRBool mEnabled; +}; diff --git a/content/base/src/nsChannelPolicy.cpp b/content/base/src/nsChannelPolicy.cpp new file mode 100644 --- /dev/null +++ b/content/base/src/nsChannelPolicy.cpp @@ -0,0 +1,76 @@ +/* -*- 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 channel policy container code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * + * Contributor(s): + * Brandon Sterne + * + * 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 ***** */ + +#include "nsChannelPolicy.h" + +nsChannelPolicy::nsChannelPolicy() +{ +} + +nsChannelPolicy::~nsChannelPolicy() +{ +} + +NS_IMPL_ISUPPORTS1(nsChannelPolicy, nsIChannelPolicy) + +NS_IMETHODIMP +nsChannelPolicy::GetLoadType(PRUint32 *aLoadType) +{ + *aLoadType = mLoadType; + return NS_OK; +} + +NS_IMETHODIMP +nsChannelPolicy::SetLoadType(PRUint32 aLoadType) +{ + mLoadType = aLoadType; + return NS_OK; +} + +NS_IMETHODIMP +nsChannelPolicy::GetContentSecurityPolicy(IContentSecurityPolicy **aCSP) +{ + *aCSP = mCSP; + NS_IF_ADDREF(*aCSP); + return NS_OK; +} + +NS_IMETHODIMP +nsChannelPolicy::SetContentSecurityPolicy(IContentSecurityPolicy *aCSP) +{ + mCSP = aCSP; + return NS_OK; +} diff --git a/content/base/src/nsChannelPolicy.h b/content/base/src/nsChannelPolicy.h new file mode 100644 --- /dev/null +++ b/content/base/src/nsChannelPolicy.h @@ -0,0 +1,67 @@ +/* -*- 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 channel policy container code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * + * Contributor(s): + * Brandon Sterne + * + * 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 ***** */ + +#ifndef nsChannelPolicy_h___ +#define nsChannelPolicy_h___ + +#include "nsCOMPtr.h" +#include "nsIChannelPolicy.h" +#include "IContentSecurityPolicy.h" + +#define NSCHANNELPOLICY_CONTRACTID "@mozilla.org/nschannelpolicy;1" +#define NSCHANNELPOLICY_CID \ +{ 0xd396b3cd, 0xf164, 0x4ce8, \ + { 0x93, 0xa7, 0xe3, 0x85, 0xe1, 0x46, 0x56, 0x3c } } + +class NS_COM nsChannelPolicy : public nsIChannelPolicy +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICHANNELPOLICY + + nsChannelPolicy(); + virtual ~nsChannelPolicy(); + +protected: + /* represents the type of content being loaded in the channel per + * nsIContentPolicy, e.g. TYPE_IMAGE, TYPE_SCRIPT + */ + unsigned long mLoadType; + /* pointer to a Content Security Policy object if available */ + nsCOMPtr mCSP; +}; + +#endif /* nsChannelPolicy_h___ */ diff --git a/content/base/src/nsContentUtils.cpp b/content/base/src/nsContentUtils.cpp --- a/content/base/src/nsContentUtils.cpp +++ b/content/base/src/nsContentUtils.cpp @@ -181,6 +181,9 @@ static NS_DEFINE_CID(kXTFServiceCID, NS_ #include "mozAutoDocUpdate.h" #include "imgICache.h" #include "jsinterp.h" +#include "nsIChannelPolicy.h" +#include "nsChannelPolicy.h" +#include "IContentSecurityPolicy.h" const char kLoadAsData[] = "loadAsData"; @@ -2452,6 +2455,20 @@ nsContentUtils::LoadImage(nsIURI* aURI, nsIURI *documentURI = aLoadingDocument->GetDocumentURI(); + // check for a Content Security Policy to pass down to the channel that + // will get created to load the image + nsCOMPtr channelPolicy; + nsCOMPtr csp; + if (aLoadingPrincipal) { + nsresult rv = aLoadingPrincipal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if (csp) { + channelPolicy = new nsChannelPolicy(); + channelPolicy->SetContentSecurityPolicy(csp); + channelPolicy->SetLoadType(3); // TYPE_IMAGE + } + } + // Make the URI immutable so people won't change it under us NS_TryToSetImmutable(aURI); @@ -2469,6 +2486,7 @@ nsContentUtils::LoadImage(nsIURI* aURI, aLoadFlags, /* load flags */ nsnull, /* cache key */ nsnull, /* existing request*/ + channelPolicy, /* CSP info */ aRequest); } 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 @@ -178,6 +178,9 @@ static NS_DEFINE_CID(kDOMEventGroupCID, #include "nsSVGUtils.h" #endif // MOZ_SMIL +// FOR CSP (autogenerated by xpidl) +#include "IContentSecurityPolicy.h" + #ifdef MOZ_LOGGING // so we can get logging even in release builds @@ -187,6 +190,7 @@ static NS_DEFINE_CID(kDOMEventGroupCID, #ifdef PR_LOGGING static PRLogModuleInfo* gDocumentLeakPRLog; +static PRLogModuleInfo* gCspPRLog; #endif void @@ -1493,6 +1497,10 @@ nsDocument::nsDocument(const char* aCont if (gDocumentLeakPRLog) PR_LOG(gDocumentLeakPRLog, PR_LOG_DEBUG, ("DOCUMENT %p created", this)); + + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); + #endif // Start out mLastStyleSheetSet as null, per spec @@ -2252,6 +2260,129 @@ nsDocument::StartDocumentLoad(const char RetrieveRelevantHeaders(aChannel); mChannel = aChannel; + +// -------------- Initialize CSP + + PRBool cspEnabled; + cspEnabled = nsContentUtils::GetBoolPref("security.csp.enable", PR_FALSE); + + if (cspEnabled) + { + nsresult rv; + PRBool system; + nsIScriptSecurityManager *ssm = nsContentUtils::GetSecurityManager(); + nsAutoString cspHeaderValue; + nsAutoString cspROHeaderValue; + nsIAtom* cspHeaderAtom = NS_NewAtom("x-content-security-policy"); + nsIAtom* cspROHeaderAtom = NS_NewAtom("x-content-security-policy-report-only"); + + this->GetHeaderData(cspHeaderAtom, cspHeaderValue); + this->GetHeaderData(cspROHeaderAtom, cspROHeaderValue); + + if(NS_SUCCEEDED(ssm->IsSystemPrincipal(NodePrincipal(), &system)) && system) { + //only makes sense to register new CSP if this document is not priviliged + return NS_OK; + } + + if(cspHeaderValue.IsEmpty() && cspROHeaderValue.IsEmpty()) { + // no CSP header present + return NS_OK; + } + +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP header specified for document %p", this)); +#endif + + nsCOMPtr mCSP; + mCSP = do_CreateInstance("@mozilla.org/contentsecuritypolicy;1", &rv); + + if(NS_FAILED(rv)) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("Failed to create CSP object: %6x", rv)); +#endif + return rv; + } + + // Store the request context for violation reports + nsCOMPtr httpChannel = do_QueryInterface(aChannel); + mCSP->ScanRequestData(httpChannel); + + // Start parsing the policy + nsCOMPtr chanURI; + aChannel->GetURI(getter_AddRefs(chanURI)); + +#ifdef PR_LOGGING + 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); +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP (report only) refined, policy: \"%s\"", + ToNewCString(cspROHeaderValue))); +#endif + + } else { + //if(!cspROHeaderValue.IsEmpty()) { + //TODO: post warning that 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\"", ToNewCString(cspHeaderValue))); +#endif + } + + // Check for frame-ancestor violation + nsCOMPtr docShell = do_QueryReferent(mDocumentContainer); + if(docShell) { + PRBool safeAncestry = false; + + // PermitsAncestry sends violation reports when necessary + rv = mCSP->PermitsAncestry(docShell, &safeAncestry); + NS_ENSURE_SUCCESS(rv, rv); + + if(!safeAncestry) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP doesn't like frame's ancestry, not loading.")); +#endif + // stop! ERROR page! + aChannel->Cancel( NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_SECURITY, 99) ); + } + } + + + //Copy into principal + nsIPrincipal* principal = GetPrincipal(); + + if (principal) { + principal->SetCsp(mCSP); +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("Inserted CSP into principal %p", principal)); + } + else { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("Couldn't copy CSP into absent principal %p", principal)); +#endif + } + } +#ifdef PR_LOGGING + else //CSP was not enabled! + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP is disabled, skipping CSP init for document %p", this)); +#endif + + // -------------- CSP initialized return NS_OK; } @@ -6635,6 +6766,8 @@ nsDocument::RetrieveRelevantHeaders(nsIC "content-disposition", "refresh", "x-dns-prefetch-control", + "x-content-security-policy", + "x-content-security-policy-read-only", // add more http headers if you need // XXXbz don't add content-location support without reading bug // 238654 and its dependencies/dups first. @@ -6938,6 +7071,8 @@ nsDocument::Destroy() // to drop any references to the document so that it can be destroyed. if (mIsGoingAway) return; + + //XXX: if there's any cleanup for CSP, do it here. mIsGoingAway = PR_TRUE; diff --git a/content/base/src/nsObjectLoadingContent.cpp b/content/base/src/nsObjectLoadingContent.cpp --- a/content/base/src/nsObjectLoadingContent.cpp +++ b/content/base/src/nsObjectLoadingContent.cpp @@ -86,6 +86,9 @@ #include "nsObjectLoadingContent.h" #include "mozAutoDocUpdate.h" +#include "IContentSecurityPolicy.h" +#include "nsIChannelPolicy.h" +#include "nsChannelPolicy.h" #ifdef PR_LOGGING static PRLogModuleInfo* gObjectLog = PR_NewLogModule("objlc"); @@ -1285,8 +1288,18 @@ nsObjectLoadingContent::LoadObject(nsIUR nsCOMPtr group = doc->GetDocumentLoadGroup(); nsCOMPtr chan; - rv = NS_NewChannel(getter_AddRefs(chan), aURI, nsnull, group, this, - nsIChannel::LOAD_CALL_CONTENT_SNIFFERS); + nsCOMPtr channelPolicy; + nsCOMPtr csp; + rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if (csp) { + channelPolicy = new nsChannelPolicy(); + channelPolicy->SetContentSecurityPolicy(csp); + channelPolicy->SetLoadType(5); // TYPE_OBJECT + } + rv = NS_NewChannelWithPolicy(getter_AddRefs(chan), aURI, nsnull, group, this, + nsIChannel::LOAD_CALL_CONTENT_SNIFFERS, + channelPolicy); NS_ENSURE_SUCCESS(rv, rv); // Referrer 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 @@ -70,6 +70,15 @@ #include "nsThreadUtils.h" #include "nsIChannelClassifier.h" #include "nsDocShellCID.h" +#include "IContentSecurityPolicy.h" +#include "prlog.h" +#include "nsIChannelPolicy.h" +#include "nsChannelPolicy.h" + +#ifdef PR_LOGGING +static PRLogModuleInfo* gCspPRLog; +#endif + ////////////////////////////////////////////////////////////// // Per-request data structure @@ -129,6 +138,11 @@ nsScriptLoader::nsScriptLoader(nsIDocume mDeferEnabled(PR_FALSE), mUnblockOnloadWhenDoneProcessing(PR_FALSE) { + // enable logging for CSP +#ifdef PR_LOGGING + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); +#endif } nsScriptLoader::~nsScriptLoader() @@ -265,10 +279,23 @@ nsScriptLoader::StartLoad(nsScriptLoadRe nsCOMPtr prompter(do_QueryInterface(docshell)); + // check for a Content Security Policy to pass down to the channel + // that will be created to load the script + nsCOMPtr channelPolicy; + nsCOMPtr csp; + rv = mDocument->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if (csp) { + channelPolicy = new nsChannelPolicy(); + channelPolicy->SetContentSecurityPolicy(csp); + channelPolicy->SetLoadType(2); // TYPE_SCRIPT + } + nsCOMPtr channel; - rv = NS_NewChannel(getter_AddRefs(channel), - aRequest->mURI, nsnull, loadGroup, - prompter, nsIRequest::LOAD_NORMAL); + rv = NS_NewChannelWithPolicy(getter_AddRefs(channel), + aRequest->mURI, nsnull, loadGroup, + prompter, nsIRequest::LOAD_NORMAL, + channelPolicy); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr httpChannel(do_QueryInterface(channel)); @@ -523,6 +550,38 @@ nsScriptLoader::ProcessScriptElement(nsI return request->mDefer ? NS_OK : NS_ERROR_HTMLPARSER_BLOCK; } } + else { + // in-line script + nsCOMPtr csp; + nsresult rv = mDocument->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + + // if NodePrincipal is a null principal, GetCsp will complain that CSP is + // not available (it doesn't make sense for a null principal). So we have + // to continue, and csp will be empty when it's checked in a couple lines. + if (rv == NS_ERROR_NOT_AVAILABLE) + rv = NS_OK; + NS_ENSURE_SUCCESS(rv, rv); + + if (csp) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("ScriptLoader's document has CSP")); +#endif + PRBool inlineOK = PR_TRUE; + // this call will send violation reports when necessary + rv = csp->GetAllowsInlineScript(&inlineOK); + NS_ENSURE_SUCCESS(rv, rv); + + if (!inlineOK) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, (" CSP blocked inline scripts")); +#endif + return NS_ERROR_FAILURE; + } + } +#ifdef PR_LOGGING + else PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("ScriptLoader's document NO CSP")); +#endif + } // Create a request object for this script request = new nsScriptLoadRequest(aElement, version); @@ -541,6 +600,34 @@ nsScriptLoader::ProcessScriptElement(nsI return rv; } } else { + // in-line script + nsCOMPtr csp; + nsresult rv = mDocument->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + + // if NodePrincipal is a null principal, GetCsp will complain that CSP is + // not available (it doesn't make sense for a null principal). So we have + // to continue, and csp will be empty when it's checked in a couple lines. + if (rv == NS_ERROR_NOT_AVAILABLE) + rv = NS_OK; + NS_ENSURE_SUCCESS(rv, rv); + + if (csp) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("New ScriptLoader i ****with CSP****")); +#endif + PRBool inlineOK; + // this call will send violation reports when necessary + rv = NS_SUCCEEDED(csp->GetAllowsInlineScript(&inlineOK)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!inlineOK) { +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP blocked inline scripts --2nd chance!")); +#endif + return NS_ERROR_FAILURE; + } + } + request->mLoading = PR_FALSE; request->mIsInline = PR_TRUE; request->mURI = mDocument->GetDocumentURI(); 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 @@ -321,6 +321,21 @@ _TEST_FILES = test_bug5141.html \ test_classList.html \ test_bug514487.html \ test_range_bounds.html \ + test_CSP.html \ + file_CSP.sjs \ + file_CSP_main.html \ + file_CSP_main.js \ + test_CSP_frameancestors.html \ + file_CSP_frameancestors.sjs \ + file_CSP_frameancestors_main.html \ + file_CSP_frameancestors_main.js \ + test_CSP_inlinescript.html \ + file_CSP_inlinescript.sjs \ + file_CSP_inlinescript_main.html \ + test_CSP_evalscript.html \ + file_CSP_evalscript.sjs \ + file_CSP_evalscript_main.html \ + file_CSP_evalscript_main.js \ $(NULL) # Disabled; see bug 492181 diff --git a/content/base/test/file_CSP.sjs b/content/base/test/file_CSP.sjs new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP.sjs @@ -0,0 +1,44 @@ +// SJS file for CSP mochitests + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var isPreflight = request.method == "OPTIONS"; + + + //avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if ("main" in query) { + var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Components.interfaces.nsIXMLHttpRequest); + //serve the main page with a CSP header! + // -- anything served from 'self' (localhost:8888) will be allowed, + // -- anything served from other hosts (example.com:80) will be blocked. + // -- XHR tests are set up in the file_CSP_main.js file which is sourced. + response.setHeader("X-Content-Security-Policy", + "allow 'self'", + false); + xhr.open("GET", "http://localhost:8888/tests/content/base/test/file_CSP_main.html", false); + xhr.send(null); + if(xhr.status == 200) { + response.write(xhr.responseText); + } + } else { + if ("type" in query) { + response.setHeader("Content-Type", unescape(query['type']), false); + } else { + response.setHeader("Content-Type", "text/html", false); + } + + if ("content" in query) { + response.setHeader("Content-Type", "text/html", false); + response.write(unescape(query['content'])); + } + } +} diff --git a/content/base/test/file_CSP_evalscript.sjs b/content/base/test/file_CSP_evalscript.sjs new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_evalscript.sjs @@ -0,0 +1,30 @@ +// SJS file for CSP eval script mochitests + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + //avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if ("main" in query) { + var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Components.interfaces.nsIXMLHttpRequest); + //serve the main page with a CSP header! + // -- anything served from 'self' (localhost:8888) will be allowed, + // -- anything served from other hosts (example.com:80) will be blocked. + // -- XHR tests are set up in the file_CSP_main.js file which is sourced. + response.setHeader("X-Content-Security-Policy", + "allow 'self'", + false); + xhr.open("GET", "http://localhost:8888/tests/content/base/test/file_CSP_evalscript_main.html", false); + xhr.send(null); + if(xhr.status == 200) { + response.write(xhr.responseText); + } + } +} diff --git a/content/base/test/file_CSP_evalscript_main.html b/content/base/test/file_CSP_evalscript_main.html new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_evalscript_main.html @@ -0,0 +1,12 @@ + + + CSP eval script tests + + + + + Foo. + + + diff --git a/content/base/test/file_CSP_evalscript_main.js b/content/base/test/file_CSP_evalscript_main.js new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_evalscript_main.js @@ -0,0 +1,69 @@ +// some javascript for the CSP eval() tests + +function logResult(str, passed) { + var elt = document.createElement('div'); + var color = passed ? "#cfc;" : "#fcc"; + elt.setAttribute('style', 'background-color:' + color + '; width:100%; border:1px solid black; padding:3px; margin:4px;'); + elt.innerHTML = str; + document.body.appendChild(elt); +} + + +// callback for when stuff is allowed by CSP +var onevalexecuted = (function(window) { + return function(shouldrun, what, data) { + window.parent.scriptRan(shouldrun, what, data); + logResult((shouldrun ? "PASS: " : "FAIL: ") + what + " : " + data, shouldrun); + };})(window); + +// callback for when stuff is blocked +var onevalblocked = (function(window) { + return function(shouldrun, what, data) { + window.parent.scriptBlocked(shouldrun, what, data); + logResult((shouldrun ? "FAIL: " : "PASS: ") + what + " : " + data, !shouldrun); + };})(window); + + +// Defer until document is loaded so that we can write the pretty result boxes +// out. +addEventListener('load', function() { + + // setTimeout(String) test -- should pass + try { + setTimeout('onevalexecuted(false, "setTimeout(String)", "setTimeout with a string was enabled.");', 10); + } catch (e) { + onevalblocked(false, "setTimeout(String)", + "setTimeout with a string was blocked"); + } + + // setTimeout(function) test -- should pass + try { + setTimeout(function() { + onevalexecuted(true, "setTimeout(function)", + "setTimeout with a function was enabled.") + }, 10); + } catch (e) { + onevalblocked(true, "setTimeout(function)", + "setTimeout with a function was blocked"); + } + + // eval() test + try { + eval('onevalexecuted(false, "eval(String)", "eval() was enabled.");'); + } catch (e) { + onevalblocked(false, "eval(String)", + "eval() was blocked"); + } + + // new Function() test + try { + var fcn = new Function('onevalexecuted(false, "new Function(String)", "new Function(String) was enabled.");'); + fcn(); + } catch (e) { + onevalblocked(false, "new Function(String)", + "new Function(String) was blocked."); + } +}, false); + + + diff --git a/content/base/test/file_CSP_frameancestors.sjs b/content/base/test/file_CSP_frameancestors.sjs new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_frameancestors.sjs @@ -0,0 +1,57 @@ +// SJS file for CSP frame ancestor mochitests +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var isPreflight = request.method == "OPTIONS"; + + + //avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // grab the desired policy from the query, and then serve a page + if (query['csp']) + response.setHeader("X-Content-Security-Policy", + unescape(query['csp']), + false); + if (query['scriptedreport']) { + // spit back a script that records that the page loaded + response.setHeader("Content-Type", "text/javascript", false); + response.write('netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");'); + if (query['double']) + response.write('window.parent.parent.parent.frameLoaded("' + query['scriptedreport'] + '", ' + + 'window.location.toString());'); + else + response.write('window.parent.parent.frameLoaded("' + query['scriptedreport'] + '", ' + + 'window.location.toString());'); + } else if (query['internalframe']) { + // spit back an internal iframe (one that might be blocked) + response.setHeader("Content-Type", "text/html", false); + response.write(''); + if (query['double']) + response.write(''); + else + response.write(''); + response.write(''); + response.write(unescape(query['internalframe'])); + response.write(''); + } else if (query['externalframe']) { + // spit back an internal iframe (one that won't be blocked, and probably + // has no CSP) + response.setHeader("Content-Type", "text/html", false); + response.write(''); + response.write(''); + response.write(unescape(query['externalframe'])); + response.write(''); + } else { + // default case: error. + response.setHeader("Content-Type", "text/html", false); + response.write(''); + response.write("ERROR: not sure what to serve."); + response.write(''); + } +} diff --git a/content/base/test/file_CSP_frameancestors_main.html b/content/base/test/file_CSP_frameancestors_main.html new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_frameancestors_main.html @@ -0,0 +1,44 @@ + + + CSP frame ancestors tests + + + + + + + + + aa_allow: /* innermost frame allows a */
+
+ + aa_block: /* innermost frame denies a */
+
+ + ab_allow: /* innermost frame allows a */
+
+ + ab_block: /* innermost frame denies a */
+
+ + aba_allow: /* innermost frame allows b,a */
+
+ + aba_block: /* innermost frame denies b */
+
+ + aba2_block: /* innermost frame denies a */
+
+ + abb_allow: /* innermost frame allows b,a */
+
+ + abb_block: /* innermost frame denies b */
+
+ + abb2_block: /* innermost frame denies a */
+
+ + + + diff --git a/content/base/test/file_CSP_frameancestors_main.js b/content/base/test/file_CSP_frameancestors_main.js new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_frameancestors_main.js @@ -0,0 +1,65 @@ +// Script to populate the test frames in the frame ancestors mochitest. +// +function setupFrames() { + + var $ = function(v) { return document.getElementById(v); } + var base = { + self: '/tests/content/base/test/file_CSP_frameancestors.sjs', + a: 'http://localhost:8888/tests/content/base/test/file_CSP_frameancestors.sjs', + b: 'http://example.com/tests/content/base/test/file_CSP_frameancestors.sjs' + }; + + var host = { a: 'http://localhost:8888', b: 'http://example.com:80' }; + + var innerframeuri = null; + var elt = null; + + elt = $('aa_allow'); + elt.src = base.a + "?testid=aa_allow&internalframe=aa_a&csp=" + + escape("allow 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + + elt = $('aa_block'); + elt.src = base.a + "?testid=aa_block&internalframe=aa_b&csp=" + + escape("allow 'none'; frame-ancestors 'none'; script-src 'self'"); + + elt = $('ab_allow'); + elt.src = base.b + "?testid=ab_allow&internalframe=ab_a&csp=" + + escape("allow 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + + elt = $('ab_block'); + elt.src = base.b + "?testid=ab_block&internalframe=ab_b&csp=" + + escape("allow 'none'; frame-ancestors 'none'; script-src 'self'"); + + /* .... two-level framing */ + elt = $('aba_allow'); + innerframeuri = base.a + "?testid=aba_allow&double=1&internalframe=aba_a&csp=" + + escape("allow 'none'; frame-ancestors " + host.a + " " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape(''); + + elt = $('aba_block'); + innerframeuri = base.a + "?testid=aba_allow&double=1&internalframe=aba_b&csp=" + + escape("allow 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape(''); + + elt = $('aba2_block'); + innerframeuri = base.a + "?testid=aba_allow&double=1&internalframe=aba2_b&csp=" + + escape("allow 'none'; frame-ancestors " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape(''); + + elt = $('abb_allow'); + innerframeuri = base.b + "?testid=abb_allow&double=1&internalframe=abb_a&csp=" + + escape("allow 'none'; frame-ancestors " + host.a + " " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape(''); + + elt = $('abb_block'); + innerframeuri = base.b + "?testid=abb_allow&double=1&internalframe=abb_b&csp=" + + escape("allow 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape(''); + + elt = $('abb2_block'); + innerframeuri = base.b + "?testid=abb_allow&double=1&internalframe=abb2_b&csp=" + + escape("allow 'none'; frame-ancestors " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape(''); +} + +window.addEventListener('load', setupFrames, false); diff --git a/content/base/test/file_CSP_inlinescript.sjs b/content/base/test/file_CSP_inlinescript.sjs new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_inlinescript.sjs @@ -0,0 +1,30 @@ +// SJS file for CSP inline script mochitests + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + //avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if ("main" in query) { + var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Components.interfaces.nsIXMLHttpRequest); + //serve the main page with a CSP header! + // -- anything served from 'self' (localhost:8888) will be allowed, + // -- anything served from other hosts (example.com:80) will be blocked. + // -- XHR tests are set up in the file_CSP_main.js file which is sourced. + response.setHeader("X-Content-Security-Policy", + "allow 'self'", + false); + xhr.open("GET", "http://localhost:8888/tests/content/base/test/file_CSP_inlinescript_main.html", false); + xhr.send(null); + if(xhr.status == 200) { + response.write(xhr.responseText); + } + } +} diff --git a/content/base/test/file_CSP_inlinescript_main.html b/content/base/test/file_CSP_inlinescript_main.html new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_inlinescript_main.html @@ -0,0 +1,15 @@ + + + CSP inline script tests + + + + + + + + + diff --git a/content/base/test/file_CSP_main.html b/content/base/test/file_CSP_main.html new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_main.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
arbitrary good
+
arbitrary_bad
+ + diff --git a/content/base/test/file_CSP_main.js b/content/base/test/file_CSP_main.js new file mode 100644 --- /dev/null +++ b/content/base/test/file_CSP_main.js @@ -0,0 +1,16 @@ +// some javascript for the CSP XHR tests +// + +try { + var xhr_good = new XMLHttpRequest(); + var xhr_good_uri ="http://localhost:8888/tests/content/base/test/file_CSP.sjs?testid=xhr_good"; + xhr_good.open("GET", xhr_good_uri, true); + xhr_good.send(null); +} catch(e) {} + +try { + var xhr_bad = new XMLHttpRequest(); + var xhr_bad_uri ="http://example.com/tests/content/base/test/file_CSP.sjs?testid=xhr_bad"; + xhr_bad.open("GET", xhr_bad_uri, true); + xhr_bad.send(null); +} catch(e) {} diff --git a/content/base/test/test_CSP.html b/content/base/test/test_CSP.html new file mode 100644 --- /dev/null +++ b/content/base/test/test_CSP.html @@ -0,0 +1,127 @@ + + + + Test for Content Security Policy Connections + + + + + +

+ + + + + + + diff --git a/content/base/test/test_CSP_evalscript.html b/content/base/test/test_CSP_evalscript.html new file mode 100644 --- /dev/null +++ b/content/base/test/test_CSP_evalscript.html @@ -0,0 +1,62 @@ + + + + Test for Content Security Policy "no eval" base restriction + + + + + +

+ + + + + + + diff --git a/content/base/test/test_CSP_frameancestors.html b/content/base/test/test_CSP_frameancestors.html new file mode 100644 --- /dev/null +++ b/content/base/test/test_CSP_frameancestors.html @@ -0,0 +1,120 @@ + + + + Test for Content Security Policy Frame Ancestors directive + + + + + +

+ + + + + + + diff --git a/content/base/test/test_CSP_inlinescript.html b/content/base/test/test_CSP_inlinescript.html new file mode 100644 --- /dev/null +++ b/content/base/test/test_CSP_inlinescript.html @@ -0,0 +1,100 @@ + + + + Test for Content Security Policy Frame Ancestors directive + + + + + +

+ + + + + + + diff --git a/content/base/test/unit/test_csputils.js b/content/base/test/unit/test_csputils.js new file mode 100644 --- /dev/null +++ b/content/base/test/unit/test_csputils.js @@ -0,0 +1,497 @@ +/* ***** 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 testing code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * + * Contributor(s): + * Sid Stamm + * + * 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 ***** */ + +//load('CSPUtils.jsm'); +Components.utils.import('resource://gre/modules/CSPUtils.jsm'); + +// load the HTTP server +do_load_httpd_js(); + +var httpServer = new nsHttpServer(); + +const POLICY_FROM_URI = "allow 'self'; img-src *"; +const POLICY_PORT = 9000; +const POLICY_URI = "http://localhost:" + POLICY_PORT + "/policy"; + +// helper to assert that an object or array must have a given key +function do_check_has_key(foo, key, stack) { + if (!stack) + stack = Components.stack.caller; + + var keys = []; + for(let k in keys) { keys.push(k); } + var text = key + " in [" + keys.join(",") + "]"; + + for(var x in foo) { + if(x == key) { + //succeed + ++_passedChecks; + dump("TEST-PASS | " + stack.filename + " | [" + stack.name + " : " + + stack.lineNumber + "] " + text + "\n"); + return; + } + } + do_throw(text, stack); +} + +// helper to use .equals on stuff +function do_check_equivalent(foo, bar, stack) { + if (!stack) + stack = Components.stack.caller; + + var text = foo + ".equals(" + bar + ")"; + + if(foo.equals && foo.equals(bar)) { + ++_passedChecks; + dump("TEST-PASS | " + stack.filename + " | [" + stack.name + " : " + + stack.lineNumber + "] " + text + "\n"); + return; + } + do_throw(text, stack); +} + +var tests = []; +function test(fcn) { + tests.push(fcn); +} + +test( + function test_CSPHost_fromstring() { + var h; + + h = CSPHost.fromString("*"); + do_check_neq(null, h); // "* lone wildcard should work" + + h = CSPHost.fromString("foo.bar"); + do_check_neq(null, h); // "standard tuple failed" + + h = CSPHost.fromString("*.bar"); + do_check_neq(null, h); // "wildcard failed" + + h = CSPHost.fromString("foo.*.bar"); + do_check_eq(null, h); // "wildcard in wrong place worked" + + h = CSPHost.fromString("com"); + do_check_eq(null, h); // "lone symbol should fail" + + h = CSPHost.fromString("f00b4r.com"); + do_check_neq(null, h); // "Numbers in hosts should work" + + h = CSPHost.fromString("foo-bar.com"); + do_check_neq(null, h); // "dashes in hosts should work" + + h = CSPHost.fromString("foo!bar.com"); + do_check_eq(null, h); // "special chars in hosts should fail" + }); + +test( + function test_CSPHost_clone() { + h = CSPHost.fromString("*.a.b.c"); + h2 = h.clone(); + for(var i in h._segments) { + // "cloned segments should match" + do_check_eq(h._segments[i], h2._segments[i]); + } + }); + +test( + function test_CSPHost_permits() { + var h = CSPHost.fromString("*.b.c"); + var h2 = CSPHost.fromString("a.b.c"); + do_check_true( h.permits(h2)); //"CSPHost *.b.c should allow CSPHost a.b.c" + do_check_true( h.permits("a.b.c")); //"CSPHost *.b.c should allow string a.b.c" + do_check_false(h.permits("b.c")); //"CSPHost *.b.c should not allow string b.c" + do_check_false(h.permits("a.a.c")); //"CSPHost *.b.c should not allow string a.a.c" + do_check_false(h2.permits(h)); //"CSPHost a.b.c should not allow CSPHost *.b.c" + do_check_false(h2.permits("b.c")); //"CSPHost a.b.c should not allow string b.c" + do_check_true( h2.permits("a.b.c")); //"CSPHost a.b.c should allow string a.b.c" + }); + +test( + function test_CSPHost_intersectWith() { + var h = CSPHost.fromString("*.b.c"); + //"*.a.b.c ^ *.b.c should be *.a.b.c" + do_check_eq("*.a.b.c", h.intersectWith(CSPHost.fromString("*.a.b.c")).toString()); + + //"*.b.c ^ *.d.e should not work (null)" + do_check_eq(null, h.intersectWith(CSPHost.fromString("*.d.e"))); + }); + +///////////////////// Test the Source object ////////////////////// + +test( + function test_CSPSource_fromString() { + // can't do these tests because "self" is not defined. + //"basic source should not be null."); + do_check_neq(null, CSPSource.fromString("a.com")); + + //"ldh characters should all work for host."); + do_check_neq(null, CSPSource.fromString("a2-c.com")); + + //"wildcard should work in first token for host."); + do_check_neq(null, CSPSource.fromString("*.a.com")); + + //print(" --- Ignore the following two errors if they print ---"); + //"wildcard should not work in non-first token for host."); + do_check_eq(null, CSPSource.fromString("x.*.a.com")); + + //"funny characters (#) should not work for host."); + do_check_eq(null, CSPSource.fromString("a#2-c.com")); + //print(" --- Stop ignoring errors that print ---\n"); + + //"failed to parse host with port."); + do_check_neq(null, CSPSource.create("a.com:23")); + //"failed to parse host with scheme."); + do_check_neq(null, CSPSource.create("https://a.com")); + //"failed to parse host with scheme and port."); + do_check_neq(null, CSPSource.create("https://a.com:200")); + }); + +test( + function test_CSPSource_fromString_withSelf() { + var src; + src = CSPSource.create("a.com", "https://foobar.com:443"); + //"src should inherit port * + do_check_true(src.permits("https://a.com:443")); + //"src should inherit and require https scheme + do_check_false(src.permits("http://a.com")); + //"src should inherit scheme 'https'" + do_check_true(src.permits("https://a.com")); + + src = CSPSource.create("http://a.com", "https://foobar.com:443"); + //"src should inherit and require http scheme" + do_check_false(src.permits("https://a.com")); + //"src should inherit scheme 'http'" + do_check_true(src.permits("http://a.com")); + //"src should inherit port and scheme from parent" + //"src should inherit default port for 'http'" + do_check_true(src.permits("http://a.com:80")); + + src = CSPSource.create("'self'", "https://foobar.com:443"); + //"src should inherit port * + do_check_true(src.permits("https://foobar.com:443")); + //"src should inherit and require https scheme + do_check_false(src.permits("http://foobar.com")); + //"src should inherit scheme 'https'" + do_check_true(src.permits("https://foobar.com")); + //"src should reject other hosts" + do_check_false(src.permits("https://a.com")); + }); + +///////////////////// Test the source list ////////////////////// + +test( + function test_CSPSourceList_fromString() { + var sd = CSPSourceList.fromString("'none'"); + //"'none' -- should parse" + do_check_neq(null,sd); + // "'none' should be a zero-length list" + do_check_eq(0, sd._sources.length); + do_check_true(sd.isNone()); + + sd = CSPSourceList.fromString("*"); + //"'*' should be a zero-length list" + do_check_eq(0, sd._sources.length); + + //print(" --- Ignore the following three errors if they print ---"); + //"funny char in host" + do_check_true(CSPSourceList.fromString("f!oo.bar").isNone()); + //"funny char in scheme" + do_check_true(CSPSourceList.fromString("ht!ps://f-oo.bar").isNone()); + //"funny char in port" + do_check_true(CSPSourceList.fromString("https://f-oo.bar:3f").isNone()); + //print(" --- Stop ignoring errors that print ---\n"); + }); + +test( + function test_CSPSourceList_fromString_twohost() { + var str = "foo.bar:21 https://ras.bar"; + var parsed = "foo.bar:21 https://ras.bar"; + var sd = CSPSourceList.fromString(str, "http://self.com:80"); + //"two-host list should parse" + do_check_neq(null,sd); + //"two-host list should parse to two hosts" + do_check_eq(2, sd._sources.length); + //"two-host list should contain original data" + do_check_eq(parsed, sd.toString()); + }); + +test( + function test_CSPSourceList_permits() { + var nullSourceList = CSPSourceList.fromString("'none'"); + var simpleSourceList = CSPSourceList.fromString("a.com", "http://self.com"); + var doubleSourceList = CSPSourceList.fromString("https://foo.com http://bar.com:88", + "http://self.com:88"); + var allSourceList = CSPSourceList.fromString("*"); + + //'none' should permit none." + do_check_false( nullSourceList.permits("http://a.com")); + //a.com should permit a.com" + do_check_true( simpleSourceList.permits("http://a.com")); + //wrong host" + do_check_false( simpleSourceList.permits("http://b.com")); + //double list permits http://bar.com:88" + do_check_true( doubleSourceList.permits("http://bar.com:88")); + //double list permits https://bar.com:88" + do_check_false( doubleSourceList.permits("https://bar.com:88")); + //double list does not permit http://bar.com:443" + do_check_false( doubleSourceList.permits("http://bar.com:443")); + //"double list permits https://foo.com:88" (should not inherit port) + do_check_false( doubleSourceList.permits("https://foo.com:88")); + //"double list does not permit foo.com on http" + do_check_false( doubleSourceList.permits("http://foo.com")); + + //"* does not permit specific host" + do_check_true( allSourceList.permits("http://x.com:23")); + //"* does not permit a long host with no port" + do_check_true( allSourceList.permits("http://a.b.c.d.e.f.g.h.i.j.k.l.x.com")); + + }); + +test( + function test_CSPSourceList_intersect() { + // for this test, 'self' values are irrelevant + // policy a /\ policy b intersects policies, not context (where 'self' + // values come into play) + var nullSourceList = CSPSourceList.fromString("'none'"); + var simpleSourceList = CSPSourceList.fromString("a.com"); + var doubleSourceList = CSPSourceList.fromString("https://foo.com http://bar.com:88"); + var singleFooSourceList = CSPSourceList.fromString("https://foo.com"); + var allSourceList = CSPSourceList.fromString("*"); + + //"Intersection of one source with 'none' source list should be none."); + do_check_true(nullSourceList.intersectWith(simpleSourceList).isNone()); + //"Intersection of two sources with 'none' source list should be none."); + do_check_true(nullSourceList.intersectWith(doubleSourceList).isNone()); + //"Intersection of '*' with 'none' source list should be none."); + do_check_true(nullSourceList.intersectWith(allSourceList).isNone()); + + //"Intersection of one source with '*' source list should be one source."); + do_check_equivalent(allSourceList.intersectWith(simpleSourceList), + simpleSourceList); + //"Intersection of two sources with '*' source list should be two sources."); + do_check_equivalent(allSourceList.intersectWith(doubleSourceList), + doubleSourceList); + + //"Non-overlapping source lists should intersect to 'none'"); + do_check_true(simpleSourceList.intersectWith(doubleSourceList).isNone()); + + //"subset and superset should intersect to subset."); + do_check_equivalent(singleFooSourceList, + doubleSourceList.intersectWith(singleFooSourceList)); + + //TODO: write more tests? + + }); + +///////////////////// Test the Whole CSP rep object ////////////////////// + +test( + function test_CSPRep_fromString() { + + // check default init + //ASSERT(!(new CSPRep())._isInitialized, "Uninitialized rep thinks it is.") + + var cspr; + var cspr_allowval; + + // check default policy "allow *" + cspr = CSPRep.fromString("allow *", "http://self.com:80"); + //"ALLOW directive is missing when specified in fromString" + do_check_has_key(cspr._directives, CSPRep.SRC_DIRECTIVES.ALLOW); + + // ... and check that the other directives were auto-filled with the + // ALLOW one. + var SD = CSPRep.SRC_DIRECTIVES; + cspr_allowval = cspr._directives[SD.ALLOW]; + for(var d in CSPRep.SRC_DIRECTIVES) { + //"Missing key " + d + do_check_has_key(cspr._directives, SD[d]); + //"Implicit directive " + d + " has non-allow value." + do_check_eq(cspr._directives[SD[d]].toString(), cspr_allowval.toString()); + } + }); + + +test( + function test_CSPRep_fromString_oneDir() { + + var cspr; + var SD = CSPRep.SRC_DIRECTIVES; + var DEFAULTS = [SD.STYLE_SRC, SD.MEDIA_SRC, SD.IMG_SRC, + SD.FRAME_ANCESTORS, SD.FRAME_SRC]; + + // check one-directive policies + cspr = CSPRep.fromString("allow bar.com; script-src https://foo.com", + "http://self.com"); + + for(var x in DEFAULTS) { + //DEFAULTS[x] + " does not use default rule." + do_check_false(cspr.permits("http://bar.com:22", DEFAULTS[x])); + //DEFAULTS[x] + " does not use default rule." + do_check_true(cspr.permits("http://bar.com:80", DEFAULTS[x])); + //DEFAULTS[x] + " does not use default rule." + do_check_false(cspr.permits("https://foo.com:400", DEFAULTS[x])); + //DEFAULTS[x] + " does not use default rule." + do_check_false(cspr.permits("https://foo.com", DEFAULTS[x])); + } + //"script-src false positive in policy. + do_check_false(cspr.permits("http://bar.com:22", SD.SCRIPT_SRC)); + //"script-src false negative in policy. + do_check_true(cspr.permits("https://foo.com:443", SD.SCRIPT_SRC)); + }); + +test( + function test_CSPRep_fromString_twodir() { + var cspr; + var SD = CSPRep.SRC_DIRECTIVES; + var DEFAULTS = [SD.STYLE_SRC, SD.MEDIA_SRC, SD.FRAME_ANCESTORS, SD.FRAME_SRC]; + + // check two-directive policies + var polstr = "allow allow.com; " + + "script-src https://foo.com; " + + "img-src bar.com:*"; + cspr = CSPRep.fromString(polstr, "http://self.com"); + + for(var x in DEFAULTS) { + do_check_true(cspr.permits("http://allow.com", DEFAULTS[x])); + //DEFAULTS[x] + " does not use default rule. + do_check_false(cspr.permits("https://foo.com:400", DEFAULTS[x])); + //DEFAULTS[x] + " does not use default rule. + do_check_false(cspr.permits("http://bar.com:400", DEFAULTS[x])); + //DEFAULTS[x] + " does not use default rule. + } + //"img-src does not use default rule. + do_check_false(cspr.permits("http://allow.com:22", SD.IMG_SRC)); + //"img-src does not use default rule. + do_check_false(cspr.permits("https://foo.com:400", SD.IMG_SRC)); + //"img-src does not use default rule. + do_check_true(cspr.permits("http://bar.com:88", SD.IMG_SRC)); + + //"script-src does not use default rule. + do_check_false(cspr.permits("http://allow.com:22", SD.SCRIPT_SRC)); + //"script-src does not use default rule. + do_check_true(cspr.permits("https://foo.com:443", SD.SCRIPT_SRC)); + //"script-src does not use default rule. + do_check_false(cspr.permits("http://bar.com:400", SD.SCRIPT_SRC)); + }); + +test(function test_CSPRep_fromString_withself() { + var cspr; + var SD = CSPRep.SRC_DIRECTIVES; + var self = "https://self.com:34"; + + // check one-directive policies + cspr = CSPRep.fromString("allow 'self'; script-src 'self' https://*:*", + self); + //"img-src does not enforce default rule, 'self'. + do_check_false(cspr.permits("https://foo.com:400", SD.IMG_SRC)); + //"img-src does not allow self + CSPdebug(cspr); + do_check_true(cspr.permits(self, SD.IMG_SRC)); + //"script-src is too relaxed + do_check_false(cspr.permits("http://evil.com", SD.SCRIPT_SRC)); + //"script-src should allow self + do_check_true(cspr.permits(self, SD.SCRIPT_SRC)); + //"script-src is too strict on host/port + do_check_true(cspr.permits("https://evil.com:100", SD.SCRIPT_SRC)); + }); + +///////////////////// TEST POLICY_URI ////////////////////// +test(function test_CSPRep_fromPolicyURI() { + var cspr; + var SD = CSPRep.SRC_DIRECTIVES; + var self = "http://localhost:" + POLICY_PORT; + + cspr = CSPRep.fromString("policy-uri " + POLICY_URI, self); + cspr_static = CSPRep.fromString(POLICY_FROM_URI, self); + + //"policy-uri failed to load" + do_check_neq(null,cspr); + + // other directives inherit self + for(var i in SD) { + //SD[i] + " parsed wrong from policy uri" + do_check_equivalent(cspr._directives[SD[i]], + cspr_static._directives[SD[i]]); + } + }); +/* + +test(function test_CSPRep_fromPolicyURI_failswhenmixed() { + var cspr; + var self = "http://localhost:" + POLICY_PORT; + var closed_policy = CSPRep.fromString("allow 'none'"); + var my_uri_policy = "policy-uri " + POLICY_URI; + + //print(" --- Ignore the following two errors if they print ---"); + cspr = CSPRep.fromString("allow *; " + my_uri_policy, self); + + //"Parsing should fail when 'policy-uri' is mixed with allow directive" + do_check_equivalent(cspr, closed_policy); + cspr = CSPRep.fromString("img-src 'self'; " + my_uri_policy, self); + + //"Parsing should fail when 'policy-uri' is mixed with other directives" + do_check_equivalent(cspr, closed_policy); + //print(" --- Stop ignoring errors that print ---\n"); + + }); +*/ + +// TODO: test reporting +// TODO: test refinements (?) +// TODO: test 'eval' and 'inline' keywords + +function run_test() { + function policyresponder(request,response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/csp", false); + response.bodyOutputStream.write(POLICY_FROM_URI, POLICY_FROM_URI.length); + } + //server.registerDirectory("/", nsILocalFileForBasePath); + httpServer.registerPathHandler("/policy", policyresponder); + httpServer.start(POLICY_PORT); + + for(let i in tests) { + tests[i](); + } + + //teardown + httpServer.stop(function() { }); + do_test_finished(); +} + + + 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 @@ -95,6 +95,7 @@ #include "nsCOMArray.h" #include "nsEventListenerService.h" #include "nsDOMEvent.h" +#include "IContentSecurityPolicy.h" #define EVENT_TYPE_EQUALS( ls, type, userType ) \ (ls->mEventType && ls->mEventType == type && \ @@ -662,6 +663,8 @@ nsEventListenerManager::AddScriptEventLi return NS_ERROR_FAILURE; } + nsresult rv; + nsCOMPtr node(do_QueryInterface(aObject)); nsCOMPtr doc; @@ -696,6 +699,31 @@ nsEventListenerManager::AddScriptEventLi // loaded as data. return NS_OK; } + + + // return early preventing the event listener from being added + // 'doc' is fetched above + if (doc) { + nsCOMPtr csp; + rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + + if (csp) { + PRBool inlineOK; + // this call will trigger violaton reports if necessary + rv = csp->GetAllowsInlineScript(&inlineOK); + NS_ENSURE_SUCCESS(rv, rv); + + if ( !inlineOK ) { + //can log something here too. + //nsAutoString attr; + //aName->ToString(attr); + //printf(" *** CSP bailing on adding event listener for: %s\n", + // ToNewCString(attr)); + return NS_OK; + } + } + } // This might be the first reference to this language in the global // We must init the language before we attempt to fetch its context. @@ -708,7 +736,6 @@ nsEventListenerManager::AddScriptEventLi NS_ENSURE_TRUE(context, NS_ERROR_FAILURE); void *scope = global->GetScriptGlobal(aLanguage); - nsresult rv; if (!aDeferCompilation) { nsCOMPtr handlerOwner = diff --git a/content/html/content/src/nsHTMLMediaElement.cpp b/content/html/content/src/nsHTMLMediaElement.cpp --- a/content/html/content/src/nsHTMLMediaElement.cpp +++ b/content/html/content/src/nsHTMLMediaElement.cpp @@ -116,6 +116,9 @@ protected: PRUint32 mLoadID; }; +#include "IContentSecurityPolicy.h" +#include "nsIChannelPolicy.h" +#include "nsChannelPolicy.h" class nsAsyncEventRunner : public nsMediaEvent { @@ -557,12 +560,25 @@ nsresult nsHTMLMediaElement::LoadResourc if (NS_CP_REJECTED(shouldLoad)) return NS_ERROR_FAILURE; nsCOMPtr loadGroup = GetDocumentLoadGroup(); - rv = NS_NewChannel(getter_AddRefs(mChannel), - aURI, - nsnull, - loadGroup, - nsnull, - nsIRequest::LOAD_NORMAL); + + // check for a Content Security Policy to pass down to the channel + // created to load the media content + nsCOMPtr channelPolicy; + nsCOMPtr csp; + rv = NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv,rv); + if (csp) { + channelPolicy = new nsChannelPolicy(); + channelPolicy->SetContentSecurityPolicy(csp); + channelPolicy->SetLoadType(15); // TYPE_MEDIA + } + rv = NS_NewChannelWithPolicy(getter_AddRefs(mChannel), + aURI, + nsnull, + loadGroup, + nsnull, + nsIRequest::LOAD_NORMAL, + channelPolicy); NS_ENSURE_SUCCESS(rv,rv); // The listener holds a strong reference to us. This creates a reference diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -205,6 +205,8 @@ #include "nsPluginError.h" +#include "IContentSecurityPolicy.h" + static NS_DEFINE_CID(kDOMScriptObjectFactoryCID, NS_DOM_SCRIPT_OBJECT_FACTORY_CID); static NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID); @@ -215,6 +217,7 @@ static NS_DEFINE_CID(kAppShellCID, NS_AP #endif #include "nsContentErrors.h" +//#include "nsIChannelPolicy.h" // Number of documents currently loading static PRInt32 gNumberOfDocumentsLoading = 0; @@ -3564,6 +3567,14 @@ nsDocShell::DisplayLoadError(nsresult aE formatStrCount = 1; error.AssignLiteral("netTimeout"); } + else if ( NS_ERROR_GET_MODULE(aError) == NS_ERROR_MODULE_SECURITY + && NS_ERROR_GET_CODE(aError) == 99) { + // CSP error + error.AssignLiteral("forbiddenFrameAncestor"); + cssClass.AssignLiteral("neterror"); + // TODO: localize this, or at least put the message into a resource + messageStr.AssignLiteral("CSP Blocked loading of this page because it doesn't trust the page embedding it."); + } else if (NS_ERROR_GET_MODULE(aError) == NS_ERROR_MODULE_SECURITY) { nsCOMPtr nsserr = do_GetService(NS_NSS_ERRORS_SERVICE_CONTRACTID); @@ -8151,6 +8162,27 @@ nsDocShell::DoURILoad(nsIURI * aURI, loadFlags |= nsIChannel::LOAD_BACKGROUND; } + // check for Content Security Policy to pass along with the + // new channel we are creating + /* + nsCOMPtr channelPolicy = + do_CreateInstance("@mozilla.org/nschannelpolicy;1"); + if (IsFrame()) { + nsCOMPtr csp; + nsCOMPtr domDoc = do_GetInterface(GetAsSupports(this), &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr doc = do_QueryInterface(domDoc); + if (doc) { + rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if (csp) { + channelPolicy->SetContentSecurityPolicy(csp); + channelPolicy->SetLoadType(nsIContentPolicy::TYPE_SUBDOCUMENT); + } + } + } + */ + // open a channel for the url nsCOMPtr channel; @@ -10832,6 +10864,38 @@ nsDocShell::OnLinkClickSync(nsIContent * return NS_OK; } + // CSP Check: + // If the requested URI is javascript: and CSP forbids it, cancel the click + PRBool bIsJavascript = PR_FALSE; + if (NS_FAILED(aURI->SchemeIs("javascript", &bIsJavascript))) + bIsJavascript = PR_FALSE; + + if(bIsJavascript) { + nsIDocument* doc = aContent->GetCurrentDoc(); + nsCOMPtr csp; + nsresult rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + + if (csp) { + // the check for allowsInlineScript sends error reports and will return + // TRUE (inline not disabled) if CSP is operating in reportOnly mode. + PRBool allowsInline = PR_FALSE; + NS_ENSURE_SUCCESS(csp->GetAllowsInlineScript(&allowsInline), NS_OK); + + if(!allowsInline) { + // XXX: could log something about the cancellation here + // (report was already sent if needed) + // nsCAutoString urispec; + // aURI->GetSpec(urispec); + // printf("Inline disabled, cancelling load for %s\n", urispec.get()); + + // cancel the javascript: uri link click + return NS_OK; + } + } + } + + { // defer to an external protocol handler if necessary... nsCOMPtr extProtService = diff --git a/dom/base/nsJSTimeoutHandler.cpp b/dom/base/nsJSTimeoutHandler.cpp --- a/dom/base/nsJSTimeoutHandler.cpp +++ b/dom/base/nsJSTimeoutHandler.cpp @@ -50,6 +50,7 @@ #include "nsServiceManagerUtils.h" #include "nsDOMError.h" #include "nsGlobalWindow.h" +#include "IContentSecurityPolicy.h" static const char kSetIntervalStr[] = "setInterval"; static const char kSetTimeoutStr[] = "setTimeout"; @@ -243,6 +244,40 @@ nsJSScriptTimeoutHandler::Init(nsGlobalW } if (expr) { + // if CSP is enabled, and setTimeout/setInterval was called with a string + // or object, disable the registration and log an error + nsCOMPtr domdoc; + aWindow->GetDocument(getter_AddRefs(domdoc)); + nsCOMPtr doc = do_QueryInterface(domdoc); + + if (doc) { + nsCOMPtr csp; + nsresult rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if(csp) { + + ////XXX: This stack push and associated pop went away once the security check + //// was removed from JS (Bug 515475, rev 4448e1b581b7) + //nsCxPusher pusher; + //pusher.PushNull(); + + PRBool allowsEval; + // this call will send violation reports as warranted (and return true if + // reportOnly is set). + rv = csp->GetAllowsEval(&allowsEval); + NS_ENSURE_SUCCESS(rv, rv); + + if (!allowsEval) { + //pusher.Pop(); // -- removed as of rev 4448e1b581b7 + ::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. + rv = NS_HOLD_JS_OBJECTS(this, nsJSScriptTimeoutHandler); NS_ENSURE_SUCCESS(rv, rv); 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 @@ -78,6 +78,7 @@ #include "nsIObjectInputStream.h" #include "nsIObjectOutputStream.h" #include "nsIWritablePropertyBag2.h" +#include "IContentSecurityPolicy.h" static NS_DEFINE_CID(kJSURICID, NS_JSURI_CID); @@ -190,6 +191,25 @@ nsresult nsJSThunk::EvaluateScript(nsICh return NS_ERROR_DOM_RETVAL_UNDEFINED; } + nsresult rv; + + // CSP check: javascript: URIs disabled unless "inline" scripts are + // allowed. + nsCOMPtr csp; + rv = principal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if(csp) { + PRBool allowsInline; + // this call will send violation reports as warranted (and return true if + // reportOnly is set). + rv = csp->GetAllowsInlineScript(&allowsInline); + NS_ENSURE_SUCCESS(rv, rv); + + // TODO: log that we're blocking this javascript: uri + if (!allowsInline) + return NS_ERROR_DOM_RETVAL_UNDEFINED; + } + // Get the global object we should be running on. nsIScriptGlobalObject* global = GetGlobalObject(aChannel); @@ -213,7 +233,6 @@ nsresult nsJSThunk::EvaluateScript(nsICh JSObject *globalJSObject = innerGlobal->GetGlobalJSObject(); - nsresult rv; nsCOMPtr domWindow(do_QueryInterface(global, &rv)); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; diff --git a/embedding/browser/webBrowser/nsContextMenuInfo.cpp b/embedding/browser/webBrowser/nsContextMenuInfo.cpp --- a/embedding/browser/webBrowser/nsContextMenuInfo.cpp +++ b/embedding/browser/webBrowser/nsContextMenuInfo.cpp @@ -59,6 +59,10 @@ #include "nsIDOMCSSPrimitiveValue.h" #include "nsNetUtil.h" #include "nsUnicharUtils.h" +#include "nsIDocument.h" +#include "nsIPrincipal.h" +#include "nsIChannelPolicy.h" +#include "IContentSecurityPolicy.h" //***************************************************************************** // class nsContextMenuInfo @@ -299,6 +303,21 @@ nsContextMenuInfo::GetBackgroundImageReq nsCOMPtr primitiveValue; nsAutoString bgStringValue; + // get Content Security Policy to pass to LoadImage + nsCOMPtr doc(do_QueryInterface(document)); + nsCOMPtr principal; + nsCOMPtr channelPolicy; + nsCOMPtr csp; + if (doc) { + principal = doc->NodePrincipal(); + nsresult rv = principal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if (csp) { + channelPolicy->SetContentSecurityPolicy(csp); + channelPolicy->SetLoadType(3); // TYPE_IMAGE + } + } + while (PR_TRUE) { nsCOMPtr domElement(do_QueryInterface(domNode)); // bail for the parent node of the root element or null argument @@ -326,7 +345,7 @@ nsContextMenuInfo::GetBackgroundImageReq return il->LoadImage(bgUri, nsnull, nsnull, nsnull, nsnull, nsnull, nsIRequest::LOAD_NORMAL, nsnull, nsnull, - aRequest); + channelPolicy, aRequest); } } diff --git a/js/src/jsapi.h b/js/src/jsapi.h --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -2027,6 +2027,7 @@ struct JSSecurityCallbacks { JSCheckAccessOp checkObjectAccess; JSPrincipalsTranscoder principalsTranscoder; JSObjectPrincipalsFinder findObjectPrincipals; + JSCSPEvalChecker contentSecurityPolicyAllows; }; 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 @@ -2182,6 +2182,16 @@ Function(JSContext *cx, JSObject *obj, u if (!js_CheckPrincipalsAccess(cx, parent, principals, CLASS_ATOM(cx, Function))) { return JS_FALSE; + } + + /* + * CSP check: is new Function() allowed at all? + * Report errors via CSP is done in the script security manager. + * js_CheckCSPPermitsJSAction is defined in jsobj.cpp + */ + if (!js_CheckCSPPermitsJSAction(cx)) { + ::JS_ReportError(cx, "call to new Function() blocked by CSP"); + return JS_FALSE; } n = argc ? argc - 1 : 0; diff --git a/js/src/jsobj.cpp b/js/src/jsobj.cpp --- a/js/src/jsobj.cpp +++ b/js/src/jsobj.cpp @@ -1123,6 +1123,25 @@ Object_p_valueOf(JSContext* cx, JSObject #endif /* + * Check if CSP allows new Function() or eval() to run in the current + * principals. + */ +JSBool +js_CheckCSPPermitsJSAction(JSContext *cx) +{ + JSSecurityCallbacks *callbacks; + callbacks = JS_GetSecurityCallbacks(cx); + + // if there are callbacks, make sure that the CSP callback is installed and + // that it permits eval(). + if (callbacks) + return callbacks->contentSecurityPolicyAllows && + callbacks->contentSecurityPolicyAllows(cx); + + return JS_TRUE; +} + +/* * Check whether principals subsumes scopeobj's principals, and return true * if so (or if scopeobj has no principals, for backward compatibility with * the JS API, which does not require principals), and false otherwise. @@ -1316,6 +1335,13 @@ obj_eval(JSContext *cx, JSObject *obj, u argv[1] = OBJECT_TO_JSVAL(scopeobj); } + // CSP check: is eval() allowed at all? + // report errors via CSP is done in the script security mgr. + if (!js_CheckCSPPermitsJSAction(cx)) { + JS_ReportError(cx, "call to eval() blocked by CSP"); + return JS_FALSE; + } + /* From here on, control must exit through label out with ok set. */ MUST_FLOW_THROUGH("out"); uintN staticLevel = caller->script->staticLevel + 1; @@ -1366,7 +1392,6 @@ obj_eval(JSContext *cx, JSObject *obj, u } } #endif - /* * Compile using caller's current scope object. * diff --git a/js/src/jsobj.h b/js/src/jsobj.h --- a/js/src/jsobj.h +++ b/js/src/jsobj.h @@ -967,6 +967,10 @@ js_CheckPrincipalsAccess(JSContext *cx, js_CheckPrincipalsAccess(JSContext *cx, JSObject *scopeobj, JSPrincipals *principals, JSAtom *caller); +/* For CSP -- checks if eval() and friends are allowed to run. */ +extern JSBool +js_CheckCSPPermitsJSAction(JSContext *cx); + /* Infallible -- returns its argument if there is no wrapped object. */ extern JSObject * js_GetWrappedObject(JSContext *cx, JSObject *obj); diff --git a/js/src/jspubtd.h b/js/src/jspubtd.h --- a/js/src/jspubtd.h +++ b/js/src/jspubtd.h @@ -579,6 +579,13 @@ typedef JSPrincipals * typedef JSPrincipals * (* JSObjectPrincipalsFinder)(JSContext *cx, JSObject *obj); +/* + * Used to check if a CSP instance wants to disable eval() and friends. + * See js_CheckCSPPermitsJSAction() in jsobj. + */ +typedef JSBool +(* JSCSPEvalChecker)(JSContext *cx); + JS_END_EXTERN_C #endif /* jspubtd_h___ */ diff --git a/layout/build/nsLayoutModule.cpp b/layout/build/nsLayoutModule.cpp --- a/layout/build/nsLayoutModule.cpp +++ b/layout/build/nsLayoutModule.cpp @@ -110,6 +110,7 @@ #include "nsDOMParser.h" #include "nsDOMSerializer.h" #include "nsXMLHttpRequest.h" +#include "nsChannelPolicy.h" // view stuff #include "nsViewsCID.h" @@ -270,6 +271,7 @@ static void Shutdown(); #endif #include "nsGeolocation.h" +#include "nsCSPService.h" // Transformiix /* {0C351177-0159-4500-86B0-A219DFDE4258} */ @@ -295,6 +297,7 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsDOMPars NS_GENERIC_FACTORY_CONSTRUCTOR(nsDOMParser) NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsDOMStorageManager, nsDOMStorageManager::GetInstance) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsChannelPolicy) //----------------------------------------------------------------------------- @@ -847,6 +850,76 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsBaseDOM NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsGeolocationService, nsGeolocationService::GetGeolocationService) +static NS_METHOD +CSPServiceRegistration(nsIComponentManager *aCompMgr, + nsIFile *aPath, + const char *registryLocation, + const char *componentType, + const nsModuleComponentInfo *info) +{ + nsresult rv; + nsCOMPtr servman = do_QueryInterface((nsISupports*)aCompMgr, &rv); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr catman; + rv = servman->GetServiceByContractID(NS_CATEGORYMANAGER_CONTRACTID, + NS_GET_IID(nsICategoryManager), + getter_AddRefs(catman)); + if (NS_FAILED(rv)) + return rv; + + nsXPIDLCString previous; + + rv = catman->AddCategoryEntry("content-policy", + "CSPService", + CSPSERVICE_CONTRACTID, + PR_TRUE, + PR_TRUE, + getter_Copies(previous)); + + rv = catman->AddCategoryEntry("net-channel-event-sinks", + "CSPService", + CSPSERVICE_CONTRACTID, + PR_TRUE, + PR_TRUE, + getter_Copies(previous)); + + return rv; +} + +static NS_METHOD +CSPServiceUnregistration(nsIComponentManager *aCompMgr, + nsIFile *aPath, + const char *registryLocation, + const nsModuleComponentInfo *info){ + nsresult rv; + + nsCOMPtr servman = do_QueryInterface((nsISupports*)aCompMgr, &rv); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr catman; + rv = servman->GetServiceByContractID(NS_CATEGORYMANAGER_CONTRACTID, + NS_GET_IID(nsICategoryManager), + getter_AddRefs(catman)); + if (NS_FAILED(rv)) + return rv; + + rv = catman->DeleteCategoryEntry("content-policy", + "CSPService", + PR_TRUE); + + rv = catman->DeleteCategoryEntry("net-channel-event-sinks", + "CSPService", + PR_TRUE); + return rv; +} + +NS_GENERIC_FACTORY_CONSTRUCTOR(CSPService) + + + // The list of components we register static const nsModuleComponentInfo gComponents[] = { #ifdef DEBUG @@ -1456,7 +1529,20 @@ static const nsModuleComponentInfo gComp { "Event Listener Service", NS_EVENTLISTENERSERVICE_CID, NS_EVENTLISTENERSERVICE_CONTRACTID, - CreateEventListenerService } + CreateEventListenerService }, + + + { "Content Security Policy Service", + CSPSERVICE_CID, + CSPSERVICE_CONTRACTID, + CSPServiceConstructor, + CSPServiceRegistration, + CSPServiceUnregistration }, + + { "Channel Policy", + NSCHANNELPOLICY_CID, + NSCHANNELPOLICY_CONTRACTID, + nsChannelPolicyConstructor } }; NS_IMPL_NSGETMODULE_WITH_CTOR(nsLayoutModule, gComponents, Initialize) diff --git a/layout/generic/nsImageFrame.cpp b/layout/generic/nsImageFrame.cpp --- a/layout/generic/nsImageFrame.cpp +++ b/layout/generic/nsImageFrame.cpp @@ -1661,6 +1661,7 @@ nsImageFrame::LoadIcon(const nsAString& loadFlags, nsnull, nsnull, + nsnull, /* channel policy not needed */ aRequest); } diff --git a/modules/libpr0n/public/imgILoader.idl b/modules/libpr0n/public/imgILoader.idl --- a/modules/libpr0n/public/imgILoader.idl +++ b/modules/libpr0n/public/imgILoader.idl @@ -48,6 +48,7 @@ interface nsIURI; interface nsIURI; interface nsISimpleEnumerator; +interface nsIChannelPolicy; #include "nsIRequest.idl" // for nsLoadFlags @@ -89,7 +90,8 @@ interface imgILoader : nsISupports in nsISupports aCX, in nsLoadFlags aLoadFlags, in nsISupports cacheKey, - in imgIRequest aRequest); + in imgIRequest aRequest, + in nsIChannelPolicy channelPolicy); /** * Start the load and decode of an image. diff --git a/modules/libpr0n/src/imgLoader.cpp b/modules/libpr0n/src/imgLoader.cpp --- a/modules/libpr0n/src/imgLoader.cpp +++ b/modules/libpr0n/src/imgLoader.cpp @@ -74,6 +74,8 @@ // so we can associate the document URI with the load group. // until this point, we have an evil hack: #include "nsIHttpChannelInternal.h" +#include "IContentSecurityPolicy.h" +#include "nsIChannelPolicy.h" #if defined(DEBUG_pavlov) || defined(DEBUG_timeless) #include "nsISimpleEnumerator.h" @@ -291,7 +293,8 @@ static nsresult NewImageChannel(nsIChann nsIURI *aReferringURI, nsILoadGroup *aLoadGroup, const nsCString& aAcceptHeader, - nsLoadFlags aLoadFlags) + nsLoadFlags aLoadFlags, + nsIChannelPolicy *channelPolicy) { nsresult rv; nsCOMPtr newChannel; @@ -316,12 +319,13 @@ static nsresult NewImageChannel(nsIChann // If all of the proxy requests are canceled then this request should be // canceled too. // - rv = NS_NewChannel(aResult, - aURI, // URI - nsnull, // Cached IOService - nsnull, // LoadGroup - callbacks, // Notification Callbacks - aLoadFlags); + rv = NS_NewChannelWithPolicy(aResult, + aURI, // URI + nsnull, // Cached IOService + nsnull, // LoadGroup + callbacks, // Notification Callbacks + aLoadFlags, + channelPolicy); if (NS_FAILED(rv)) return rv; @@ -978,7 +982,8 @@ PRBool imgLoader::ValidateRequestWithNew nsISupports *aCX, nsLoadFlags aLoadFlags, imgIRequest *aExistingRequest, - imgIRequest **aProxyRequest) + imgIRequest **aProxyRequest, + nsIChannelPolicy *policy) { // now we need to insert a new channel request object inbetween the real // request and the proxy that basically delays loading the image until it @@ -1006,7 +1011,8 @@ PRBool imgLoader::ValidateRequestWithNew aReferrerURI, aLoadGroup, mAcceptHeader, - aLoadFlags); + aLoadFlags, + policy); if (NS_FAILED(rv)) { return PR_FALSE; } @@ -1066,7 +1072,8 @@ PRBool imgLoader::ValidateEntry(imgCache nsLoadFlags aLoadFlags, PRBool aCanMakeNewChannel, imgIRequest *aExistingRequest, - imgIRequest **aProxyRequest) + imgIRequest **aProxyRequest, + nsIChannelPolicy *policy = nsnull) { LOG_SCOPE(gImgLog, "imgLoader::ValidateEntry"); @@ -1175,7 +1182,7 @@ PRBool imgLoader::ValidateEntry(imgCache return ValidateRequestWithNewChannel(request, aURI, aInitialDocumentURI, aReferrerURI, aLoadGroup, aObserver, aCX, aLoadFlags, aExistingRequest, - aProxyRequest); + aProxyRequest, policy); } return !validateRequest; @@ -1316,6 +1323,7 @@ NS_IMETHODIMP imgLoader::LoadImage(nsIUR nsLoadFlags aLoadFlags, nsISupports *aCacheKey, imgIRequest *aRequest, + nsIChannelPolicy *channelPolicy, imgIRequest **_retval) { VerifyCacheSizes(); @@ -1381,8 +1389,9 @@ NS_IMETHODIMP imgLoader::LoadImage(nsIUR aURI->GetSpec(spec); if (cache.Get(spec, getter_AddRefs(entry)) && entry) { - if (ValidateEntry(entry, aURI, aInitialDocumentURI, aReferrerURI, aLoadGroup, aObserver, aCX, - requestFlags, PR_TRUE, aRequest, _retval)) { + if (ValidateEntry(entry, aURI, aInitialDocumentURI, aReferrerURI, + aLoadGroup, aObserver, aCX, requestFlags, PR_TRUE, + aRequest, _retval, channelPolicy)) { request = getter_AddRefs(entry->GetRequest()); // If this entry has no proxies, its request has no reference to the entry. @@ -1419,7 +1428,8 @@ NS_IMETHODIMP imgLoader::LoadImage(nsIUR aReferrerURI, aLoadGroup, mAcceptHeader, - requestFlags); + requestFlags, + channelPolicy); if (NS_FAILED(rv)) return NS_ERROR_FAILURE; diff --git a/modules/libpr0n/src/imgLoader.h b/modules/libpr0n/src/imgLoader.h --- a/modules/libpr0n/src/imgLoader.h +++ b/modules/libpr0n/src/imgLoader.h @@ -48,6 +48,7 @@ #include "prtypes.h" #include "imgRequest.h" #include "nsIObserverService.h" +#include "nsIChannelPolicy.h" #ifdef LOADER_THREADSAFE #include "prlock.h" @@ -300,7 +301,8 @@ private: // methods imgIDecoderObserver *aObserver, nsISupports *aCX, nsLoadFlags aLoadFlags, PRBool aCanMakeNewChannel, imgIRequest *aExistingRequest, - imgIRequest **aProxyRequest); + imgIRequest **aProxyRequest, + nsIChannelPolicy *channelPolicy); PRBool ValidateRequestWithNewChannel(imgRequest *request, nsIURI *aURI, nsIURI *aInitialDocumentURI, nsIURI *aReferrerURI, @@ -308,7 +310,8 @@ private: // methods imgIDecoderObserver *aObserver, nsISupports *aCX, nsLoadFlags aLoadFlags, imgIRequest *aExistingRequest, - imgIRequest **aProxyRequest); + imgIRequest **aProxyRequest, + nsIChannelPolicy *channelPolicy); nsresult CreateNewProxyForRequest(imgRequest *aRequest, nsILoadGroup *aLoadGroup, imgIDecoderObserver *aObserver, 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 @@ -926,6 +926,8 @@ pref("security.xpconnect.plugin.unrestri pref("security.xpconnect.plugin.unrestricted", true); // security-sensitive dialogs should delay button enabling. In milliseconds. pref("security.dialog_enable_delay", 2000); + +pref("security.csp.enable", true); // Modifier key prefs: default to Windows settings, // menu access key = alt, accelerator key = control. diff --git a/netwerk/base/public/Makefile.in b/netwerk/base/public/Makefile.in --- a/netwerk/base/public/Makefile.in +++ b/netwerk/base/public/Makefile.in @@ -74,6 +74,7 @@ XPIDLSRCS = \ nsIAsyncStreamCopier.idl \ nsIBufferedStreams.idl \ nsICancelable.idl \ + nsIChannelPolicy.idl \ nsICryptoHash.idl \ nsICryptoHMAC.idl \ nsIDownloader.idl \ diff --git a/netwerk/base/public/nsIChannelPolicy.idl b/netwerk/base/public/nsIChannelPolicy.idl new file mode 100644 --- /dev/null +++ b/netwerk/base/public/nsIChannelPolicy.idl @@ -0,0 +1,47 @@ +/* ***** 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 channel policy container code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * + * Contributor(s): + * Brandon Sterne + * + * 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 ***** */ + +/* A container for policy information to be used during channel creation */ + +#include "nsISupports.idl" + +interface IContentSecurityPolicy; + +[scriptable, uuid(18045e96-1afe-4162-837a-04691267158c)] +interface nsIChannelPolicy : nsISupports +{ + attribute unsigned long loadType; + attribute IContentSecurityPolicy contentSecurityPolicy; +}; diff --git a/netwerk/base/public/nsNetUtil.h b/netwerk/base/public/nsNetUtil.h --- a/netwerk/base/public/nsNetUtil.h +++ b/netwerk/base/public/nsNetUtil.h @@ -96,6 +96,7 @@ #include "nsINestedURI.h" #include "nsIMutable.h" #include "nsIPropertyBag2.h" +#include "nsIWritablePropertyBag2.h" #include "nsIIDNService.h" #include "nsIChannelEventSink.h" @@ -186,6 +187,44 @@ NS_NewChannel(nsIChannel **res rv |= chan->SetNotificationCallbacks(callbacks); if (loadFlags != nsIRequest::LOAD_NORMAL) rv |= chan->SetLoadFlags(loadFlags); + if (NS_SUCCEEDED(rv)) + chan.forget(result); + } + } + return rv; +} + +// Create a new channel with load type and Content Security Policy added +// to the property bag to be used during channel redirects +inline nsresult +NS_NewChannelWithPolicy(nsIChannel **result, + nsIURI *uri, + nsIIOService *ioService = nsnull, + nsILoadGroup *loadGroup = nsnull, + nsIInterfaceRequestor *callbacks = nsnull, + PRUint32 loadFlags = nsIRequest::LOAD_NORMAL, + nsISupports *channelPolicy = nsnull) +{ + nsresult rv; + nsCOMPtr grip; + rv = net_EnsureIOService(&ioService, grip); + if (ioService) { + nsCOMPtr chan; + rv = ioService->NewChannelFromURI(uri, getter_AddRefs(chan)); + if (NS_SUCCEEDED(rv)) { + if (loadGroup) + rv |= chan->SetLoadGroup(loadGroup); + if (callbacks) + rv |= chan->SetNotificationCallbacks(callbacks); + if (loadFlags != nsIRequest::LOAD_NORMAL) + rv |= chan->SetLoadFlags(loadFlags); + if (channelPolicy) { + nsCOMPtr props = do_QueryInterface(chan, &rv); + if (props) { + props->SetPropertyAsInterface(NS_LITERAL_STRING("csp.channelPolicy"), + channelPolicy); + } + } if (NS_SUCCEEDED(rv)) chan.forget(result); } diff --git a/toolkit/system/gnome/nsAlertsIconListener.cpp b/toolkit/system/gnome/nsAlertsIconListener.cpp --- a/toolkit/system/gnome/nsAlertsIconListener.cpp +++ b/toolkit/system/gnome/nsAlertsIconListener.cpp @@ -242,9 +242,11 @@ nsAlertsIconListener::StartRequest(const if (!il) return ShowAlert(NULL); + // XXXbsterne currently passing in null for channel policy + // I don't have a testcase that triggers this code return il->LoadImage(imageUri, nsnull, nsnull, nsnull, this, nsnull, nsIRequest::LOAD_NORMAL, nsnull, nsnull, - getter_AddRefs(mIconRequest)); + nsnull, getter_AddRefs(mIconRequest)); } void diff --git a/widget/src/cocoa/nsMenuItemIconX.mm b/widget/src/cocoa/nsMenuItemIconX.mm --- a/widget/src/cocoa/nsMenuItemIconX.mm +++ b/widget/src/cocoa/nsMenuItemIconX.mm @@ -62,6 +62,9 @@ #include "nsMenuItemX.h" #include "gfxImageSurface.h" #include "imgIContainer.h" +#include "nsIPrincipal.h" +#include "IContentSecurityPolicy.h" +#include "nsIChannelPolicy.h" static const PRUint32 kIconWidth = 16; static const PRUint32 kIconHeight = 16; @@ -244,6 +247,20 @@ nsMenuItemIconX::LoadIcon(nsIURI* aIconU &rv); if (NS_FAILED(rv)) return rv; + // get Content Security Policy to pass to LoadImage + nsCOMPtr principal; + nsCOMPtr channelPolicy; + nsCOMPtr csp; + if (document) { + principal = document->NodePrincipal(); + rv = principal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + if (csp) { + channelPolicy->SetContentSecurityPolicy(csp); + channelPolicy->SetLoadType(3); // TYPE_IMAGE + } + } + if (!mSetIcon) { // Set a completely transparent 16x16 image as the icon on this menu item // as a placeholder. This keeps the menu item text displayed in the same @@ -265,9 +282,13 @@ nsMenuItemIconX::LoadIcon(nsIURI* aIconU [mNativeMenuItem setImage:sPlaceholderIconImage]; } + // XXXbsterne I'm not sure passing in channelPolicy is actually necessary + // here. I can trigger this code, for example, by clicking on menu items + // but nsMenuItemIconX::LoadIcon always seems to be called with either + // chrome:// or moz-anno:favicon URIs. rv = loader->LoadImage(aIconURI, nsnull, nsnull, loadGroup, this, - nsnull, nsIRequest::LOAD_NORMAL, nsnull, - nsnull, getter_AddRefs(mIconRequest)); + nsnull, nsIRequest::LOAD_NORMAL, nsnull, nsnull, + channelPolicy, getter_AddRefs(mIconRequest)); if (NS_FAILED(rv)) return rv; return NS_OK;