michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Content Security Policy Utilities michael@0: * michael@0: * Overview michael@0: * This contains a set of classes and utilities for CSP. It is in this michael@0: * separate file for testing purposes. michael@0: */ michael@0: michael@0: const Cu = Components.utils; michael@0: const Ci = Components.interfaces; michael@0: michael@0: const WARN_FLAG = Ci.nsIScriptError.warningFlag; michael@0: const ERROR_FLAG = Ci.nsIScriptError.ERROR_FLAG; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: michael@0: // Module stuff michael@0: this.EXPORTED_SYMBOLS = ["CSPRep", "CSPSourceList", "CSPSource", "CSPHost", michael@0: "CSPdebug", "CSPViolationReportListener", "CSPLocalizer", michael@0: "CSPPrefObserver"]; michael@0: michael@0: var STRINGS_URI = "chrome://global/locale/security/csp.properties"; michael@0: michael@0: // these are not exported michael@0: var gIoService = Components.classes["@mozilla.org/network/io-service;1"] michael@0: .getService(Ci.nsIIOService); michael@0: michael@0: var gETLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"] michael@0: .getService(Ci.nsIEffectiveTLDService); michael@0: michael@0: // These regexps represent the concrete syntax on the w3 spec as of 7-5-2012 michael@0: // scheme = michael@0: const R_SCHEME = new RegExp ("([a-zA-Z0-9\\-]+)", 'i'); michael@0: const R_GETSCHEME = new RegExp ("^" + R_SCHEME.source + "(?=\\:)", 'i'); michael@0: michael@0: // scheme-source = scheme ":" michael@0: const R_SCHEMESRC = new RegExp ("^" + R_SCHEME.source + "\\:$", 'i'); michael@0: michael@0: // host-char = ALPHA / DIGIT / "-" michael@0: // For the app: protocol, we need to add {} to the valid character set michael@0: const HOSTCHAR = "{}a-zA-Z0-9\\-"; michael@0: const R_HOSTCHAR = new RegExp ("[" + HOSTCHAR + "]", 'i'); michael@0: michael@0: // Complementary character set of HOSTCHAR (characters that can't appear) michael@0: const R_COMP_HCHAR = new RegExp ("[^" + HOSTCHAR + "]", "i"); michael@0: michael@0: // Invalid character set for host strings (which can include dots and star) michael@0: const R_INV_HCHAR = new RegExp ("[^" + HOSTCHAR + "\\.\\*]", 'i'); michael@0: michael@0: michael@0: // host = "*" / [ "*." ] 1*host-char *( "." 1*host-char ) michael@0: const R_HOST = new RegExp ("\\*|(((\\*\\.)?" + R_HOSTCHAR.source + michael@0: "+)" + "(\\." + R_HOSTCHAR.source + "+)*)", 'i'); michael@0: michael@0: // port = ":" ( 1*DIGIT / "*" ) michael@0: const R_PORT = new RegExp ("(\\:([0-9]+|\\*))", 'i'); michael@0: michael@0: // host-source = [ scheme "://" ] host [ port path file ] michael@0: const R_HOSTSRC = new RegExp ("^((" + R_SCHEME.source + "\\:\\/\\/)?(" michael@0: + R_HOST.source + ")" michael@0: + R_PORT.source + "?)$", 'i'); michael@0: michael@0: function STRIP_INPUTDELIM(re) { michael@0: return re.replace(/(^\^)|(\$$)/g, ""); michael@0: } michael@0: michael@0: // ext-host-source = host-source "/" *( ) michael@0: // ; ext-host-source is reserved for future use. michael@0: const R_VCHAR_EXCEPT = new RegExp("[!-+--:<-~]"); // ranges exclude , and ; michael@0: const R_EXTHOSTSRC = new RegExp ("^" + STRIP_INPUTDELIM(R_HOSTSRC.source) michael@0: + "\\/" michael@0: + R_VCHAR_EXCEPT.source + "*$", 'i'); michael@0: michael@0: // keyword-source = "'self'" / "'unsafe-inline'" / "'unsafe-eval'" michael@0: const R_KEYWORDSRC = new RegExp ("^('self'|'unsafe-inline'|'unsafe-eval')$", 'i'); michael@0: michael@0: const R_BASE64 = new RegExp ("([a-zA-Z0-9+/]+={0,2})"); michael@0: michael@0: // nonce-source = "'nonce-" nonce-value "'" michael@0: // nonce-value = 1*( ALPHA / DIGIT / "+" / "/" ) michael@0: const R_NONCESRC = new RegExp ("^'nonce-" + R_BASE64.source + "'$"); michael@0: michael@0: // hash-source = "'" hash-algo "-" hash-value "'" michael@0: // hash-algo = "sha256" / "sha384" / "sha512" michael@0: // hash-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" ) michael@0: // Each algo must be a valid argument to nsICryptoHash.init michael@0: const R_HASH_ALGOS = new RegExp ("(sha256|sha384|sha512)"); michael@0: const R_HASHSRC = new RegExp ("^'" + R_HASH_ALGOS.source + "-" + R_BASE64.source + "'$"); michael@0: michael@0: // source-exp = scheme-source / host-source / keyword-source michael@0: const R_SOURCEEXP = new RegExp (R_SCHEMESRC.source + "|" + michael@0: R_HOSTSRC.source + "|" + michael@0: R_EXTHOSTSRC.source + "|" + michael@0: R_KEYWORDSRC.source + "|" + michael@0: R_NONCESRC.source + "|" + michael@0: R_HASHSRC.source, 'i'); michael@0: michael@0: const R_QUOTELESS_KEYWORDS = new RegExp ("^(self|unsafe-inline|unsafe-eval|" + michael@0: "inline-script|eval-script|none)$", 'i'); michael@0: michael@0: this.CSPPrefObserver = { michael@0: get debugEnabled () { michael@0: if (!this._branch) michael@0: this._initialize(); michael@0: return this._debugEnabled; michael@0: }, michael@0: michael@0: get experimentalEnabled () { michael@0: if (!this._branch) michael@0: this._initialize(); michael@0: return this._experimentalEnabled; michael@0: }, michael@0: michael@0: _initialize: function() { michael@0: var prefSvc = Components.classes["@mozilla.org/preferences-service;1"] michael@0: .getService(Ci.nsIPrefService); michael@0: this._branch = prefSvc.getBranch("security.csp."); michael@0: this._branch.addObserver("", this, false); michael@0: this._debugEnabled = this._branch.getBoolPref("debug"); michael@0: this._experimentalEnabled = this._branch.getBoolPref("experimentalEnabled"); michael@0: }, michael@0: michael@0: unregister: function() { michael@0: if (!this._branch) return; michael@0: this._branch.removeObserver("", this); michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic != "nsPref:changed") return; michael@0: if (aData === "debug") michael@0: this._debugEnabled = this._branch.getBoolPref("debug"); michael@0: if (aData === "experimentalEnabled") michael@0: this._experimentalEnabled = this._branch.getBoolPref("experimentalEnabled"); michael@0: }, michael@0: }; michael@0: michael@0: this.CSPdebug = function CSPdebug(aMsg) { michael@0: if (!CSPPrefObserver.debugEnabled) return; michael@0: michael@0: aMsg = 'CSP debug: ' + aMsg + "\n"; michael@0: Components.classes["@mozilla.org/consoleservice;1"] michael@0: .getService(Ci.nsIConsoleService) michael@0: .logStringMessage(aMsg); michael@0: } michael@0: michael@0: // Callback to resume a request once the policy-uri has been fetched michael@0: function CSPPolicyURIListener(policyURI, docRequest, csp, reportOnly) { michael@0: this._policyURI = policyURI; // location of remote policy michael@0: this._docRequest = docRequest; // the parent document request michael@0: this._csp = csp; // parent document's CSP michael@0: this._policy = ""; // contents fetched from policyURI michael@0: this._wrapper = null; // nsIScriptableInputStream michael@0: this._docURI = docRequest.QueryInterface(Ci.nsIChannel) michael@0: .URI; // parent document URI (to be used as 'self') michael@0: this._reportOnly = reportOnly; michael@0: } michael@0: michael@0: CSPPolicyURIListener.prototype = { michael@0: michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Ci.nsIStreamListener) || michael@0: iid.equals(Ci.nsIRequestObserver) || michael@0: iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: onStartRequest: michael@0: function(request, context) {}, michael@0: michael@0: onDataAvailable: michael@0: function(request, context, inputStream, offset, count) { michael@0: if (this._wrapper == null) { michael@0: this._wrapper = Components.classes["@mozilla.org/scriptableinputstream;1"] michael@0: .createInstance(Ci.nsIScriptableInputStream); michael@0: this._wrapper.init(inputStream); michael@0: } michael@0: // store the remote policy as it becomes available michael@0: this._policy += this._wrapper.read(count); michael@0: }, michael@0: michael@0: onStopRequest: michael@0: function(request, context, status) { michael@0: if (Components.isSuccessCode(status)) { michael@0: // send the policy we received back to the parent document's CSP michael@0: // for parsing michael@0: this._csp.appendPolicy(this._policy, this._docURI, michael@0: this._reportOnly, this._csp._specCompliant); michael@0: } michael@0: else { michael@0: // problem fetching policy so fail closed by appending a "block it all" michael@0: // policy. Also toss an error into the console so developers can see why michael@0: // this policy is used. michael@0: this._csp.log(WARN_FLAG, CSPLocalizer.getFormatStr("errorFetchingPolicy", michael@0: [status])); michael@0: this._csp.appendPolicy("default-src 'none'", this._docURI, michael@0: this._reportOnly, this._csp._specCompliant); michael@0: } michael@0: // resume the parent document request michael@0: this._docRequest.resume(); michael@0: } michael@0: }; michael@0: michael@0: //:::::::::::::::::::::::: CLASSES ::::::::::::::::::::::::::// michael@0: michael@0: /** michael@0: * Class that represents a parsed policy structure. michael@0: * michael@0: * @param aSpecCompliant: true: this policy is a CSP 1.0 spec michael@0: * compliant policy and should be parsed as such. michael@0: * false or undefined: this is a policy using michael@0: * our original implementation's CSP syntax. michael@0: */ michael@0: this.CSPRep = function CSPRep(aSpecCompliant) { michael@0: // this gets set to true when the policy is done parsing, or when a michael@0: // URI-borne policy has finished loading. michael@0: this._isInitialized = false; michael@0: michael@0: this._allowEval = false; michael@0: this._allowInlineScripts = false; michael@0: this._reportOnlyMode = false; michael@0: michael@0: // don't auto-populate _directives, so it is easier to find bugs michael@0: this._directives = {}; michael@0: michael@0: // Is this a 1.0 spec compliant CSPRep ? michael@0: // Default to false if not specified. michael@0: this._specCompliant = (aSpecCompliant !== undefined) ? aSpecCompliant : false; michael@0: michael@0: // Only CSP 1.0 spec compliant policies block inline styles by default. michael@0: this._allowInlineStyles = !aSpecCompliant; michael@0: } michael@0: michael@0: // Source directives for our original CSP implementation. michael@0: // These can be removed when the original implementation is deprecated. michael@0: CSPRep.SRC_DIRECTIVES_OLD = { michael@0: DEFAULT_SRC: "default-src", michael@0: SCRIPT_SRC: "script-src", michael@0: STYLE_SRC: "style-src", michael@0: MEDIA_SRC: "media-src", michael@0: IMG_SRC: "img-src", michael@0: OBJECT_SRC: "object-src", michael@0: FRAME_SRC: "frame-src", michael@0: FRAME_ANCESTORS: "frame-ancestors", michael@0: FONT_SRC: "font-src", michael@0: XHR_SRC: "xhr-src" michael@0: }; michael@0: michael@0: // Source directives for our CSP 1.0 spec compliant implementation. michael@0: CSPRep.SRC_DIRECTIVES_NEW = { michael@0: DEFAULT_SRC: "default-src", michael@0: SCRIPT_SRC: "script-src", michael@0: STYLE_SRC: "style-src", michael@0: MEDIA_SRC: "media-src", michael@0: IMG_SRC: "img-src", michael@0: OBJECT_SRC: "object-src", michael@0: FRAME_SRC: "frame-src", michael@0: FRAME_ANCESTORS: "frame-ancestors", michael@0: FONT_SRC: "font-src", michael@0: CONNECT_SRC: "connect-src" michael@0: }; michael@0: michael@0: CSPRep.URI_DIRECTIVES = { michael@0: REPORT_URI: "report-uri", /* list of URIs */ michael@0: POLICY_URI: "policy-uri" /* single URI */ michael@0: }; michael@0: michael@0: // These directives no longer exist in CSP 1.0 and michael@0: // later and will eventually be removed when we no longer michael@0: // support our original implementation's syntax. michael@0: CSPRep.OPTIONS_DIRECTIVE = "options"; michael@0: CSPRep.ALLOW_DIRECTIVE = "allow"; michael@0: michael@0: /** michael@0: * Factory to create a new CSPRep, parsed from a string. michael@0: * michael@0: * @param aStr michael@0: * string rep of a CSP michael@0: * @param self (optional) michael@0: * URI representing the "self" source michael@0: * @param reportOnly (optional) michael@0: * whether or not this CSP is report-only (defaults to false) michael@0: * @param docRequest (optional) michael@0: * request for the parent document which may need to be suspended michael@0: * while the policy-uri is asynchronously fetched michael@0: * @param csp (optional) michael@0: * the CSP object to update once the policy has been fetched michael@0: * @param enforceSelfChecks (optional) michael@0: * if present, and "true", will check to be sure "self" has the michael@0: * appropriate values to inherit when they are omitted from the source. michael@0: * @returns michael@0: * an instance of CSPRep michael@0: */ michael@0: CSPRep.fromString = function(aStr, self, reportOnly, docRequest, csp, michael@0: enforceSelfChecks) { michael@0: var SD = CSPRep.SRC_DIRECTIVES_OLD; michael@0: var UD = CSPRep.URI_DIRECTIVES; michael@0: var aCSPR = new CSPRep(); michael@0: aCSPR._originalText = aStr; michael@0: aCSPR._innerWindowID = innerWindowFromRequest(docRequest); michael@0: if (typeof reportOnly === 'undefined') reportOnly = false; michael@0: aCSPR._reportOnlyMode = reportOnly; michael@0: michael@0: var selfUri = null; michael@0: if (self instanceof Ci.nsIURI) { michael@0: selfUri = self.cloneIgnoringRef(); michael@0: // clean userpass out of the URI (not used for CSP origin checking, but michael@0: // shows up in prePath). michael@0: try { michael@0: // GetUserPass throws for some protocols without userPass michael@0: selfUri.userPass = ''; michael@0: } catch (ex) {} michael@0: } michael@0: michael@0: var dirs = aStr.split(";"); michael@0: michael@0: directive: michael@0: for each(var dir in dirs) { michael@0: dir = dir.trim(); michael@0: if (dir.length < 1) continue; michael@0: michael@0: var dirname = dir.split(/\s+/)[0].toLowerCase(); michael@0: var dirvalue = dir.substring(dirname.length).trim(); michael@0: michael@0: if (aCSPR._directives.hasOwnProperty(dirname)) { michael@0: // Check for (most) duplicate directives michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective", michael@0: [dirname])); michael@0: CSPdebug("Skipping duplicate directive: \"" + dir + "\""); michael@0: continue directive; michael@0: } michael@0: michael@0: // OPTIONS DIRECTIVE //////////////////////////////////////////////// michael@0: if (dirname === CSPRep.OPTIONS_DIRECTIVE) { michael@0: if (aCSPR._allowInlineScripts || aCSPR._allowEval) { michael@0: // Check for duplicate options directives michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective", michael@0: [dirname])); michael@0: CSPdebug("Skipping duplicate directive: \"" + dir + "\""); michael@0: continue directive; michael@0: } michael@0: michael@0: // grab value tokens and interpret them michael@0: var options = dirvalue.split(/\s+/); michael@0: for each (var opt in options) { michael@0: if (opt === "inline-script") michael@0: aCSPR._allowInlineScripts = true; michael@0: else if (opt === "eval-script") michael@0: aCSPR._allowEval = true; michael@0: else michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("ignoringUnknownOption", michael@0: [opt])); michael@0: } michael@0: continue directive; michael@0: } michael@0: michael@0: // ALLOW DIRECTIVE ////////////////////////////////////////////////// michael@0: // parse "allow" as equivalent to "default-src", at least until the spec michael@0: // stabilizes, at which time we can stop parsing "allow" michael@0: if (dirname === CSPRep.ALLOW_DIRECTIVE) { michael@0: cspWarn(aCSPR, CSPLocalizer.getStr("allowDirectiveIsDeprecated")); michael@0: if (aCSPR._directives.hasOwnProperty(SD.DEFAULT_SRC)) { michael@0: // Check for duplicate default-src and allow directives michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective", michael@0: [dirname])); michael@0: CSPdebug("Skipping duplicate directive: \"" + dir + "\""); michael@0: continue directive; michael@0: } michael@0: var dv = CSPSourceList.fromString(dirvalue, aCSPR, selfUri, michael@0: enforceSelfChecks); michael@0: if (dv) { michael@0: aCSPR._directives[SD.DEFAULT_SRC] = dv; michael@0: continue directive; michael@0: } michael@0: } michael@0: michael@0: // SOURCE DIRECTIVES //////////////////////////////////////////////// michael@0: for each(var sdi in SD) { michael@0: if (dirname === sdi) { michael@0: // process dirs, and enforce that 'self' is defined. michael@0: var dv = CSPSourceList.fromString(dirvalue, aCSPR, selfUri, michael@0: enforceSelfChecks); michael@0: if (dv) { michael@0: aCSPR._directives[sdi] = dv; michael@0: continue directive; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // REPORT URI /////////////////////////////////////////////////////// michael@0: if (dirname === UD.REPORT_URI) { michael@0: // might be space-separated list of URIs michael@0: var uriStrings = dirvalue.split(/\s+/); michael@0: var okUriStrings = []; michael@0: michael@0: for (let i in uriStrings) { michael@0: var uri = null; michael@0: try { michael@0: // Relative URIs are okay, but to ensure we send the reports to the michael@0: // right spot, the relative URIs are expanded here during parsing. michael@0: // The resulting CSPRep instance will have only absolute URIs. michael@0: uri = gIoService.newURI(uriStrings[i],null,selfUri); michael@0: michael@0: // if there's no host, this will throw NS_ERROR_FAILURE, causing a michael@0: // parse failure. michael@0: uri.host; michael@0: michael@0: // warn about, but do not prohibit non-http and non-https schemes for michael@0: // reporting URIs. The spec allows unrestricted URIs resolved michael@0: // relative to "self", but we should let devs know if the scheme is michael@0: // abnormal and may fail a POST. michael@0: if (!uri.schemeIs("http") && !uri.schemeIs("https")) { michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotHttpsOrHttp2", michael@0: [uri.asciiSpec])); michael@0: } michael@0: } catch(e) { michael@0: switch (e.result) { michael@0: case Components.results.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: michael@0: case Components.results.NS_ERROR_HOST_IS_IP_ADDRESS: michael@0: if (uri.host !== selfUri.host) { michael@0: cspWarn(aCSPR, michael@0: CSPLocalizer.getFormatStr("pageCannotSendReportsTo", michael@0: [selfUri.host, uri.host])); michael@0: continue; michael@0: } michael@0: break; michael@0: michael@0: default: michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotParseReportURI", michael@0: [uriStrings[i]])); michael@0: continue; michael@0: } michael@0: } michael@0: // all verification passed michael@0: okUriStrings.push(uri.asciiSpec); michael@0: } michael@0: aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' '); michael@0: continue directive; michael@0: } michael@0: michael@0: // POLICY URI ////////////////////////////////////////////////////////// michael@0: if (dirname === UD.POLICY_URI) { michael@0: // POLICY_URI can only be alone michael@0: if (aCSPR._directives.length > 0 || dirs.length > 1) { michael@0: cspError(aCSPR, CSPLocalizer.getStr("policyURINotAlone")); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: // if we were called without a reference to the parent document request michael@0: // we won't be able to suspend it while we fetch the policy -> fail closed michael@0: if (!docRequest || !csp) { michael@0: cspError(aCSPR, CSPLocalizer.getStr("noParentRequest")); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: michael@0: var uri = ''; michael@0: try { michael@0: uri = gIoService.newURI(dirvalue, null, selfUri); michael@0: } catch(e) { michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("policyURIParseError", michael@0: [dirvalue])); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: michael@0: // Verify that policy URI comes from the same origin michael@0: if (selfUri) { michael@0: if (selfUri.host !== uri.host) { michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingHost", michael@0: [uri.host])); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: if (selfUri.port !== uri.port) { michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingPort", michael@0: [uri.port.toString()])); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: if (selfUri.scheme !== uri.scheme) { michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingScheme", michael@0: [uri.scheme])); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: } michael@0: michael@0: // suspend the parent document request while we fetch the policy-uri michael@0: try { michael@0: docRequest.suspend(); michael@0: var chan = gIoService.newChannel(uri.asciiSpec, null, null); michael@0: // make request anonymous (no cookies, etc.) so the request for the michael@0: // policy-uri can't be abused for CSRF michael@0: chan.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS; michael@0: chan.loadGroup = docRequest.loadGroup; michael@0: chan.asyncOpen(new CSPPolicyURIListener(uri, docRequest, csp, reportOnly), null); michael@0: } michael@0: catch (e) { michael@0: // resume the document request and apply most restrictive policy michael@0: docRequest.resume(); michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("errorFetchingPolicy", michael@0: [e.toString()])); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: michael@0: // return a fully-open policy to be used until the contents of the michael@0: // policy-uri come back. michael@0: return CSPRep.fromString("default-src *", null, reportOnly); michael@0: } michael@0: michael@0: // UNIDENTIFIED DIRECTIVE ///////////////////////////////////////////// michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotProcessUnknownDirective", michael@0: [dirname])); michael@0: michael@0: } // end directive: loop michael@0: michael@0: // the X-Content-Security-Policy syntax requires an allow or default-src michael@0: // directive to be present. michael@0: if (!aCSPR._directives[SD.DEFAULT_SRC]) { michael@0: cspWarn(aCSPR, CSPLocalizer.getStr("allowOrDefaultSrcRequired")); michael@0: return CSPRep.fromString("default-src 'none'", null, reportOnly); michael@0: } michael@0: michael@0: // If this is a Report-Only header and report-uri is not in the directive michael@0: // list, tell developer either specify report-uri directive or use michael@0: // a non-Report-Only CSP header. michael@0: if (aCSPR._reportOnlyMode && !aCSPR._directives.hasOwnProperty(UD.REPORT_URI)) { michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotInReportOnlyHeader", michael@0: [selfUri ? selfUri.prePath : "undefined"])) michael@0: } michael@0: michael@0: return aCSPR; michael@0: }; michael@0: michael@0: /** michael@0: * Factory to create a new CSPRep, parsed from a string, compliant michael@0: * with the CSP 1.0 spec. michael@0: * michael@0: * @param aStr michael@0: * string rep of a CSP michael@0: * @param self (optional) michael@0: * URI representing the "self" source michael@0: * @param reportOnly (optional) michael@0: * whether or not this CSP is report-only (defaults to false) michael@0: * @param docRequest (optional) michael@0: * request for the parent document which may need to be suspended michael@0: * while the policy-uri is asynchronously fetched michael@0: * @param csp (optional) michael@0: * the CSP object to update once the policy has been fetched michael@0: * @param enforceSelfChecks (optional) michael@0: * if present, and "true", will check to be sure "self" has the michael@0: * appropriate values to inherit when they are omitted from the source. michael@0: * @returns michael@0: * an instance of CSPRep michael@0: */ michael@0: // When we deprecate our original CSP implementation, we rename this to michael@0: // CSPRep.fromString and remove the existing CSPRep.fromString above. michael@0: CSPRep.fromStringSpecCompliant = function(aStr, self, reportOnly, docRequest, csp, michael@0: enforceSelfChecks) { michael@0: var SD = CSPRep.SRC_DIRECTIVES_NEW; michael@0: var UD = CSPRep.URI_DIRECTIVES; michael@0: var aCSPR = new CSPRep(true); michael@0: aCSPR._originalText = aStr; michael@0: aCSPR._innerWindowID = innerWindowFromRequest(docRequest); michael@0: if (typeof reportOnly === 'undefined') reportOnly = false; michael@0: aCSPR._reportOnlyMode = reportOnly; michael@0: michael@0: var selfUri = null; michael@0: if (self instanceof Ci.nsIURI) { michael@0: selfUri = self.cloneIgnoringRef(); michael@0: // clean userpass out of the URI (not used for CSP origin checking, but michael@0: // shows up in prePath). michael@0: try { michael@0: // GetUserPass throws for some protocols without userPass michael@0: selfUri.userPass = ''; michael@0: } catch (ex) {} michael@0: } michael@0: michael@0: var dirs_list = aStr.split(";"); michael@0: var dirs = {}; michael@0: for each(var dir in dirs_list) { michael@0: dir = dir.trim(); michael@0: if (dir.length < 1) continue; michael@0: michael@0: var dirname = dir.split(/\s+/)[0].toLowerCase(); michael@0: var dirvalue = dir.substring(dirname.length).trim(); michael@0: dirs[dirname] = dirvalue; michael@0: } michael@0: michael@0: // Spec compliant policies have different default behavior for inline michael@0: // scripts, styles, and eval. Bug 885433 michael@0: aCSPR._allowEval = true; michael@0: aCSPR._allowInlineScripts = true; michael@0: aCSPR._allowInlineStyles = true; michael@0: michael@0: // In CSP 1.0, you need to opt-in to blocking inline scripts and eval by michael@0: // specifying either default-src or script-src, and to blocking inline michael@0: // styles by specifying either default-src or style-src. michael@0: if ("default-src" in dirs) { michael@0: // Parse the source list (look ahead) so we can set the defaults properly, michael@0: // honoring the 'unsafe-inline' and 'unsafe-eval' keywords michael@0: var defaultSrcValue = CSPSourceList.fromString(dirs["default-src"], null, self); michael@0: if (!defaultSrcValue._allowUnsafeInline) { michael@0: aCSPR._allowInlineScripts = false; michael@0: aCSPR._allowInlineStyles = false; michael@0: } michael@0: if (!defaultSrcValue._allowUnsafeEval) { michael@0: aCSPR._allowEval = false; michael@0: } michael@0: } michael@0: if ("script-src" in dirs) { michael@0: aCSPR._allowInlineScripts = false; michael@0: aCSPR._allowEval = false; michael@0: } michael@0: if ("style-src" in dirs) { michael@0: aCSPR._allowInlineStyles = false; michael@0: } michael@0: michael@0: directive: michael@0: for (var dirname in dirs) { michael@0: var dirvalue = dirs[dirname]; michael@0: michael@0: if (aCSPR._directives.hasOwnProperty(dirname)) { michael@0: // Check for (most) duplicate directives michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective", michael@0: [dirname])); michael@0: CSPdebug("Skipping duplicate directive: \"" + dir + "\""); michael@0: continue directive; michael@0: } michael@0: michael@0: // SOURCE DIRECTIVES //////////////////////////////////////////////// michael@0: for each(var sdi in SD) { michael@0: if (dirname === sdi) { michael@0: // process dirs, and enforce that 'self' is defined. michael@0: var dv = CSPSourceList.fromString(dirvalue, aCSPR, self, michael@0: enforceSelfChecks); michael@0: if (dv) { michael@0: // Check for unsafe-inline in style-src michael@0: if (sdi === "style-src" && dv._allowUnsafeInline) { michael@0: aCSPR._allowInlineStyles = true; michael@0: } else if (sdi === "script-src") { michael@0: // Check for unsafe-inline and unsafe-eval in script-src michael@0: if (dv._allowUnsafeInline) { michael@0: aCSPR._allowInlineScripts = true; michael@0: } michael@0: if (dv._allowUnsafeEval) { michael@0: aCSPR._allowEval = true; michael@0: } michael@0: } michael@0: michael@0: aCSPR._directives[sdi] = dv; michael@0: continue directive; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // REPORT URI /////////////////////////////////////////////////////// michael@0: if (dirname === UD.REPORT_URI) { michael@0: // might be space-separated list of URIs michael@0: var uriStrings = dirvalue.split(/\s+/); michael@0: var okUriStrings = []; michael@0: michael@0: for (let i in uriStrings) { michael@0: var uri = null; michael@0: try { michael@0: // Relative URIs are okay, but to ensure we send the reports to the michael@0: // right spot, the relative URIs are expanded here during parsing. michael@0: // The resulting CSPRep instance will have only absolute URIs. michael@0: uri = gIoService.newURI(uriStrings[i],null,selfUri); michael@0: michael@0: // if there's no host, this will throw NS_ERROR_FAILURE, causing a michael@0: // parse failure. michael@0: uri.host; michael@0: michael@0: // warn about, but do not prohibit non-http and non-https schemes for michael@0: // reporting URIs. The spec allows unrestricted URIs resolved michael@0: // relative to "self", but we should let devs know if the scheme is michael@0: // abnormal and may fail a POST. michael@0: if (!uri.schemeIs("http") && !uri.schemeIs("https")) { michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotHttpsOrHttp2", michael@0: [uri.asciiSpec])); michael@0: } michael@0: } catch(e) { michael@0: switch (e.result) { michael@0: case Components.results.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: michael@0: case Components.results.NS_ERROR_HOST_IS_IP_ADDRESS: michael@0: if (uri.host !== selfUri.host) { michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("pageCannotSendReportsTo", michael@0: [selfUri.host, uri.host])); michael@0: continue; michael@0: } michael@0: break; michael@0: michael@0: default: michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotParseReportURI", michael@0: [uriStrings[i]])); michael@0: continue; michael@0: } michael@0: } michael@0: // all verification passed. michael@0: okUriStrings.push(uri.asciiSpec); michael@0: } michael@0: aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' '); michael@0: continue directive; michael@0: } michael@0: michael@0: // POLICY URI ////////////////////////////////////////////////////////// michael@0: if (dirname === UD.POLICY_URI) { michael@0: // POLICY_URI can only be alone michael@0: if (aCSPR._directives.length > 0 || dirs.length > 1) { michael@0: cspError(aCSPR, CSPLocalizer.getStr("policyURINotAlone")); michael@0: return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly); michael@0: } michael@0: // if we were called without a reference to the parent document request michael@0: // we won't be able to suspend it while we fetch the policy -> fail closed michael@0: if (!docRequest || !csp) { michael@0: cspError(aCSPR, CSPLocalizer.getStr("noParentRequest")); michael@0: return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly); michael@0: } michael@0: michael@0: var uri = ''; michael@0: try { michael@0: uri = gIoService.newURI(dirvalue, null, selfUri); michael@0: } catch(e) { michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("policyURIParseError", [dirvalue])); michael@0: return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly); michael@0: } michael@0: michael@0: // Verify that policy URI comes from the same origin michael@0: if (selfUri) { michael@0: if (selfUri.host !== uri.host){ michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingHost", [uri.host])); michael@0: return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly); michael@0: } michael@0: if (selfUri.port !== uri.port){ michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingPort", [uri.port.toString()])); michael@0: return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly); michael@0: } michael@0: if (selfUri.scheme !== uri.scheme){ michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingScheme", [uri.scheme])); michael@0: return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly); michael@0: } michael@0: } michael@0: michael@0: // suspend the parent document request while we fetch the policy-uri michael@0: try { michael@0: docRequest.suspend(); michael@0: var chan = gIoService.newChannel(uri.asciiSpec, null, null); michael@0: // make request anonymous (no cookies, etc.) so the request for the michael@0: // policy-uri can't be abused for CSRF michael@0: chan.loadFlags |= Components.interfaces.nsIChannel.LOAD_ANONYMOUS; michael@0: chan.loadGroup = docRequest.loadGroup; michael@0: chan.asyncOpen(new CSPPolicyURIListener(uri, docRequest, csp, reportOnly), null); michael@0: } michael@0: catch (e) { michael@0: // resume the document request and apply most restrictive policy michael@0: docRequest.resume(); michael@0: cspError(aCSPR, CSPLocalizer.getFormatStr("errorFetchingPolicy", [e.toString()])); michael@0: return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly); michael@0: } michael@0: michael@0: // return a fully-open policy to be used until the contents of the michael@0: // policy-uri come back michael@0: return CSPRep.fromStringSpecCompliant("default-src *", null, reportOnly); michael@0: } michael@0: michael@0: // UNIDENTIFIED DIRECTIVE ///////////////////////////////////////////// michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotProcessUnknownDirective", [dirname])); michael@0: michael@0: } // end directive: loop michael@0: michael@0: // If this is a Report-Only header and report-uri is not in the directive michael@0: // list, tell developer either specify report-uri directive or use michael@0: // a non-Report-Only CSP header. michael@0: if (aCSPR._reportOnlyMode && !aCSPR._directives.hasOwnProperty(UD.REPORT_URI)) { michael@0: cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotInReportOnlyHeader", michael@0: [selfUri ? selfUri.prePath : "undefined"])); michael@0: } michael@0: michael@0: return aCSPR; michael@0: }; michael@0: michael@0: CSPRep.prototype = { michael@0: /** michael@0: * Returns a space-separated list of all report uris defined, or 'none' if there are none. michael@0: */ michael@0: getReportURIs: michael@0: function() { michael@0: if (!this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI]) michael@0: return ""; michael@0: return this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI]; michael@0: }, michael@0: michael@0: /** michael@0: * Compares this CSPRep instance to another. michael@0: */ michael@0: equals: michael@0: function(that) { michael@0: if (this._directives.length != that._directives.length) { michael@0: return false; michael@0: } michael@0: for (var i in this._directives) { michael@0: if (!that._directives[i] || !this._directives[i].equals(that._directives[i])) { michael@0: return false; michael@0: } michael@0: } michael@0: return (this.allowsInlineScripts === that.allowsInlineScripts) michael@0: && (this.allowsEvalInScripts === that.allowsEvalInScripts) michael@0: && (this.allowsInlineStyles === that.allowsInlineStyles); michael@0: }, michael@0: michael@0: /** michael@0: * Generates canonical string representation of the policy. michael@0: */ michael@0: toString: michael@0: function csp_toString() { michael@0: var dirs = []; michael@0: michael@0: if (!this._specCompliant && (this._allowEval || this._allowInlineScripts)) { michael@0: dirs.push("options" + (this._allowEval ? " eval-script" : "") michael@0: + (this._allowInlineScripts ? " inline-script" : "")); michael@0: } michael@0: for (var i in this._directives) { michael@0: if (this._directives[i]) { michael@0: dirs.push(i + " " + this._directives[i].toString()); michael@0: } michael@0: } michael@0: return dirs.join("; "); michael@0: }, michael@0: michael@0: permitsNonce: michael@0: function csp_permitsNonce(aNonce, aDirective) { michael@0: if (!this._directives.hasOwnProperty(aDirective)) return false; michael@0: return this._directives[aDirective]._sources.some(function (source) { michael@0: return source instanceof CSPNonceSource && source.permits(aNonce); michael@0: }); michael@0: }, michael@0: michael@0: permitsHash: michael@0: function csp_permitsHash(aContent, aDirective) { michael@0: if (!this._directives.hasOwnProperty(aDirective)) return false; michael@0: return this._directives[aDirective]._sources.some(function (source) { michael@0: return source instanceof CSPHashSource && source.permits(aContent); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Determines if this policy accepts a URI. michael@0: * @param aURI michael@0: * URI of the requested resource michael@0: * @param aDirective michael@0: * one of the SRC_DIRECTIVES defined above michael@0: * @returns michael@0: * true if the policy permits the URI in given context. michael@0: */ michael@0: permits: michael@0: function csp_permits(aURI, aDirective) { michael@0: if (!aURI) return false; michael@0: michael@0: // GLOBALLY ALLOW "about:" SCHEME michael@0: if (aURI instanceof String && aURI.substring(0,6) === "about:") michael@0: return true; michael@0: if (aURI instanceof Ci.nsIURI && aURI.scheme === "about") michael@0: return true; michael@0: michael@0: // make sure the right directive set is used michael@0: let DIRS = this._specCompliant ? CSPRep.SRC_DIRECTIVES_NEW : CSPRep.SRC_DIRECTIVES_OLD; michael@0: michael@0: let directiveInPolicy = false; michael@0: for (var i in DIRS) { michael@0: if (DIRS[i] === aDirective) { michael@0: // for catching calls with invalid contexts (below) michael@0: directiveInPolicy = true; michael@0: if (this._directives.hasOwnProperty(aDirective)) { michael@0: return this._directives[aDirective].permits(aURI); michael@0: } michael@0: //found matching dir, can stop looking michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // frame-ancestors is a special case; it doesn't fall back to default-src. michael@0: if (aDirective === DIRS.FRAME_ANCESTORS) michael@0: return true; michael@0: michael@0: // All directives that don't fall back to default-src should have an escape michael@0: // hatch above (like frame-ancestors). michael@0: if (!directiveInPolicy) { michael@0: // if this code runs, there's probably something calling permits() that michael@0: // shouldn't be calling permits(). michael@0: CSPdebug("permits called with invalid load type: " + aDirective); michael@0: return false; michael@0: } michael@0: michael@0: // no directives specifically matched, fall back to default-src. michael@0: // (default-src may not be present for CSP 1.0-compliant policies, and michael@0: // indicates no relevant directives were present and the load should be michael@0: // permitted). michael@0: if (this._directives.hasOwnProperty(DIRS.DEFAULT_SRC)) { michael@0: return this._directives[DIRS.DEFAULT_SRC].permits(aURI); michael@0: } michael@0: michael@0: // no relevant directives present -- this means for CSP 1.0 that the load michael@0: // should be permitted (and for the old CSP, to block it). michael@0: return this._specCompliant; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if "eval" is enabled through the "eval" keyword. michael@0: */ michael@0: get allowsEvalInScripts () { michael@0: return this._allowEval; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if inline scripts are enabled through the "inline" michael@0: * keyword. michael@0: */ michael@0: get allowsInlineScripts () { michael@0: return this._allowInlineScripts; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if inline styles are enabled through the "inline-style" michael@0: * keyword. michael@0: */ michael@0: get allowsInlineStyles () { michael@0: return this._allowInlineStyles; michael@0: }, michael@0: michael@0: /** michael@0: * Sends a message to the error console and web developer console. michael@0: * @param aFlag michael@0: * The nsIScriptError flag constant indicating severity michael@0: * @param aMsg michael@0: * The message to send michael@0: * @param aSource (optional) michael@0: * The URL of the file in which the error occurred michael@0: * @param aScriptLine (optional) michael@0: * The line in the source file which the error occurred michael@0: * @param aLineNum (optional) michael@0: * The number of the line where the error occurred michael@0: */ michael@0: log: michael@0: function cspd_log(aFlag, aMsg, aSource, aScriptLine, aLineNum) { michael@0: var textMessage = "Content Security Policy: " + aMsg; michael@0: var consoleMsg = Components.classes["@mozilla.org/scripterror;1"] michael@0: .createInstance(Ci.nsIScriptError); michael@0: if (this._innerWindowID) { michael@0: consoleMsg.initWithWindowID(textMessage, aSource, aScriptLine, aLineNum, michael@0: 0, aFlag, michael@0: "CSP", michael@0: this._innerWindowID); michael@0: } else { michael@0: consoleMsg.init(textMessage, aSource, aScriptLine, aLineNum, 0, michael@0: aFlag, michael@0: "CSP"); michael@0: } michael@0: Components.classes["@mozilla.org/consoleservice;1"] michael@0: .getService(Ci.nsIConsoleService).logMessage(consoleMsg); michael@0: }, michael@0: michael@0: }; michael@0: michael@0: ////////////////////////////////////////////////////////////////////// michael@0: /** michael@0: * Class to represent a list of sources michael@0: */ michael@0: this.CSPSourceList = function CSPSourceList() { michael@0: this._sources = []; michael@0: this._permitAllSources = false; michael@0: michael@0: // When this is true, the source list contains 'unsafe-inline'. michael@0: this._allowUnsafeInline = false; michael@0: michael@0: // When this is true, the source list contains 'unsafe-eval'. michael@0: this._allowUnsafeEval = false; michael@0: michael@0: // When this is true, the source list contains at least one nonce-source michael@0: this._hasNonceSource = false; michael@0: michael@0: // When this is true, the source list contains at least one hash-source michael@0: this._hasHashSource = false; michael@0: } michael@0: michael@0: /** michael@0: * Factory to create a new CSPSourceList, parsed from a string. michael@0: * michael@0: * @param aStr michael@0: * string rep of a CSP Source List michael@0: * @param aCSPRep michael@0: * the CSPRep to which this souce list belongs. If null, CSP errors and michael@0: * warnings will not be sent to the web console. michael@0: * @param self (optional) michael@0: * URI or CSPSource representing the "self" source michael@0: * @param enforceSelfChecks (optional) michael@0: * if present, and "true", will check to be sure "self" has the michael@0: * appropriate values to inherit when they are omitted from the source. michael@0: * @returns michael@0: * an instance of CSPSourceList michael@0: */ michael@0: CSPSourceList.fromString = function(aStr, aCSPRep, self, enforceSelfChecks) { michael@0: // source-list = *WSP [ source-expression *( 1*WSP source-expression ) *WSP ] michael@0: // / *WSP "'none'" *WSP michael@0: michael@0: /* If self parameter is passed, convert to CSPSource, michael@0: unless it is already a CSPSource. */ michael@0: if (self && !(self instanceof CSPSource)) { michael@0: self = CSPSource.create(self, aCSPRep); michael@0: } michael@0: michael@0: var slObj = new CSPSourceList(); michael@0: slObj._CSPRep = aCSPRep; michael@0: aStr = aStr.trim(); michael@0: // w3 specifies case insensitive equality michael@0: if (aStr.toLowerCase() === "'none'") { michael@0: slObj._permitAllSources = false; michael@0: return slObj; michael@0: } michael@0: michael@0: var tokens = aStr.split(/\s+/); michael@0: for (var i in tokens) { michael@0: if (!R_SOURCEEXP.test(tokens[i])) { michael@0: cspWarn(aCSPRep, michael@0: CSPLocalizer.getFormatStr("failedToParseUnrecognizedSource", michael@0: [tokens[i]])); michael@0: continue; michael@0: } michael@0: var src = CSPSource.create(tokens[i], aCSPRep, self, enforceSelfChecks); michael@0: if (!src) { michael@0: cspWarn(aCSPRep, michael@0: CSPLocalizer.getFormatStr("failedToParseUnrecognizedSource", michael@0: [tokens[i]])); michael@0: continue; michael@0: } michael@0: michael@0: // if a source allows unsafe-inline, set our flag to indicate this. michael@0: if (src._allowUnsafeInline) michael@0: slObj._allowUnsafeInline = true; michael@0: michael@0: // if a source allows unsafe-eval, set our flag to indicate this. michael@0: if (src._allowUnsafeEval) michael@0: slObj._allowUnsafeEval = true; michael@0: michael@0: if (src instanceof CSPNonceSource) michael@0: slObj._hasNonceSource = true; michael@0: michael@0: if (src instanceof CSPHashSource) michael@0: slObj._hasHashSource = true; michael@0: michael@0: // if a source is a *, then we can permit all sources michael@0: if (src.permitAll) { michael@0: slObj._permitAllSources = true; michael@0: } else { michael@0: slObj._sources.push(src); michael@0: } michael@0: } michael@0: michael@0: return slObj; michael@0: }; michael@0: michael@0: CSPSourceList.prototype = { michael@0: /** michael@0: * Compares one CSPSourceList to another. michael@0: * michael@0: * @param that michael@0: * another CSPSourceList michael@0: * @returns michael@0: * true if they have the same data michael@0: */ michael@0: equals: michael@0: function(that) { michael@0: // special case to default-src * and 'none' to look different michael@0: // (both have a ._sources.length of 0). michael@0: if (that._permitAllSources != this._permitAllSources) { michael@0: return false; michael@0: } michael@0: if (that._sources.length != this._sources.length) { michael@0: return false; michael@0: } michael@0: // sort both arrays and compare like a zipper michael@0: // XXX (sid): I think we can make this more efficient michael@0: var sortfn = function(a,b) { michael@0: return a.toString.toLowerCase() > b.toString.toLowerCase(); michael@0: }; michael@0: var a_sorted = this._sources.sort(sortfn); michael@0: var b_sorted = that._sources.sort(sortfn); michael@0: for (var i in a_sorted) { michael@0: if (!a_sorted[i].equals(b_sorted[i])) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Generates canonical string representation of the Source List. michael@0: */ michael@0: toString: michael@0: function() { michael@0: if (this.isNone()) { michael@0: return "'none'"; michael@0: } michael@0: if (this._permitAllSources) { michael@0: return "*"; michael@0: } michael@0: return this._sources.map(function(x) { return x.toString(); }).join(" "); michael@0: }, michael@0: michael@0: /** michael@0: * Returns whether or not this source list represents the "'none'" special michael@0: * case. michael@0: */ michael@0: isNone: michael@0: function() { michael@0: return (!this._permitAllSources) && (this._sources.length < 1); michael@0: }, michael@0: michael@0: /** michael@0: * Returns whether or not this source list permits all sources (*). michael@0: */ michael@0: isAll: michael@0: function() { michael@0: return this._permitAllSources; michael@0: }, michael@0: michael@0: /** michael@0: * Makes a new deep copy of this object. michael@0: * @returns michael@0: * a new CSPSourceList michael@0: */ michael@0: clone: michael@0: function() { michael@0: var aSL = new CSPSourceList(); michael@0: aSL._permitAllSources = this._permitAllSources; michael@0: aSL._CSPRep = this._CSPRep; michael@0: for (var i in this._sources) { michael@0: aSL._sources[i] = this._sources[i].clone(); michael@0: } michael@0: return aSL; michael@0: }, michael@0: michael@0: /** michael@0: * Determines if this directive accepts a URI. michael@0: * @param aURI michael@0: * the URI in question michael@0: * @returns michael@0: * true if the URI matches a source in this source list. michael@0: */ michael@0: permits: michael@0: function cspsd_permits(aURI) { michael@0: if (this.isNone()) return false; michael@0: if (this.isAll()) return true; michael@0: michael@0: for (var i in this._sources) { michael@0: if (this._sources[i].permits(aURI)) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: ////////////////////////////////////////////////////////////////////// michael@0: /** michael@0: * Class to model a source (scheme, host, port) michael@0: */ michael@0: this.CSPSource = function CSPSource() { michael@0: this._scheme = undefined; michael@0: this._port = undefined; michael@0: this._host = undefined; michael@0: michael@0: //when set to true, this allows all source michael@0: this._permitAll = false; michael@0: michael@0: // when set to true, this source represents 'self' michael@0: this._isSelf = false; michael@0: michael@0: // when set to true, this source allows inline scripts or styles michael@0: this._allowUnsafeInline = false; michael@0: michael@0: // when set to true, this source allows eval to be used michael@0: this._allowUnsafeEval = false; michael@0: } michael@0: michael@0: /** michael@0: * General factory method to create a new source from one of the following michael@0: * types: michael@0: * - nsURI michael@0: * - string michael@0: * - CSPSource (clone) michael@0: * @param aData michael@0: * string, nsURI, or CSPSource michael@0: * @param aCSPRep michael@0: * The CSPRep this source belongs to. If null, CSP errors and warnings michael@0: * will not be sent to the web console. michael@0: * @param self (optional) michael@0: * if present, string, URI, or CSPSource representing the "self" resource michael@0: * @param enforceSelfChecks (optional) michael@0: * if present, and "true", will check to be sure "self" has the michael@0: * appropriate values to inherit when they are omitted from the source. michael@0: * @returns michael@0: * an instance of CSPSource michael@0: */ michael@0: CSPSource.create = function(aData, aCSPRep, self, enforceSelfChecks) { michael@0: if (typeof aData === 'string') michael@0: return CSPSource.fromString(aData, aCSPRep, self, enforceSelfChecks); michael@0: michael@0: if (aData instanceof Ci.nsIURI) { michael@0: // clean userpass out of the URI (not used for CSP origin checking, but michael@0: // shows up in prePath). michael@0: let cleanedUri = aData.cloneIgnoringRef(); michael@0: try { michael@0: // GetUserPass throws for some protocols without userPass michael@0: cleanedUri.userPass = ''; michael@0: } catch (ex) {} michael@0: michael@0: return CSPSource.fromURI(cleanedUri, aCSPRep, self, enforceSelfChecks); michael@0: } michael@0: michael@0: if (aData instanceof CSPSource) { michael@0: var ns = aData.clone(); michael@0: ns._self = CSPSource.create(self); michael@0: return ns; michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Factory to create a new CSPSource, from a nsIURI. michael@0: * michael@0: * Don't use this if you want to wildcard ports! michael@0: * michael@0: * @param aURI michael@0: * nsIURI rep of a URI michael@0: * @param aCSPRep michael@0: * The policy this source belongs to. If null, CSP errors and warnings michael@0: * will not be sent to the web console. michael@0: * @param self (optional) michael@0: * string or CSPSource representing the "self" source michael@0: * @param enforceSelfChecks (optional) michael@0: * if present, and "true", will check to be sure "self" has the michael@0: * appropriate values to inherit when they are omitted from aURI. michael@0: * @returns michael@0: * an instance of CSPSource michael@0: */ michael@0: CSPSource.fromURI = function(aURI, aCSPRep, self, enforceSelfChecks) { michael@0: if (!(aURI instanceof Ci.nsIURI)) { michael@0: cspError(aCSPRep, CSPLocalizer.getStr("cspSourceNotURI")); michael@0: return null; michael@0: } michael@0: michael@0: if (!self && enforceSelfChecks) { michael@0: cspError(aCSPRep, CSPLocalizer.getStr("selfDataNotProvided")); michael@0: return null; michael@0: } michael@0: michael@0: if (self && !(self instanceof CSPSource)) { michael@0: self = CSPSource.create(self, aCSPRep, undefined, false); michael@0: } michael@0: michael@0: var sObj = new CSPSource(); michael@0: sObj._self = self; michael@0: sObj._CSPRep = aCSPRep; michael@0: michael@0: // PARSE michael@0: // If 'self' is undefined, then use default port for scheme if there is one. michael@0: michael@0: // grab scheme (if there is one) michael@0: try { michael@0: sObj._scheme = aURI.scheme; michael@0: } catch(e) { michael@0: sObj._scheme = undefined; michael@0: cspError(aCSPRep, CSPLocalizer.getFormatStr("uriWithoutScheme", michael@0: [aURI.asciiSpec])); michael@0: return null; michael@0: } michael@0: michael@0: // grab host (if there is one) michael@0: try { michael@0: // if there's no host, an exception will get thrown michael@0: // (NS_ERROR_FAILURE) michael@0: sObj._host = CSPHost.fromString(aURI.host); michael@0: } catch(e) { michael@0: sObj._host = undefined; michael@0: } michael@0: michael@0: // grab port (if there is one) michael@0: // creating a source from an nsURI is limited in that one cannot specify "*" michael@0: // for port. In fact, there's no way to represent "*" differently than michael@0: // a blank port in an nsURI, since "*" turns into -1, and so does an michael@0: // absence of port declaration. michael@0: michael@0: // port is never inherited from self -- this gets too confusing. michael@0: // Instead, whatever scheme is used (an explicit one or the inherited michael@0: // one) dictates the port if no port is explicitly stated. michael@0: // Set it to undefined here and the default port will be resolved in the michael@0: // getter for .port. michael@0: sObj._port = undefined; michael@0: try { michael@0: // if there's no port, an exception will get thrown michael@0: // (NS_ERROR_FAILURE) michael@0: if (aURI.port > 0) { michael@0: sObj._port = aURI.port; michael@0: } michael@0: } catch(e) { michael@0: sObj._port = undefined; michael@0: } michael@0: michael@0: return sObj; michael@0: }; michael@0: michael@0: /** michael@0: * Factory to create a new CSPSource, parsed from a string. michael@0: * michael@0: * @param aStr michael@0: * string rep of a CSP Source michael@0: * @param aCSPRep michael@0: * the CSPRep this CSPSource belongs to michael@0: * @param self (optional) michael@0: * string, URI, or CSPSource representing the "self" source michael@0: * @param enforceSelfChecks (optional) michael@0: * if present, and "true", will check to be sure "self" has the michael@0: * appropriate values to inherit when they are omitted from aURI. michael@0: * @returns michael@0: * an instance of CSPSource michael@0: */ michael@0: CSPSource.fromString = function(aStr, aCSPRep, self, enforceSelfChecks) { michael@0: if (!aStr) michael@0: return null; michael@0: michael@0: if (!(typeof aStr === 'string')) { michael@0: cspError(aCSPRep, CSPLocalizer.getStr("argumentIsNotString")); michael@0: return null; michael@0: } michael@0: michael@0: var sObj = new CSPSource(); michael@0: sObj._self = self; michael@0: sObj._CSPRep = aCSPRep; michael@0: michael@0: michael@0: // if equal, return does match michael@0: if (aStr === "*") { michael@0: sObj._permitAll = true; michael@0: return sObj; michael@0: } michael@0: michael@0: if (!self && enforceSelfChecks) { michael@0: cspError(aCSPRep, CSPLocalizer.getStr("selfDataNotProvided")); michael@0: return null; michael@0: } michael@0: michael@0: if (self && !(self instanceof CSPSource)) { michael@0: self = CSPSource.create(self, aCSPRep, undefined, false); michael@0: } michael@0: michael@0: // check for 'unsafe-inline' (case insensitive) michael@0: if (aStr.toLowerCase() === "'unsafe-inline'"){ michael@0: sObj._allowUnsafeInline = true; michael@0: return sObj; michael@0: } michael@0: michael@0: // check for 'unsafe-eval' (case insensitive) michael@0: if (aStr.toLowerCase() === "'unsafe-eval'"){ michael@0: sObj._allowUnsafeEval = true; michael@0: return sObj; michael@0: } michael@0: michael@0: // Check for scheme-source match - this only matches if the source michael@0: // string is just a scheme with no host. michael@0: if (R_SCHEMESRC.test(aStr)) { michael@0: var schemeSrcMatch = R_GETSCHEME.exec(aStr); michael@0: sObj._scheme = schemeSrcMatch[0]; michael@0: if (!sObj._host) sObj._host = CSPHost.fromString("*"); michael@0: if (!sObj._port) sObj._port = "*"; michael@0: return sObj; michael@0: } michael@0: michael@0: // check for host-source or ext-host-source match michael@0: if (R_HOSTSRC.test(aStr) || R_EXTHOSTSRC.test(aStr)) { michael@0: var schemeMatch = R_GETSCHEME.exec(aStr); michael@0: // check that the scheme isn't accidentally matching the host. There should michael@0: // be '://' if there is a valid scheme in an (EXT)HOSTSRC michael@0: if (!schemeMatch || aStr.indexOf("://") == -1) { michael@0: sObj._scheme = self.scheme; michael@0: schemeMatch = null; michael@0: } else { michael@0: sObj._scheme = schemeMatch[0]; michael@0: } michael@0: michael@0: // Bug 916054: in CSP 1.0, source-expressions that are paths should have michael@0: // the path after the origin ignored and only the origin enforced. michael@0: if (R_EXTHOSTSRC.test(aStr)) { michael@0: var extHostMatch = R_EXTHOSTSRC.exec(aStr); michael@0: aStr = extHostMatch[1]; michael@0: } michael@0: michael@0: var hostMatch = R_HOSTSRC.exec(aStr); michael@0: if (!hostMatch) { michael@0: cspError(aCSPRep, CSPLocalizer.getFormatStr("couldntParseInvalidSource", michael@0: [aStr])); michael@0: return null; michael@0: } michael@0: // Host regex gets scheme, so remove scheme from aStr. Add 3 for '://' michael@0: if (schemeMatch) { michael@0: hostMatch = R_HOSTSRC.exec(aStr.substring(schemeMatch[0].length + 3)); michael@0: } michael@0: michael@0: var portMatch = R_PORT.exec(hostMatch); michael@0: // Host regex also gets port, so remove the port here. michael@0: if (portMatch) { michael@0: hostMatch = R_HOSTSRC.exec(hostMatch[0].substring(0, hostMatch[0].length - portMatch[0].length)); michael@0: } michael@0: michael@0: sObj._host = CSPHost.fromString(hostMatch[0]); michael@0: if (!portMatch) { michael@0: // gets the default port for the given scheme michael@0: var defPort = Services.io.getProtocolHandler(sObj._scheme).defaultPort; michael@0: if (!defPort) { michael@0: cspError(aCSPRep, michael@0: CSPLocalizer.getFormatStr("couldntParseInvalidSource", michael@0: [aStr])); michael@0: return null; michael@0: } michael@0: sObj._port = defPort; michael@0: } michael@0: else { michael@0: // strip the ':' from the port michael@0: sObj._port = portMatch[0].substr(1); michael@0: } michael@0: // A CSP keyword without quotes is a valid hostname, but this can also be a mistake. michael@0: // Raise a CSP warning in the web console to developer to check his/her intent. michael@0: if (R_QUOTELESS_KEYWORDS.test(aStr)) { michael@0: cspWarn(aCSPRep, CSPLocalizer.getFormatStr("hostNameMightBeKeyword", michael@0: [aStr, aStr.toLowerCase()])); michael@0: } michael@0: return sObj; michael@0: } michael@0: michael@0: // check for a nonce-source match michael@0: if (R_NONCESRC.test(aStr)) { michael@0: return CSPNonceSource.fromString(aStr, aCSPRep); michael@0: } michael@0: michael@0: // check for a hash-source match michael@0: if (R_HASHSRC.test(aStr)) { michael@0: return CSPHashSource.fromString(aStr, aCSPRep); michael@0: } michael@0: michael@0: // check for 'self' (case insensitive) michael@0: if (aStr.toLowerCase() === "'self'") { michael@0: if (!self) { michael@0: cspError(aCSPRep, CSPLocalizer.getStr("selfKeywordNoSelfData")); michael@0: return null; michael@0: } michael@0: sObj._self = self.clone(); michael@0: sObj._isSelf = true; michael@0: return sObj; michael@0: } michael@0: michael@0: cspError(aCSPRep, CSPLocalizer.getFormatStr("couldntParseInvalidSource", michael@0: [aStr])); michael@0: return null; michael@0: }; michael@0: michael@0: CSPSource.validSchemeName = function(aStr) { michael@0: // ::= michael@0: // ::= michael@0: // | michael@0: // ::= | | "+" | "." | "-" michael@0: michael@0: return aStr.match(/^[a-zA-Z][a-zA-Z0-9+.-]*$/); michael@0: }; michael@0: michael@0: CSPSource.prototype = { michael@0: michael@0: get scheme () { michael@0: if (this._isSelf && this._self) michael@0: return this._self.scheme; michael@0: if (!this._scheme && this._self) michael@0: return this._self.scheme; michael@0: return this._scheme; michael@0: }, michael@0: michael@0: get host () { michael@0: if (this._isSelf && this._self) michael@0: return this._self.host; michael@0: if (!this._host && this._self) michael@0: return this._self.host; michael@0: return this._host; michael@0: }, michael@0: michael@0: get permitAll () { michael@0: if (this._isSelf && this._self) michael@0: return this._self.permitAll; michael@0: return this._permitAll; michael@0: }, michael@0: michael@0: /** michael@0: * If this doesn't have a nonstandard port (hard-defined), use the default michael@0: * port for this source's scheme. Should never inherit port from 'self'. michael@0: */ michael@0: get port () { michael@0: if (this._isSelf && this._self) michael@0: return this._self.port; michael@0: if (this._port) return this._port; michael@0: // if no port, get the default port for the scheme michael@0: // (which may be inherited from 'self') michael@0: if (this.scheme) { michael@0: try { michael@0: var port = gIoService.getProtocolHandler(this.scheme).defaultPort; michael@0: if (port > 0) return port; michael@0: } catch(e) { michael@0: // if any errors happen, fail gracefully. michael@0: } michael@0: } michael@0: michael@0: return undefined; michael@0: }, michael@0: michael@0: /** michael@0: * Generates canonical string representation of the Source. michael@0: */ michael@0: toString: michael@0: function() { michael@0: if (this._isSelf) michael@0: return this._self.toString(); michael@0: michael@0: if (this._allowUnsafeInline) michael@0: return "'unsafe-inline'"; michael@0: michael@0: if (this._allowUnsafeEval) michael@0: return "'unsafe-eval'"; michael@0: michael@0: var s = ""; michael@0: if (this.scheme) michael@0: s = s + this.scheme + "://"; michael@0: if (this._host) michael@0: s = s + this._host; michael@0: if (this.port) michael@0: s = s + ":" + this.port; michael@0: return s; michael@0: }, michael@0: michael@0: /** michael@0: * Makes a new deep copy of this object. michael@0: * @returns michael@0: * a new CSPSource michael@0: */ michael@0: clone: michael@0: function() { michael@0: var aClone = new CSPSource(); michael@0: aClone._self = this._self ? this._self.clone() : undefined; michael@0: aClone._scheme = this._scheme; michael@0: aClone._port = this._port; michael@0: aClone._host = this._host ? this._host.clone() : undefined; michael@0: aClone._isSelf = this._isSelf; michael@0: aClone._CSPRep = this._CSPRep; michael@0: return aClone; michael@0: }, michael@0: michael@0: /** michael@0: * Determines if this Source accepts a URI. michael@0: * @param aSource michael@0: * the URI, or CSPSource in question michael@0: * @returns michael@0: * true if the URI matches a source in this source list. michael@0: */ michael@0: permits: michael@0: function(aSource) { michael@0: if (!aSource) return false; michael@0: michael@0: if (!(aSource instanceof CSPSource)) michael@0: aSource = CSPSource.create(aSource, this._CSPRep); michael@0: michael@0: // verify scheme michael@0: if (this.scheme.toLowerCase() != aSource.scheme.toLowerCase()) michael@0: return false; michael@0: michael@0: // port is defined in 'this' (undefined means it may not be relevant michael@0: // to the scheme) AND this port (implicit or explicit) matches michael@0: // aSource's port michael@0: if (this.port && this.port !== "*" && this.port != aSource.port) michael@0: return false; michael@0: michael@0: // host is defined in 'this' (undefined means it may not be relevant michael@0: // to the scheme) AND this host (implicit or explicit) permits michael@0: // aSource's host. michael@0: if (this.host && !this.host.permits(aSource.host)) michael@0: return false; michael@0: michael@0: // all scheme, host and port matched! michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Compares one CSPSource to another. michael@0: * michael@0: * @param that michael@0: * another CSPSource michael@0: * @param resolveSelf (optional) michael@0: * if present, and 'true', implied values are obtained from 'self' michael@0: * instead of assumed to be "anything" michael@0: * @returns michael@0: * true if they have the same data michael@0: */ michael@0: equals: michael@0: function(that, resolveSelf) { michael@0: // 1. schemes match michael@0: // 2. ports match michael@0: // 3. either both hosts are undefined, or one equals the other. michael@0: if (resolveSelf) michael@0: return this.scheme.toLowerCase() === that.scheme.toLowerCase() michael@0: && this.port === that.port michael@0: && (!(this.host || that.host) || michael@0: (this.host && this.host.equals(that.host))); michael@0: michael@0: // otherwise, compare raw (non-self-resolved values) michael@0: return this._scheme.toLowerCase() === that._scheme.toLowerCase() michael@0: && this._port === that._port michael@0: && (!(this._host || that._host) || michael@0: (this._host && this._host.equals(that._host))); michael@0: }, michael@0: michael@0: }; michael@0: michael@0: ////////////////////////////////////////////////////////////////////// michael@0: /** michael@0: * Class to model a host *.x.y. michael@0: */ michael@0: this.CSPHost = function CSPHost() { michael@0: this._segments = []; michael@0: } michael@0: michael@0: /** michael@0: * Factory to create a new CSPHost, parsed from a string. michael@0: * michael@0: * @param aStr michael@0: * string rep of a CSP Host michael@0: * @returns michael@0: * an instance of CSPHost michael@0: */ michael@0: CSPHost.fromString = function(aStr) { michael@0: if (!aStr) return null; michael@0: michael@0: // host string must be LDH with dots and stars. michael@0: var invalidChar = aStr.match(R_INV_HCHAR); michael@0: if (invalidChar) { michael@0: CSPdebug("Invalid character '" + invalidChar + "' in host " + aStr); michael@0: return null; michael@0: } michael@0: michael@0: var hObj = new CSPHost(); michael@0: hObj._segments = aStr.split(/\./); michael@0: if (hObj._segments.length < 1) michael@0: return null; michael@0: michael@0: // validate data in segments michael@0: for (var i in hObj._segments) { michael@0: var seg = hObj._segments[i]; michael@0: if (seg == "*") { michael@0: if (i > 0) { michael@0: // Wildcard must be FIRST michael@0: CSPdebug("Wildcard char located at invalid position in '" + aStr + "'"); michael@0: return null; michael@0: } michael@0: } michael@0: else if (seg.match(R_COMP_HCHAR)) { michael@0: // Non-wildcard segment must be LDH string michael@0: CSPdebug("Invalid segment '" + seg + "' in host value"); michael@0: return null; michael@0: } michael@0: } michael@0: return hObj; michael@0: }; michael@0: michael@0: CSPHost.prototype = { michael@0: /** michael@0: * Generates canonical string representation of the Host. michael@0: */ michael@0: toString: michael@0: function() { michael@0: return this._segments.join("."); michael@0: }, michael@0: michael@0: /** michael@0: * Makes a new deep copy of this object. michael@0: * @returns michael@0: * a new CSPHost michael@0: */ michael@0: clone: michael@0: function() { michael@0: var aHost = new CSPHost(); michael@0: for (var i in this._segments) { michael@0: aHost._segments[i] = this._segments[i]; michael@0: } michael@0: return aHost; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if this host accepts the provided host (or the other way michael@0: * around). michael@0: * @param aHost michael@0: * the FQDN in question (CSPHost or String) michael@0: * @returns michael@0: */ michael@0: permits: michael@0: function(aHost) { michael@0: if (!aHost) { michael@0: aHost = CSPHost.fromString("*"); michael@0: } michael@0: michael@0: if (!(aHost instanceof CSPHost)) { michael@0: // -- compare CSPHost to String michael@0: aHost = CSPHost.fromString(aHost); michael@0: } michael@0: var thislen = this._segments.length; michael@0: var thatlen = aHost._segments.length; michael@0: michael@0: // don't accept a less specific host: michael@0: // \--> *.b.a doesn't accept b.a. michael@0: if (thatlen < thislen) { return false; } michael@0: michael@0: // check for more specific host (and wildcard): michael@0: // \--> *.b.a accepts d.c.b.a. michael@0: // \--> c.b.a doesn't accept d.c.b.a. michael@0: if ((thatlen > thislen) && this._segments[0] != "*") { michael@0: return false; michael@0: } michael@0: michael@0: // Given the wildcard condition (from above), michael@0: // only necessary to compare elements that are present michael@0: // in this host. Extra tokens in aHost are ok. michael@0: // * Compare from right to left. michael@0: for (var i=1; i <= thislen; i++) { michael@0: if (this._segments[thislen-i] != "*" && michael@0: (this._segments[thislen-i].toLowerCase() != michael@0: aHost._segments[thatlen-i].toLowerCase())) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: // at this point, all conditions are met, so the host is allowed michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Compares one CSPHost to another. michael@0: * michael@0: * @param that michael@0: * another CSPHost michael@0: * @returns michael@0: * true if they have the same data michael@0: */ michael@0: equals: michael@0: function(that) { michael@0: if (this._segments.length != that._segments.length) michael@0: return false; michael@0: michael@0: for (var i=0; i