diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index fce0e766087d..68728bd45479 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -326,6 +326,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 index 127f8949ca5f..f1c78641acb7 100644 --- a/caps/idl/nsIPrincipal.idl +++ b/caps/idl/nsIPrincipal.idl @@ -47,11 +47,12 @@ struct JSPrincipals; %} interface nsIURI; +interface IContentSecurityPolicy; [ptr] native JSContext(JSContext); [ptr] native JSPrincipals(JSPrincipals); -[scriptable, uuid(b8268b9a-2403-44ed-81e3-614075c92034)] +[scriptable, uuid(799ab95c-0038-4e0f-b705-74c21f185bb5)] interface nsIPrincipal : nsISerializable { /** @@ -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 index 1a933163efd2..b9ff39127275 100644 --- 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/src/nsNullPrincipal.cpp b/caps/src/nsNullPrincipal.cpp index 63171280293f..ad770e2a93c3 100644 --- a/caps/src/nsNullPrincipal.cpp +++ b/caps/src/nsNullPrincipal.cpp @@ -251,6 +251,21 @@ nsNullPrincipal::GetURI(nsIURI** aURI) return NS_EnsureSafeToReturn(mURI, aURI); } +NS_IMETHODIMP +nsNullPrincipal::GetCsp(IContentSecurityPolicy** aCsp) +{ + // CSP on a null principal makes no sense + *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) { diff --git a/caps/src/nsPrincipal.cpp b/caps/src/nsPrincipal.cpp index e6124df9d86c..5a8bbd18e23c 100644 --- 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" @@ -774,6 +775,25 @@ nsPrincipal::GetCertificate(nsISupports** aCertificate) return NS_OK; } +NS_IMETHODIMP +nsPrincipal::GetCsp(IContentSecurityPolicy** aCsp) +{ + NS_IF_ADDREF(*aCsp = mCSP); + return NS_OK; +} + +NS_IMETHODIMP +nsPrincipal::SetCsp(IContentSecurityPolicy* aCsp) +{ + // If CSP was already set, it should not be destroyed! Instead, it should + // get set anew when a new principal is created. + if (mCSP) + return NS_ERROR_ALREADY_INITIALIZED; + + mCSP = aCsp; + return NS_OK; +} + NS_IMETHODIMP nsPrincipal::GetHashValue(PRUint32* aValue) { diff --git a/caps/src/nsSystemPrincipal.cpp b/caps/src/nsSystemPrincipal.cpp index 7673e85467c2..c2e0aa2cf549 100644 --- a/caps/src/nsSystemPrincipal.cpp +++ b/caps/src/nsSystemPrincipal.cpp @@ -223,6 +223,20 @@ nsSystemPrincipal::GetHasCertificate(PRBool* aResult) return NS_OK; } +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) { diff --git a/content/base/public/IContentSecurityPolicy.idl b/content/base/public/IContentSecurityPolicy.idl new file mode 100644 index 000000000000..df348fd97f24 --- /dev/null +++ b/content/base/public/IContentSecurityPolicy.idl @@ -0,0 +1,152 @@ +/* ***** 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 index cb90f36fdf24..772360551461 100644 --- a/content/base/public/Makefile.in +++ b/content/base/public/Makefile.in @@ -113,6 +113,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 index 000000000000..1a39f0f2391f --- /dev/null +++ b/content/base/src/CSPUtils.jsm @@ -0,0 +1,1295 @@ +/* ***** 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.trim(); + var dirname = dir.split(/\s+/)[0]; + var dirvalue = dir.substring(dirname.length).trim(); + + // 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],null,null); + 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; + + req.send(null); + if (req.status == 200) { + 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 a space-separated list 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]); + } + + // REPORT_URI + var reportURIDir = CSPRep.URI_DIRECTIVES.REPORT_URI; + if (this._directives[reportURIDir] && aCSPRep._directives[reportURIDir]) { + newRep._directives[reportURIDir] = + this._directives[reportURIDir].concat(aCSPRep._directives[reportURIDir]); + } + else if (this._directives[reportURIDir]) { + // blank concat makes a copy of the string. + newRep._directives[reportURIDir] = this._directives[reportURIDir].concat(); + } + else if (aCSPRep._directives[reportURIDir]) { + // blank concat makes a copy of the string. + newRep._directives[reportURIDir] = aCSPRep._directives[reportURIDir].concat(); + } + + 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): we should figure out a better algorithm for this. + // This is horribly inefficient. + 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 ns; + } + + 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 { + // port is never inherited from self -- this gets too confusing. + // Instead, whatever scheme is used (an explicit one or the inherited + // one) dictates the port if no port is explicitly stated. + if (sObj._scheme) { + 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; + } + + // 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. + */ +{ + let cp = Ci.nsIContentPolicy; + let csp = ContentSecurityPolicy; + let cspr_sd = CSPRep.SRC_DIRECTIVES; + + csp._MAPPINGS=[]; + + /* default, catch-all case */ + csp._MAPPINGS[cp.TYPE_OTHER] = cspr_sd.ALLOW; + + /* self */ + csp._MAPPINGS[cp.TYPE_DOCUMENT] = null; + + /* shouldn't see this one */ + csp._MAPPINGS[cp.TYPE_REFRESH] = null; + + /* categorized content types */ + csp._MAPPINGS[cp.TYPE_SCRIPT] = cspr_sd.SCRIPT_SRC; + csp._MAPPINGS[cp.TYPE_IMAGE] = cspr_sd.IMG_SRC; + csp._MAPPINGS[cp.TYPE_STYLESHEET] = cspr_sd.STYLE_SRC; + csp._MAPPINGS[cp.TYPE_OBJECT] = cspr_sd.OBJECT_SRC; + csp._MAPPINGS[cp.TYPE_SUBDOCUMENT] = cspr_sd.FRAME_SRC; + csp._MAPPINGS[cp.TYPE_MEDIA] = cspr_sd.MEDIA_SRC; + csp._MAPPINGS[cp.TYPE_FONT] = cspr_sd.FONT_SRC; + csp._MAPPINGS[cp.TYPE_XMLHTTPREQUEST] = cspr_sd.XHR_SRC; + + + /* These must go through the catch-all */ + csp._MAPPINGS[cp.TYPE_XBL] = cspr_sd.ALLOW; + csp._MAPPINGS[cp.TYPE_PING] = cspr_sd.ALLOW; + csp._MAPPINGS[cp.TYPE_OBJECT_SUBREQUEST] = cspr_sd.ALLOW; + csp._MAPPINGS[cp.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.asciiSpec + + " 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); + + // (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 uriString = this._policy.getReportURIs(); + var uris = uriString.split(/\s+/); + 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" + + " " + + (typeof blockedUri === "nsIURI" ? blockedUri.asciiSpec : 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) { + if (uris[i] === "") + continue; + + 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); + + try { + 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]); + } catch(e) { + // it's possible that the URI was invalid, just log a + // warning and skip over that. + CSPWarning("Tried to send report to invalid URI: \"" + 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 index 000000000000..813819ba526f --- /dev/null +++ b/content/base/src/nsCSPService.cpp @@ -0,0 +1,214 @@ +/* -*- 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 "nsIURI.h" +#include "nsIPrincipal.h" +#include "nsIObserver.h" +#include "nsIDocument.h" +#include "nsIContent.h" +#include "nsContentUtils.h" +#include "nsCSPService.h" +#include "IContentSecurityPolicy.h" + +/* Keeps track of whether or not CSP is enabled */ +static PRBool gCSPEnabled = PR_TRUE; + +#ifdef PR_LOGGING +static PRLogModuleInfo* gCspPRLog; +#endif + +CSPService::CSPService() +{ + nsContentUtils::AddBoolPrefVarCache("security.csp.enable", &gCSPEnabled); + +#ifdef PR_LOGGING + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); +#endif +} + +CSPService::~CSPService() +{ +} + +NS_IMPL_ISUPPORTS1(CSPService, nsIContentPolicy) + +/* 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; + 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(); + principal->GetCsp(getter_AddRefs(csp)); + + 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; + 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(); + principal->GetCsp(getter_AddRefs(csp)); + + 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; + aContentLocation->GetSpec(uriSpec); + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("COULD NOT get nsIDocument for location: %s", uriSpec.get())); + } +#endif + return NS_OK; +} diff --git a/content/base/src/nsCSPService.h b/content/base/src/nsCSPService.h new file mode 100644 index 000000000000..a1e056164cd1 --- /dev/null +++ b/content/base/src/nsCSPService.h @@ -0,0 +1,57 @@ +/* -*- 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 "nsXPCOM.h" +#include "nsIContentPolicy.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: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPOLICY + + CSPService(); + virtual ~CSPService(); + +private: + PRBool mEnabled; +}; diff --git a/content/base/src/nsDocument.cpp b/content/base/src/nsDocument.cpp index 8fd3149d78ff..2f20b1635db0 100644 --- a/content/base/src/nsDocument.cpp +++ b/content/base/src/nsDocument.cpp @@ -181,6 +181,9 @@ static NS_DEFINE_CID(kDOMEventGroupCID, NS_DOMEVENTGROUP_CID); #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 @@ -188,8 +191,12 @@ static NS_DEFINE_CID(kDOMEventGroupCID, NS_DOMEVENTGROUP_CID); #endif #include "prlog.h" +/* Keeps track of whether or not CSP is enabled */ +static PRBool gCSPEnabled = PR_TRUE; + #ifdef PR_LOGGING static PRLogModuleInfo* gDocumentLeakPRLog; +static PRLogModuleInfo* gCspPRLog; #endif void @@ -1495,8 +1502,13 @@ nsDocument::nsDocument(const char* aContentType) if (gDocumentLeakPRLog) PR_LOG(gDocumentLeakPRLog, PR_LOG_DEBUG, ("DOCUMENT %p created", this)); + + if (!gCspPRLog) + gCspPRLog = PR_NewLogModule("CSP"); #endif + nsContentUtils::AddBoolPrefVarCache("security.csp.enable", &gCSPEnabled); + // Start out mLastStyleSheetSet as null, per spec SetDOMStringToNull(mLastStyleSheetSet); } @@ -2251,10 +2263,114 @@ nsDocument::StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, RetrieveRelevantHeaders(aChannel); mChannel = aChannel; + + nsresult rv = InitCSP(); + NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } +nsresult +nsDocument::InitCSP() +{ + if (gCSPEnabled) { + nsAutoString cspHeaderValue; + nsAutoString cspROHeaderValue; + + this->GetHeaderData(nsGkAtoms::headerCSP, cspHeaderValue); + this->GetHeaderData(nsGkAtoms::headerCSPReportOnly, cspROHeaderValue); + + PRBool system = PR_FALSE; + nsIScriptSecurityManager *ssm = nsContentUtils::GetSecurityManager(); + + 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, stop processing + return NS_OK; + } + +#ifdef PR_LOGGING + PR_LOG(gCspPRLog, PR_LOG_DEBUG, ("CSP header specified for document %p", this)); +#endif + + nsresult rv; + 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: %x", rv)); +#endif + return rv; + } + + // Store the request context for violation reports + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + mCSP->ScanRequestData(httpChannel); + + // Start parsing the policy + nsCOMPtr chanURI; + mChannel->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\"", + NS_ConvertUTF16toUTF8(cspROHeaderValue).get())); + } +#endif + } else { + //XXX(sstamm): maybe we should post a warning when both read only and regular + // CSP headers are present. + mCSP->RefinePolicy(cspHeaderValue, chanURI); +#ifdef PR_LOGGING + { + PR_LOG(gCspPRLog, PR_LOG_DEBUG, + ("CSP refined, policy: \"%s\"", + NS_ConvertUTF16toUTF8(cspHeaderValue).get())); + } +#endif + } + + //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 + return NS_OK; +} + void nsDocument::StopDocumentLoad() { @@ -6642,6 +6758,8 @@ nsDocument::RetrieveRelevantHeaders(nsIChannel *aChannel) "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. diff --git a/content/base/src/nsDocument.h b/content/base/src/nsDocument.h index 3a829c2d6282..4ad6ccd0a15b 100644 --- a/content/base/src/nsDocument.h +++ b/content/base/src/nsDocument.h @@ -1211,6 +1211,8 @@ private: void PostUnblockOnloadEvent(); void DoUnblockOnload(); + nsresult InitCSP(); + /** * See if aDocument is a child of this. If so, return the frame element in * this document that holds currentDoc (or an ancestor). diff --git a/content/base/src/nsGkAtomList.h b/content/base/src/nsGkAtomList.h index 8cd6af9acc30..cc800ee4a12a 100644 --- a/content/base/src/nsGkAtomList.h +++ b/content/base/src/nsGkAtomList.h @@ -1007,6 +1007,8 @@ GK_ATOM(withParam, "with-param") GK_ATOM(wizard, "wizard") GK_ATOM(wrap, "wrap") GK_ATOM(headerDNSPrefetchControl,"x-dns-prefetch-control") +GK_ATOM(headerCSP, "x-content-security-policy") +GK_ATOM(headerCSPReportOnly, "x-content-security-policy-report-only") GK_ATOM(xml, "xml") GK_ATOM(xmlns, "xmlns") GK_ATOM(xmp, "xmp") diff --git a/content/base/test/Makefile.in b/content/base/test/Makefile.in index fa81473572a1..6f8b2cf6222e 100644 --- a/content/base/test/Makefile.in +++ b/content/base/test/Makefile.in @@ -333,6 +333,10 @@ _TEST_FILES = test_bug5141.html \ test_bug503481b.html \ file_bug503481b_inner.html \ test_viewport_scroll.html \ + test_CSP.html \ + file_CSP.sjs \ + file_CSP_main.html \ + file_CSP_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 index 000000000000..b31158ca853a --- /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_main.html b/content/base/test/file_CSP_main.html new file mode 100644 index 000000000000..1b86486bc2a8 --- /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 index 000000000000..f2d12ff85e8d --- /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 index 000000000000..3875e387fa1e --- /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/unit/test_csputils.js b/content/base/test/unit/test_csputils.js new file mode 100644 index 000000000000..a7a46807c914 --- /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/layout/build/nsLayoutModule.cpp b/layout/build/nsLayoutModule.cpp index 52be6f8fd3f5..dbe1f0be1edc 100644 --- a/layout/build/nsLayoutModule.cpp +++ b/layout/build/nsLayoutModule.cpp @@ -268,6 +268,7 @@ static void Shutdown(); #endif #include "nsGeolocation.h" +#include "nsCSPService.h" // Transformiix /* {0C351177-0159-4500-86B0-A219DFDE4258} */ @@ -848,6 +849,60 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsGeolocation, Init) 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)); + 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); + + return rv; +} + +NS_GENERIC_FACTORY_CONSTRUCTOR(CSPService) + // The list of components we register static const nsModuleComponentInfo gComponents[] = { #ifdef DEBUG @@ -1453,6 +1508,12 @@ static const nsModuleComponentInfo gComponents[] = { "@mozilla.org/focus-manager;1", CreateFocusManager }, + { "Content Security Policy Service", + CSPSERVICE_CID, + CSPSERVICE_CONTRACTID, + CSPServiceConstructor, + CSPServiceRegistration, + CSPServiceUnregistration }, { "Event Listener Service", NS_EVENTLISTENERSERVICE_CID, diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 04b27863a34c..a704c5d88fb3 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -950,6 +950,8 @@ 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. // Use 17 for Ctrl, 18 for Alt, 224 for Meta, 0 for none. Mac settings in macprefs.js