diff -r 000000000000 -r 6474c204b198 content/base/src/contentSecurityPolicy.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/base/src/contentSecurityPolicy.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1049 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +/** + * 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. + */ + +// these identifiers are also defined in contentSecurityPolicy.manifest. +const CSP_CLASS_ID = Components.ID("{d1680bb4-1ac0-4772-9437-1188375e44f2}"); +const CSP_CONTRACT_ID = "@mozilla.org/contentsecuritypolicy;1"; + +/* :::::::: 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"; + +// Needed to support CSP 1.0 spec and our original CSP implementation - should +// be removed when our original implementation is deprecated. +const CSP_TYPE_XMLHTTPREQUEST_SPEC_COMPLIANT = "csp_type_xmlhttprequest_spec_compliant"; +const CSP_TYPE_WEBSOCKET_SPEC_COMPLIANT = "csp_type_websocket_spec_compliant"; + +const WARN_FLAG = Ci.nsIScriptError.warningFlag; +const ERROR_FLAG = Ci.nsIScriptError.ERROR_FLAG; + +const INLINE_STYLE_VIOLATION_OBSERVER_SUBJECT = 'violated base restriction: Inline Stylesheets will not apply'; +const INLINE_SCRIPT_VIOLATION_OBSERVER_SUBJECT = 'violated base restriction: Inline Scripts will not execute'; +const EVAL_VIOLATION_OBSERVER_SUBJECT = 'violated base restriction: Code will not be created from strings'; +const SCRIPT_NONCE_VIOLATION_OBSERVER_SUBJECT = 'Inline Script had invalid nonce' +const STYLE_NONCE_VIOLATION_OBSERVER_SUBJECT = 'Inline Style had invalid nonce' +const SCRIPT_HASH_VIOLATION_OBSERVER_SUBJECT = 'Inline Script had invalid hash'; +const STYLE_HASH_VIOLATION_OBSERVER_SUBJECT = 'Inline Style had invalid hash'; + +// The cutoff length of content location in creating CSP cache key. +const CSP_CACHE_URI_CUTOFF_SIZE = 512; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/CSPUtils.jsm"); + +/* ::::: Policy Parsing & Data structures :::::: */ + +function ContentSecurityPolicy() { + CSPdebug("CSP CREATED"); + this._isInitialized = false; + + this._policies = []; + + this._request = ""; + this._requestOrigin = ""; + this._weakRequestPrincipal = { get : function() { return null; } }; + this._referrer = ""; + this._weakDocRequest = { get : function() { return null; } }; + CSPdebug("CSP object initialized, no policies to enforce yet"); + + this._cache = new Map(); +} + +/* + * Set up mappings from nsIContentPolicy content types to CSP directives. + */ +{ + let cp = Ci.nsIContentPolicy; + let csp = ContentSecurityPolicy; + let cspr_sd_old = CSPRep.SRC_DIRECTIVES_OLD; + let cspr_sd_new = CSPRep.SRC_DIRECTIVES_NEW; + + csp._MAPPINGS=[]; + + /* default, catch-all case */ + // This is the same in old and new CSP so use the new mapping. + csp._MAPPINGS[cp.TYPE_OTHER] = cspr_sd_new.DEFAULT_SRC; + + /* self */ + csp._MAPPINGS[cp.TYPE_DOCUMENT] = null; + + /* shouldn't see this one */ + csp._MAPPINGS[cp.TYPE_REFRESH] = null; + + /* categorized content types */ + // These are the same in old and new CSP's so just use the new mappings. + csp._MAPPINGS[cp.TYPE_SCRIPT] = cspr_sd_new.SCRIPT_SRC; + csp._MAPPINGS[cp.TYPE_IMAGE] = cspr_sd_new.IMG_SRC; + csp._MAPPINGS[cp.TYPE_STYLESHEET] = cspr_sd_new.STYLE_SRC; + csp._MAPPINGS[cp.TYPE_OBJECT] = cspr_sd_new.OBJECT_SRC; + csp._MAPPINGS[cp.TYPE_OBJECT_SUBREQUEST] = cspr_sd_new.OBJECT_SRC; + csp._MAPPINGS[cp.TYPE_SUBDOCUMENT] = cspr_sd_new.FRAME_SRC; + csp._MAPPINGS[cp.TYPE_MEDIA] = cspr_sd_new.MEDIA_SRC; + csp._MAPPINGS[cp.TYPE_FONT] = cspr_sd_new.FONT_SRC; + csp._MAPPINGS[cp.TYPE_XSLT] = cspr_sd_new.SCRIPT_SRC; + csp._MAPPINGS[cp.TYPE_BEACON] = cspr_sd_new.CONNECT_SRC; + + /* Our original CSP implementation's mappings for XHR and websocket + * These should be changed to be = cspr_sd.CONNECT_SRC when we remove + * the original implementation - NOTE: order in this array is important !!! + */ + csp._MAPPINGS[cp.TYPE_XMLHTTPREQUEST] = cspr_sd_old.XHR_SRC; + csp._MAPPINGS[cp.TYPE_WEBSOCKET] = cspr_sd_old.XHR_SRC; + + /* CSP cannot block CSP reports */ + csp._MAPPINGS[cp.TYPE_CSP_REPORT] = null; + + /* These must go through the catch-all */ + csp._MAPPINGS[cp.TYPE_XBL] = cspr_sd_new.DEFAULT_SRC; + csp._MAPPINGS[cp.TYPE_PING] = cspr_sd_new.DEFAULT_SRC; + csp._MAPPINGS[cp.TYPE_DTD] = cspr_sd_new.DEFAULT_SRC; + + /* CSP 1.0 spec compliant mappings for XHR and websocket */ + // The directive name for XHR, websocket, and EventSource is different + // in the 1.0 spec than in our original implementation, these mappings + // address this. These won't be needed when we deprecate our original + // implementation. + csp._MAPPINGS[CSP_TYPE_XMLHTTPREQUEST_SPEC_COMPLIANT] = cspr_sd_new.CONNECT_SRC; + csp._MAPPINGS[CSP_TYPE_WEBSOCKET_SPEC_COMPLIANT] = cspr_sd_new.CONNECT_SRC; + // TODO : EventSource will be here and also will use connect-src + // after we fix Bug 802872 - CSP should restrict EventSource using the connect-src + // directive. For background see Bug 667490 - EventSource should use the same + // nsIContentPolicy type as XHR (which is fixed) +} + +ContentSecurityPolicy.prototype = { + classID: CSP_CLASS_ID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentSecurityPolicy, + Ci.nsISerializable, + Ci.nsISupports]), + + // Class info is required to be able to serialize + classInfo: XPCOMUtils.generateCI({classID: CSP_CLASS_ID, + contractID: CSP_CONTRACT_ID, + interfaces: [Ci.nsIContentSecurityPolicy, + Ci.nsISerializable], + flags: Ci.nsIClassInfo.MAIN_THREAD_ONLY}), + + get isInitialized() { + return this._isInitialized; + }, + + set isInitialized (foo) { + this._isInitialized = foo; + }, + + _getPolicyInternal: function(index) { + if (index < 0 || index >= this._policies.length) { + throw Cr.NS_ERROR_FAILURE; + } + return this._policies[index]; + }, + + _buildViolatedDirectiveString: + function(aDirectiveName, aPolicy) { + var SD = CSPRep.SRC_DIRECTIVES_NEW; + var cspContext = (SD[aDirectiveName] in aPolicy._directives) ? SD[aDirectiveName] : SD.DEFAULT_SRC; + var directive = aPolicy._directives[cspContext]; + return cspContext + ' ' + directive.toString(); + }, + + /** + * Returns policy string representing the policy at "index". + */ + getPolicy: function(index) { + return this._getPolicyInternal(index).toString(); + }, + + /** + * Returns count of policies. + */ + get numPolicies() { + return this._policies.length; + }, + + getAllowsInlineScript: function(shouldReportViolations) { + // report it? (for each policy, is it violated?) + shouldReportViolations.value = this._policies.some(function(a) { return !a.allowsInlineScripts; }); + + // allow it to execute? (Do all the policies allow it to execute)? + return this._policies.every(function(a) { + return a._reportOnlyMode || a.allowsInlineScripts; + }); + }, + + getAllowsEval: function(shouldReportViolations) { + // report it? (for each policy, is it violated?) + shouldReportViolations.value = this._policies.some(function(a) { return !a.allowsEvalInScripts; }); + + // allow it to execute? (Do all the policies allow it to execute)? + return this._policies.every(function(a) { + return a._reportOnlyMode || a.allowsEvalInScripts; + }); + }, + + getAllowsInlineStyle: function(shouldReportViolations) { + // report it? (for each policy, is it violated?) + shouldReportViolations.value = this._policies.some(function(a) { return !a.allowsInlineStyles; }); + + // allow it to execute? (Do all the policies allow it to execute)? + return this._policies.every(function(a) { + return a._reportOnlyMode || a.allowsInlineStyles; + }); + }, + + getAllowsNonce: function(aNonce, aContentType, shouldReportViolation) { + if (!(aContentType == Ci.nsIContentPolicy.TYPE_SCRIPT || + aContentType == Ci.nsIContentPolicy.TYPE_STYLESHEET)) { + CSPdebug("Nonce check requested for an invalid content type (not script or style): " + aContentType); + return false; + } + + let directive = ContentSecurityPolicy._MAPPINGS[aContentType]; + let policyAllowsNonce = [ policy.permitsNonce(aNonce, directive) + for (policy of this._policies) ]; + + shouldReportViolation.value = this._policies.some(function(policy, i) { + // Don't report a violation if the policy didn't use nonce-source + return policy._directives.hasOwnProperty(directive) && + policy._directives[directive]._hasNonceSource && + !policyAllowsNonce[i]; + }); + + // allow it to execute? (Do all the policies allow it to execute)? + return this._policies.every(function(policy, i) { + return policy._reportOnlyMode || policyAllowsNonce[i]; + }); + }, + + getAllowsHash: function(aContent, aContentType, shouldReportViolation) { + if (!(aContentType == Ci.nsIContentPolicy.TYPE_SCRIPT || + aContentType == Ci.nsIContentPolicy.TYPE_STYLESHEET)) { + CSPdebug("Hash check requested for an invalid content type (not script or style): " + aContentType); + return false; + } + + let directive = ContentSecurityPolicy._MAPPINGS[aContentType]; + let policyAllowsHash = [ policy.permitsHash(aContent, directive) + for (policy of this._policies) ]; + + shouldReportViolation.value = this._policies.some(function(policy, i) { + // Don't report a violation if the policy didn't use hash-source + return policy._directives.hasOwnProperty(directive) && + policy._directives[directive]._hasHashSource && + !policyAllowsHash[i]; + }); + + // allow it to execute? (Do all the policies allow it to execute)? + return this._policies.every(function(policy, i) { + return policy._reportOnlyMode || policyAllowsHash[i]; + }); + }, + + /** + * For each policy, log any violation on the Error Console and send a report + * if a report-uri is present in the policy + * + * @param aViolationType + * one of the VIOLATION_TYPE_* constants, e.g. inline-script or eval + * @param aSourceFile + * name of the source file containing the violation (if available) + * @param aContentSample + * sample of the violating content (to aid debugging) + * @param aLineNum + * source line number of the violation (if available) + * @param aNonce + * (optional) If this is a nonce violation, include the nonce so we can + * recheck to determine which policies were violated and send the + * appropriate reports. + * @param aContent + * (optional) If this is a hash violation, include contents of the inline + * resource in the question so we can recheck the hash in order to + * determine which policies were violated and send the appropriate + * reports. + */ + logViolationDetails: + function(aViolationType, aSourceFile, aScriptSample, aLineNum, aNonce, aContent) { + for (let policyIndex=0; policyIndex < this._policies.length; policyIndex++) { + let policy = this._policies[policyIndex]; + + // call-sites to the eval/inline checks recieve two return values: allows + // and violates. Policies that are report-only allow the + // loads/compilations but violations should still be reported. Not all + // policies in this nsIContentSecurityPolicy instance will be violated, + // which is why we must check again here. + switch (aViolationType) { + case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_STYLE: + if (!policy.allowsInlineStyles) { + var violatedDirective = this._buildViolatedDirectiveString('STYLE_SRC', policy); + this._asyncReportViolation('self', null, violatedDirective, policyIndex, + INLINE_STYLE_VIOLATION_OBSERVER_SUBJECT, + aSourceFile, aScriptSample, aLineNum); + } + break; + case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_SCRIPT: + if (!policy.allowsInlineScripts) { + var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); + this._asyncReportViolation('self', null, violatedDirective, policyIndex, + INLINE_SCRIPT_VIOLATION_OBSERVER_SUBJECT, + aSourceFile, aScriptSample, aLineNum); + } + break; + case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL: + if (!policy.allowsEvalInScripts) { + var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); + this._asyncReportViolation('self', null, violatedDirective, policyIndex, + EVAL_VIOLATION_OBSERVER_SUBJECT, + aSourceFile, aScriptSample, aLineNum); + } + break; + case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_NONCE_SCRIPT: + var scriptType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_SCRIPT]; + if (!policy.permitsNonce(aNonce, scriptType)) { + var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); + this._asyncReportViolation('self', null, violatedDirective, policyIndex, + SCRIPT_NONCE_VIOLATION_OBSERVER_SUBJECT, + aSourceFile, aScriptSample, aLineNum); + } + break; + case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_NONCE_STYLE: + var styleType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_STYLE]; + if (!policy.permitsNonce(aNonce, styleType)) { + var violatedDirective = this._buildViolatedDirectiveString('STYLE_SRC', policy); + this._asyncReportViolation('self', null, violatedDirective, policyIndex, + STYLE_NONCE_VIOLATION_OBSERVER_SUBJECT, + aSourceFile, aScriptSample, aLineNum); + } + break; + case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_HASH_SCRIPT: + var scriptType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_SCRIPT]; + if (!policy.permitsHash(aContent, scriptType)) { + var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); + this._asyncReportViolation('self', null, violatedDirective, policyIndex, + SCRIPT_HASH_VIOLATION_OBSERVER_SUBJECT, + aSourceFile, aScriptSample, aLineNum); + } + break; + case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_HASH_STYLE: + var styleType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_STYLE]; + if (!policy.permitsHash(aContent, styleType)) { + var violatedDirective = this._buildViolatedDirectiveString('STYLE_SRC', policy); + this._asyncReportViolation('self', null, violatedDirective, policyIndex, + STYLE_HASH_VIOLATION_OBSERVER_SUBJECT, + aSourceFile, aScriptSample, aLineNum); + } + break; + } + } + }, + + /** + * Given an nsIHttpChannel, fill out the appropriate data. + */ + setRequestContext: + function(aSelfURI, aReferrerURI, aPrincipal, aChannel) { + + // this requires either a self URI or a http channel. + if (!aSelfURI && !aChannel) + return; + + if (aChannel) { + // Save the docRequest for fetching a policy-uri + this._weakDocRequest = Cu.getWeakReference(aChannel); + } + + // save the document URI (minus ) and referrer for reporting + let uri = aSelfURI ? aSelfURI.cloneIgnoringRef() : aChannel.URI.cloneIgnoringRef(); + try { // GetUserPass throws for some protocols without userPass + uri.userPass = ''; + } catch (ex) {} + this._request = uri.asciiSpec; + this._requestOrigin = uri; + + // store a reference to the principal, that can later be used in shouldLoad + if (aPrincipal) { + this._weakRequestPrincipal = Cu.getWeakReference(aPrincipal); + } else if (aChannel) { + this._weakRequestPrincipal = Cu.getWeakReference(Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager) + .getChannelPrincipal(aChannel)); + } else { + CSPdebug("No principal or channel for document context; violation reports may not work."); + } + + // pick one: referrerURI, channel's referrer, or null (first available) + let ref = null; + if (aReferrerURI) + ref = aReferrerURI; + else if (aChannel instanceof Ci.nsIHttpChannel) + ref = aChannel.referrer; + + if (ref) { + let referrer = aChannel.referrer.cloneIgnoringRef(); + try { // GetUserPass throws for some protocols without userPass + referrer.userPass = ''; + } catch (ex) {} + this._referrer = referrer.asciiSpec; + } + }, + +/* ........ Methods .............. */ + + /** + * Adds a new policy to our list of policies for this CSP context. + * @returns the count of policies. + */ + appendPolicy: + function csp_appendPolicy(aPolicy, selfURI, aReportOnly, aSpecCompliant) { + return this._appendPolicyInternal(aPolicy, selfURI, aReportOnly, + aSpecCompliant, true); + }, + + /** + * Adds a new policy to our list of policies for this CSP context. + * Only to be called from this module (not exported) + * @returns the count of policies. + */ + _appendPolicyInternal: + function csp_appendPolicy(aPolicy, selfURI, aReportOnly, aSpecCompliant, + aEnforceSelfChecks) { +#ifndef MOZ_B2G + CSPdebug("APPENDING POLICY: " + aPolicy); + CSPdebug(" SELF: " + (selfURI ? selfURI.asciiSpec : " null")); + CSPdebug("CSP 1.0 COMPLIANT : " + aSpecCompliant); +#endif + + // For nested schemes such as view-source: make sure we are taking the + // innermost URI to use as 'self' since that's where we will extract the + // scheme, host and port from + if (selfURI instanceof Ci.nsINestedURI) { +#ifndef MOZ_B2G + CSPdebug(" INNER: " + selfURI.innermostURI.asciiSpec); +#endif + selfURI = selfURI.innermostURI; + } + + // stay uninitialized until policy setup is done + var newpolicy; + + // If there is a policy-uri, fetch the policy, then re-call this function. + // (1) parse and create a CSPRep object + // Note that we pass the full URI since when it's parsed as 'self' to construct a + // CSPSource only the scheme, host, and port are kept. + + // If we want to be CSP 1.0 spec compliant, use the new parser. + // The old one will be deprecated in the future and will be + // removed at that time. + if (aSpecCompliant) { + newpolicy = CSPRep.fromStringSpecCompliant(aPolicy, + selfURI, + aReportOnly, + this._weakDocRequest.get(), + this, + aEnforceSelfChecks); + } else { + newpolicy = CSPRep.fromString(aPolicy, + selfURI, + aReportOnly, + this._weakDocRequest.get(), + this, + aEnforceSelfChecks); + } + + newpolicy._specCompliant = !!aSpecCompliant; + newpolicy._isInitialized = true; + this._policies.push(newpolicy); + this._cache.clear(); // reset cache since effective policy changes + }, + + /** + * Removes a policy from the array of policies. + */ + removePolicy: + function csp_removePolicy(index) { + if (index < 0 || index >= this._policies.length) { + CSPdebug("Cannot remove policy " + index + "; not enough policies."); + return; + } + this._policies.splice(index, 1); + this._cache.clear(); // reset cache since effective policy changes + }, + + /** + * Generates and sends a violation report to the specified report URIs. + */ + sendReports: + function(blockedUri, originalUri, violatedDirective, + violatedPolicyIndex, aSourceFile, + aScriptSample, aLineNum) { + + let policy = this._getPolicyInternal(violatedPolicyIndex); + if (!policy) { + CSPdebug("ERROR in SendReports: policy " + violatedPolicyIndex + " is not defined."); + return; + } + + var uriString = policy.getReportURIs(); + var uris = uriString.split(/\s+/); + if (uris.length > 0) { + // see if we need to sanitize the blocked-uri + let blocked = ''; + if (originalUri) { + // We've redirected, only report the blocked origin + try { + let clone = blockedUri.clone(); + clone.path = ''; + blocked = clone.asciiSpec; + } catch(e) { + CSPdebug(".... blockedUri can't be cloned: " + blockedUri); + } + } + else if (blockedUri instanceof Ci.nsIURI) { + blocked = blockedUri.cloneIgnoringRef().asciiSpec; + } + else { + // blockedUri is a string for eval/inline-script violations + blocked = blockedUri; + } + + // Generate report to send composed of + // { + // csp-report: { + // document-uri: "http://example.com/file.html?params", + // referrer: "...", + // blocked-uri: "...", + // violated-directive: "..." + // } + // } + var report = { + 'csp-report': { + 'document-uri': this._request, + 'referrer': this._referrer, + 'blocked-uri': blocked, + 'violated-directive': violatedDirective + } + } + + // extra report fields for script errors (if available) + if (originalUri) + report["csp-report"]["original-uri"] = originalUri.cloneIgnoringRef().asciiSpec; + if (aSourceFile) + report["csp-report"]["source-file"] = aSourceFile; + if (aScriptSample) + report["csp-report"]["script-sample"] = aScriptSample; + if (aLineNum) + report["csp-report"]["line-number"] = aLineNum; + + var reportString = JSON.stringify(report); + CSPdebug("Constructed violation report:\n" + reportString); + + // For each URI in the report list, send out a report. + // We make the assumption that all of the URIs are absolute URIs; this + // should be taken care of in CSPRep.fromString (where it converts any + // relative URIs into absolute ones based on "self"). + for (let i in uris) { + if (uris[i] === "") + continue; + + try { + var chan = Services.io.newChannel(uris[i], null, null); + if (!chan) { + CSPdebug("Error creating channel for " + uris[i]); + continue; + } + + var content = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + content.data = reportString + "\n\n"; + + // make sure this is an anonymous request (no cookies) so in case the + // policy URI is injected, it can't be abused for CSRF. + chan.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS; + + // we need to set an nsIChannelEventSink on the channel object + // so we can tell it to not follow redirects when posting the reports + chan.notificationCallbacks = new CSPReportRedirectSink(policy); + if (this._weakDocRequest.get()) { + chan.loadGroup = this._weakDocRequest.get().loadGroup; + } + + chan.QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(content, "application/json", content.available()); + + try { + // if this is an HTTP channel, set the request method to post + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "POST"; + } catch(e) {} // throws only if chan is not an nsIHttpChannel. + + // check with the content policy service to see if we're allowed to + // send this request. + try { + var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"] + .getService(Ci.nsIContentPolicy); + if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_CSP_REPORT, + chan.URI, this._requestOrigin, + null, null, null, this._weakRequestPrincipal.get()) + != Ci.nsIContentPolicy.ACCEPT) { + continue; // skip unauthorized URIs + } + } catch(e) { + continue; // refuse to load if we can't do a security check. + } + + //send data (and set up error notifications) + chan.asyncOpen(new CSPViolationReportListener(uris[i]), null); + 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. + policy.log(WARN_FLAG, CSPLocalizer.getFormatStr("triedToSendReport", [uris[i]])); + policy.log(WARN_FLAG, CSPLocalizer.getFormatStr("errorWas", [e.toString()])); + } + } + } + }, + + /** + * Logs a meaningful CSP warning to the developer console. + */ + logToConsole: + function(blockedUri, originalUri, violatedDirective, aViolatedPolicyIndex, + aSourceFile, aScriptSample, aLineNum, aObserverSubject) { + let policy = this._policies[aViolatedPolicyIndex]; + switch(aObserverSubject.data) { + case INLINE_STYLE_VIOLATION_OBSERVER_SUBJECT: + violatedDirective = CSPLocalizer.getStr("inlineStyleBlocked"); + break; + case INLINE_SCRIPT_VIOLATION_OBSERVER_SUBJECT: + violatedDirective = CSPLocalizer.getStr("inlineScriptBlocked"); + break; + case EVAL_VIOLATION_OBSERVER_SUBJECT: + violatedDirective = CSPLocalizer.getStr("scriptFromStringBlocked"); + break; + } + var violationMessage = null; + if (blockedUri["asciiSpec"]) { + violationMessage = CSPLocalizer.getFormatStr("CSPViolationWithURI", [violatedDirective, blockedUri.asciiSpec]); + } else { + violationMessage = CSPLocalizer.getFormatStr("CSPViolation", [violatedDirective]); + } + policy.log(WARN_FLAG, violationMessage, + (aSourceFile) ? aSourceFile : null, + (aScriptSample) ? decodeURIComponent(aScriptSample) : null, + (aLineNum) ? aLineNum : null); + }, + +/** + * Exposed Method to analyze docShell for approved frame ancestry. + * NOTE: 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 and the load + * should progress. + */ + permitsAncestry: + function(docShell) { + // Cannot shortcut checking all the policies since violation reports have + // to be triggered if any policy wants it. + var permitted = true; + for (let i = 0; i < this._policies.length; i++) { + if (!this._permitsAncestryInternal(docShell, this._policies[i], i)) { + permitted = false; + } + } + return permitted; + }, + + _permitsAncestryInternal: + function(docShell, policy, policyIndex) { + if (!docShell) { return false; } + + // 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; + } + // delete any userpass + let ancestor = it.currentURI.cloneIgnoringRef(); + try { // GetUserPass throws for some protocols without userPass + ancestor.userPass = ''; + } catch (ex) {} + +#ifndef MOZ_B2G + CSPdebug(" found frame ancestor " + ancestor.asciiSpec); +#endif + ancestors.push(ancestor); + } + } + + // scan the discovered ancestors + // frame-ancestors is the same in both old and new source directives, + // so don't need to differentiate here. + let cspContext = CSPRep.SRC_DIRECTIVES_NEW.FRAME_ANCESTORS; + for (let i in ancestors) { + let ancestor = ancestors[i]; + if (!policy.permits(ancestor, cspContext)) { + // report the frame-ancestor violation + let directive = policy._directives[cspContext]; + let violatedPolicy = 'frame-ancestors ' + directive.toString(); + + this._asyncReportViolation(ancestors[i], null, violatedPolicy, + policyIndex); + + // need to lie if we are testing in report-only mode + return policy._reportOnlyMode; + } + } + return true; + }, + + /** + * Creates a cache key from content location and content type. + */ + _createCacheKey: + function (aContentLocation, aContentType) { + if (aContentType != Ci.nsIContentPolicy.TYPE_SCRIPT && + aContentLocation.scheme == "data") { + // For non-script data: URI, use ("data:", aContentType) as the cache key. + return aContentLocation.scheme + ":" + aContentType; + } + + let uri = aContentLocation.spec; + if (uri.length > CSP_CACHE_URI_CUTOFF_SIZE) { + // Don't cache for a URI longer than the cutoff size. + return null; + } + return uri + "!" + aContentType; + }, + + /** + * 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, + aOriginalUri) { + let key = this._createCacheKey(aContentLocation, aContentType); + if (key && this._cache.has(key)) { + return this._cache.get(key); + } + +#ifndef MOZ_B2G + // Try to remove as much as possible from the hot path on b2g. + CSPdebug("shouldLoad location = " + aContentLocation.asciiSpec); + CSPdebug("shouldLoad content type = " + aContentType); +#endif + + // The mapping for XHR and websockets is different between our original + // implementation and the 1.0 spec, we handle this here. + var cspContext; + + let cp = Ci.nsIContentPolicy; + + // Infer if this is a preload for elements that use nonce-source. Since, + // for preloads, aContext is the document and not the element associated + // with the resource, we cannot determine the nonce. See Bug 612921 and + // Bug 855326. + let nonceSourceValid = aContentType == cp.TYPE_SCRIPT || + aContentType == cp.TYPE_STYLESHEET; + var possiblePreloadNonceConflict = nonceSourceValid && + aContext instanceof Ci.nsIDOMHTMLDocument; + + // iterate through all the _policies and send reports where a policy is + // violated. After the check, determine the overall effect (blocked or + // loaded?) and cache it. + let policyAllowsLoadArray = []; + for (let policyIndex=0; policyIndex < this._policies.length; policyIndex++) { + let policy = this._policies[policyIndex]; + +#ifndef MOZ_B2G + CSPdebug("policy is " + (policy._specCompliant ? + "1.0 compliant" : "pre-1.0")); + CSPdebug("policy is " + (policy._reportOnlyMode ? + "report-only" : "blocking")); +#endif + + if (aContentType == cp.TYPE_XMLHTTPREQUEST && this._policies[policyIndex]._specCompliant) { + cspContext = ContentSecurityPolicy._MAPPINGS[CSP_TYPE_XMLHTTPREQUEST_SPEC_COMPLIANT]; + } else if (aContentType == cp.TYPE_WEBSOCKET && this._policies[policyIndex]._specCompliant) { + cspContext = ContentSecurityPolicy._MAPPINGS[CSP_TYPE_WEBSOCKET_SPEC_COMPLIANT]; + } else { + cspContext = ContentSecurityPolicy._MAPPINGS[aContentType]; + } + +#ifndef MOZ_B2G + CSPdebug("shouldLoad cspContext = " + cspContext); +#endif + + // if the mapping is null, there's no policy, let it through. + if (!cspContext) { + return Ci.nsIContentPolicy.ACCEPT; + } + + // check if location is permitted + let permitted = policy.permits(aContentLocation, cspContext); + + // check any valid content type for nonce if location is not permitted + if (!permitted && nonceSourceValid && + aContext instanceof Ci.nsIDOMHTMLElement && + aContext.hasAttribute('nonce')) { + permitted = policy.permitsNonce(aContext.getAttribute('nonce'), + cspContext); + } + + // record whether the thing should be blocked or just reported. + policyAllowsLoadArray.push(permitted || policy._reportOnlyMode); + let res = permitted ? cp.ACCEPT : cp.REJECT_SERVER; + + // frame-ancestors is taken care of early on (as this document is loaded) + + // If the result is *NOT* ACCEPT, then send report + // Do not send report if this is a nonce-source preload - the decision may + // be wrong and will incorrectly fail the unit tests. + if (res != cp.ACCEPT && !possiblePreloadNonceConflict) { + CSPdebug("blocking request for " + aContentLocation.asciiSpec); + try { + let directive = "unknown directive", + violatedPolicy = "unknown policy"; + + // The policy might not explicitly declare each source directive (so + // the cspContext may be implicit). If so, we have to report + // violations as appropriate: specific or the default-src directive. + if (policy._directives.hasOwnProperty(cspContext)) { + directive = policy._directives[cspContext]; + violatedPolicy = cspContext + ' ' + directive.toString(); + } else if (policy._directives.hasOwnProperty("default-src")) { + directive = policy._directives["default-src"]; + violatedPolicy = "default-src " + directive.toString(); + } else { + violatedPolicy = "unknown directive"; + CSPdebug('ERROR in blocking content: ' + + 'CSP is not sure which part of the policy caused this block'); + } + + this._asyncReportViolation(aContentLocation, + aOriginalUri, + violatedPolicy, + policyIndex); + } catch(e) { + CSPdebug('---------------- ERROR: ' + e); + } + } + } // end for-each loop over policies + + // the ultimate decision is based on whether any policies want to reject + // the load. The array keeps track of whether the policies allowed the + // loads. If any doesn't, we'll reject the load (and cache the result). + let ret = (policyAllowsLoadArray.some(function(a,b) { return !a; }) ? + cp.REJECT_SERVER : cp.ACCEPT); + + // Do not cache the result if this is a nonce-source preload + if (key && !possiblePreloadNonceConflict) { + this._cache.set(key, ret); + } + return ret; + }, + + 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; + }, + + /** + * Asynchronously notifies any nsIObservers listening to the CSP violation + * topic that a violation occurred. Also triggers report sending. All + * asynchronous on the main thread. + * + * @param aBlockedContentSource + * Either a CSP Source (like 'self', as string) or nsIURI: the source + * of the violation. + * @param aOriginalUri + * The original URI if the blocked content is a redirect, else null + * @param aViolatedDirective + * the directive that was violated (string). + * @param aViolatedPolicyIndex + * the index of the policy that was violated (so we know where to send + * the reports). + * @param aObserverSubject + * optional, subject sent to the nsIObservers listening to the CSP + * violation topic. + * @param aSourceFile + * name of the file containing the inline script violation + * @param aScriptSample + * a sample of the violating inline script + * @param aLineNum + * source line number of the violation (if available) + */ + _asyncReportViolation: + function(aBlockedContentSource, aOriginalUri, aViolatedDirective, + aViolatedPolicyIndex, aObserverSubject, + aSourceFile, aScriptSample, aLineNum) { + // if optional observerSubject isn't specified, default to the source of + // the violation. + if (!aObserverSubject) + aObserverSubject = aBlockedContentSource; + + // gotta wrap things that aren't nsISupports, since it's sent out to + // observers as such. Objects that are not nsISupports are converted to + // strings and then wrapped into a nsISupportsCString. + if (!(aObserverSubject instanceof Ci.nsISupports)) { + let d = aObserverSubject; + aObserverSubject = Cc["@mozilla.org/supports-cstring;1"] + .createInstance(Ci.nsISupportsCString); + aObserverSubject.data = d; + } + + var reportSender = this; + Services.tm.mainThread.dispatch( + function() { + Services.obs.notifyObservers(aObserverSubject, + CSP_VIOLATION_TOPIC, + aViolatedDirective); + reportSender.sendReports(aBlockedContentSource, aOriginalUri, + aViolatedDirective, aViolatedPolicyIndex, + aSourceFile, aScriptSample, aLineNum); + reportSender.logToConsole(aBlockedContentSource, aOriginalUri, + aViolatedDirective, aViolatedPolicyIndex, + aSourceFile, aScriptSample, + aLineNum, aObserverSubject); + + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + +/* ........ nsISerializable Methods: .............. */ + + read: + function(aStream) { + + this._requestOrigin = aStream.readObject(true); + this._requestOrigin.QueryInterface(Ci.nsIURI); + + for (let pCount = aStream.read32(); pCount > 0; pCount--) { + let polStr = aStream.readString(); + let reportOnly = aStream.readBoolean(); + let specCompliant = aStream.readBoolean(); + // don't need self info because when the policy is turned back into a + // string, 'self' is replaced with the explicit source expression. + this._appendPolicyInternal(polStr, null, reportOnly, specCompliant, + false); + } + + // NOTE: the document instance that's deserializing this object (via its + // principal) should hook itself into this._principal manually. If they + // don't, the CSP reports will likely be blocked by nsMixedContentBlocker. + }, + + write: + function(aStream) { + // need to serialize the context: request origin and such. They are + // used when sending reports. Since _request and _requestOrigin are just + // different representations of the same thing, only save _requestOrigin + // (an nsIURI). + aStream.writeCompoundObject(this._requestOrigin, Ci.nsIURI, true); + + // we can't serialize a reference to the principal that triggered this + // instance to serialize, so when this is deserialized by the principal the + // caller must hook it up manually by calling setRequestContext on this + // instance with the appropriate nsIChannel. + + // Finally, serialize all the policies. + aStream.write32(this._policies.length); + + for each (var policy in this._policies) { + aStream.writeWStringZ(policy.toString()); + aStream.writeBoolean(policy._reportOnlyMode); + aStream.writeBoolean(policy._specCompliant); + } + }, +}; + +// The POST of the violation report (if it happens) should not follow +// redirects, per the spec. hence, we implement an nsIChannelEventSink +// with an object so we can tell XHR to abort if a redirect happens. +function CSPReportRedirectSink(policy) { + this._policy = policy; +} + +CSPReportRedirectSink.prototype = { + QueryInterface: function requestor_qi(iid) { + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIInterfaceRequestor) || + iid.equals(Ci.nsIChannelEventSink)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + // nsIInterfaceRequestor + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + // nsIChannelEventSink + asyncOnChannelRedirect: function channel_redirect(oldChannel, newChannel, + flags, callback) { + this._policy.log(WARN_FLAG, CSPLocalizer.getFormatStr("reportPostRedirect", [oldChannel.URI.asciiSpec])); + + // cancel the old channel so XHR failure callback happens + oldChannel.cancel(Cr.NS_ERROR_ABORT); + + // notify an observer that we have blocked the report POST due to a redirect, + // used in testing, do this async since we're in an async call now to begin with + Services.tm.mainThread.dispatch( + function() { + observerSubject = Cc["@mozilla.org/supports-cstring;1"] + .createInstance(Ci.nsISupportsCString); + observerSubject.data = oldChannel.URI.asciiSpec; + + Services.obs.notifyObservers(observerSubject, + CSP_VIOLATION_TOPIC, + "denied redirect while sending violation report"); + }, Ci.nsIThread.DISPATCH_NORMAL); + + // throw to stop the redirect happening + throw Cr.NS_BINDING_REDIRECTED; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentSecurityPolicy]);