Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | |
michael@0 | 6 | /** |
michael@0 | 7 | * Content Security Policy |
michael@0 | 8 | * |
michael@0 | 9 | * Overview |
michael@0 | 10 | * This is a stub component that will be fleshed out to do all the fancy stuff |
michael@0 | 11 | * that ContentSecurityPolicy has to do. |
michael@0 | 12 | */ |
michael@0 | 13 | |
michael@0 | 14 | // these identifiers are also defined in contentSecurityPolicy.manifest. |
michael@0 | 15 | const CSP_CLASS_ID = Components.ID("{d1680bb4-1ac0-4772-9437-1188375e44f2}"); |
michael@0 | 16 | const CSP_CONTRACT_ID = "@mozilla.org/contentsecuritypolicy;1"; |
michael@0 | 17 | |
michael@0 | 18 | /* :::::::: Constants and Helpers ::::::::::::::: */ |
michael@0 | 19 | |
michael@0 | 20 | const Cc = Components.classes; |
michael@0 | 21 | const Ci = Components.interfaces; |
michael@0 | 22 | const Cr = Components.results; |
michael@0 | 23 | const Cu = Components.utils; |
michael@0 | 24 | |
michael@0 | 25 | const CSP_VIOLATION_TOPIC = "csp-on-violate-policy"; |
michael@0 | 26 | |
michael@0 | 27 | // Needed to support CSP 1.0 spec and our original CSP implementation - should |
michael@0 | 28 | // be removed when our original implementation is deprecated. |
michael@0 | 29 | const CSP_TYPE_XMLHTTPREQUEST_SPEC_COMPLIANT = "csp_type_xmlhttprequest_spec_compliant"; |
michael@0 | 30 | const CSP_TYPE_WEBSOCKET_SPEC_COMPLIANT = "csp_type_websocket_spec_compliant"; |
michael@0 | 31 | |
michael@0 | 32 | const WARN_FLAG = Ci.nsIScriptError.warningFlag; |
michael@0 | 33 | const ERROR_FLAG = Ci.nsIScriptError.ERROR_FLAG; |
michael@0 | 34 | |
michael@0 | 35 | const INLINE_STYLE_VIOLATION_OBSERVER_SUBJECT = 'violated base restriction: Inline Stylesheets will not apply'; |
michael@0 | 36 | const INLINE_SCRIPT_VIOLATION_OBSERVER_SUBJECT = 'violated base restriction: Inline Scripts will not execute'; |
michael@0 | 37 | const EVAL_VIOLATION_OBSERVER_SUBJECT = 'violated base restriction: Code will not be created from strings'; |
michael@0 | 38 | const SCRIPT_NONCE_VIOLATION_OBSERVER_SUBJECT = 'Inline Script had invalid nonce' |
michael@0 | 39 | const STYLE_NONCE_VIOLATION_OBSERVER_SUBJECT = 'Inline Style had invalid nonce' |
michael@0 | 40 | const SCRIPT_HASH_VIOLATION_OBSERVER_SUBJECT = 'Inline Script had invalid hash'; |
michael@0 | 41 | const STYLE_HASH_VIOLATION_OBSERVER_SUBJECT = 'Inline Style had invalid hash'; |
michael@0 | 42 | |
michael@0 | 43 | // The cutoff length of content location in creating CSP cache key. |
michael@0 | 44 | const CSP_CACHE_URI_CUTOFF_SIZE = 512; |
michael@0 | 45 | |
michael@0 | 46 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 47 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 48 | Cu.import("resource://gre/modules/CSPUtils.jsm"); |
michael@0 | 49 | |
michael@0 | 50 | /* ::::: Policy Parsing & Data structures :::::: */ |
michael@0 | 51 | |
michael@0 | 52 | function ContentSecurityPolicy() { |
michael@0 | 53 | CSPdebug("CSP CREATED"); |
michael@0 | 54 | this._isInitialized = false; |
michael@0 | 55 | |
michael@0 | 56 | this._policies = []; |
michael@0 | 57 | |
michael@0 | 58 | this._request = ""; |
michael@0 | 59 | this._requestOrigin = ""; |
michael@0 | 60 | this._weakRequestPrincipal = { get : function() { return null; } }; |
michael@0 | 61 | this._referrer = ""; |
michael@0 | 62 | this._weakDocRequest = { get : function() { return null; } }; |
michael@0 | 63 | CSPdebug("CSP object initialized, no policies to enforce yet"); |
michael@0 | 64 | |
michael@0 | 65 | this._cache = new Map(); |
michael@0 | 66 | } |
michael@0 | 67 | |
michael@0 | 68 | /* |
michael@0 | 69 | * Set up mappings from nsIContentPolicy content types to CSP directives. |
michael@0 | 70 | */ |
michael@0 | 71 | { |
michael@0 | 72 | let cp = Ci.nsIContentPolicy; |
michael@0 | 73 | let csp = ContentSecurityPolicy; |
michael@0 | 74 | let cspr_sd_old = CSPRep.SRC_DIRECTIVES_OLD; |
michael@0 | 75 | let cspr_sd_new = CSPRep.SRC_DIRECTIVES_NEW; |
michael@0 | 76 | |
michael@0 | 77 | csp._MAPPINGS=[]; |
michael@0 | 78 | |
michael@0 | 79 | /* default, catch-all case */ |
michael@0 | 80 | // This is the same in old and new CSP so use the new mapping. |
michael@0 | 81 | csp._MAPPINGS[cp.TYPE_OTHER] = cspr_sd_new.DEFAULT_SRC; |
michael@0 | 82 | |
michael@0 | 83 | /* self */ |
michael@0 | 84 | csp._MAPPINGS[cp.TYPE_DOCUMENT] = null; |
michael@0 | 85 | |
michael@0 | 86 | /* shouldn't see this one */ |
michael@0 | 87 | csp._MAPPINGS[cp.TYPE_REFRESH] = null; |
michael@0 | 88 | |
michael@0 | 89 | /* categorized content types */ |
michael@0 | 90 | // These are the same in old and new CSP's so just use the new mappings. |
michael@0 | 91 | csp._MAPPINGS[cp.TYPE_SCRIPT] = cspr_sd_new.SCRIPT_SRC; |
michael@0 | 92 | csp._MAPPINGS[cp.TYPE_IMAGE] = cspr_sd_new.IMG_SRC; |
michael@0 | 93 | csp._MAPPINGS[cp.TYPE_STYLESHEET] = cspr_sd_new.STYLE_SRC; |
michael@0 | 94 | csp._MAPPINGS[cp.TYPE_OBJECT] = cspr_sd_new.OBJECT_SRC; |
michael@0 | 95 | csp._MAPPINGS[cp.TYPE_OBJECT_SUBREQUEST] = cspr_sd_new.OBJECT_SRC; |
michael@0 | 96 | csp._MAPPINGS[cp.TYPE_SUBDOCUMENT] = cspr_sd_new.FRAME_SRC; |
michael@0 | 97 | csp._MAPPINGS[cp.TYPE_MEDIA] = cspr_sd_new.MEDIA_SRC; |
michael@0 | 98 | csp._MAPPINGS[cp.TYPE_FONT] = cspr_sd_new.FONT_SRC; |
michael@0 | 99 | csp._MAPPINGS[cp.TYPE_XSLT] = cspr_sd_new.SCRIPT_SRC; |
michael@0 | 100 | csp._MAPPINGS[cp.TYPE_BEACON] = cspr_sd_new.CONNECT_SRC; |
michael@0 | 101 | |
michael@0 | 102 | /* Our original CSP implementation's mappings for XHR and websocket |
michael@0 | 103 | * These should be changed to be = cspr_sd.CONNECT_SRC when we remove |
michael@0 | 104 | * the original implementation - NOTE: order in this array is important !!! |
michael@0 | 105 | */ |
michael@0 | 106 | csp._MAPPINGS[cp.TYPE_XMLHTTPREQUEST] = cspr_sd_old.XHR_SRC; |
michael@0 | 107 | csp._MAPPINGS[cp.TYPE_WEBSOCKET] = cspr_sd_old.XHR_SRC; |
michael@0 | 108 | |
michael@0 | 109 | /* CSP cannot block CSP reports */ |
michael@0 | 110 | csp._MAPPINGS[cp.TYPE_CSP_REPORT] = null; |
michael@0 | 111 | |
michael@0 | 112 | /* These must go through the catch-all */ |
michael@0 | 113 | csp._MAPPINGS[cp.TYPE_XBL] = cspr_sd_new.DEFAULT_SRC; |
michael@0 | 114 | csp._MAPPINGS[cp.TYPE_PING] = cspr_sd_new.DEFAULT_SRC; |
michael@0 | 115 | csp._MAPPINGS[cp.TYPE_DTD] = cspr_sd_new.DEFAULT_SRC; |
michael@0 | 116 | |
michael@0 | 117 | /* CSP 1.0 spec compliant mappings for XHR and websocket */ |
michael@0 | 118 | // The directive name for XHR, websocket, and EventSource is different |
michael@0 | 119 | // in the 1.0 spec than in our original implementation, these mappings |
michael@0 | 120 | // address this. These won't be needed when we deprecate our original |
michael@0 | 121 | // implementation. |
michael@0 | 122 | csp._MAPPINGS[CSP_TYPE_XMLHTTPREQUEST_SPEC_COMPLIANT] = cspr_sd_new.CONNECT_SRC; |
michael@0 | 123 | csp._MAPPINGS[CSP_TYPE_WEBSOCKET_SPEC_COMPLIANT] = cspr_sd_new.CONNECT_SRC; |
michael@0 | 124 | // TODO : EventSource will be here and also will use connect-src |
michael@0 | 125 | // after we fix Bug 802872 - CSP should restrict EventSource using the connect-src |
michael@0 | 126 | // directive. For background see Bug 667490 - EventSource should use the same |
michael@0 | 127 | // nsIContentPolicy type as XHR (which is fixed) |
michael@0 | 128 | } |
michael@0 | 129 | |
michael@0 | 130 | ContentSecurityPolicy.prototype = { |
michael@0 | 131 | classID: CSP_CLASS_ID, |
michael@0 | 132 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentSecurityPolicy, |
michael@0 | 133 | Ci.nsISerializable, |
michael@0 | 134 | Ci.nsISupports]), |
michael@0 | 135 | |
michael@0 | 136 | // Class info is required to be able to serialize |
michael@0 | 137 | classInfo: XPCOMUtils.generateCI({classID: CSP_CLASS_ID, |
michael@0 | 138 | contractID: CSP_CONTRACT_ID, |
michael@0 | 139 | interfaces: [Ci.nsIContentSecurityPolicy, |
michael@0 | 140 | Ci.nsISerializable], |
michael@0 | 141 | flags: Ci.nsIClassInfo.MAIN_THREAD_ONLY}), |
michael@0 | 142 | |
michael@0 | 143 | get isInitialized() { |
michael@0 | 144 | return this._isInitialized; |
michael@0 | 145 | }, |
michael@0 | 146 | |
michael@0 | 147 | set isInitialized (foo) { |
michael@0 | 148 | this._isInitialized = foo; |
michael@0 | 149 | }, |
michael@0 | 150 | |
michael@0 | 151 | _getPolicyInternal: function(index) { |
michael@0 | 152 | if (index < 0 || index >= this._policies.length) { |
michael@0 | 153 | throw Cr.NS_ERROR_FAILURE; |
michael@0 | 154 | } |
michael@0 | 155 | return this._policies[index]; |
michael@0 | 156 | }, |
michael@0 | 157 | |
michael@0 | 158 | _buildViolatedDirectiveString: |
michael@0 | 159 | function(aDirectiveName, aPolicy) { |
michael@0 | 160 | var SD = CSPRep.SRC_DIRECTIVES_NEW; |
michael@0 | 161 | var cspContext = (SD[aDirectiveName] in aPolicy._directives) ? SD[aDirectiveName] : SD.DEFAULT_SRC; |
michael@0 | 162 | var directive = aPolicy._directives[cspContext]; |
michael@0 | 163 | return cspContext + ' ' + directive.toString(); |
michael@0 | 164 | }, |
michael@0 | 165 | |
michael@0 | 166 | /** |
michael@0 | 167 | * Returns policy string representing the policy at "index". |
michael@0 | 168 | */ |
michael@0 | 169 | getPolicy: function(index) { |
michael@0 | 170 | return this._getPolicyInternal(index).toString(); |
michael@0 | 171 | }, |
michael@0 | 172 | |
michael@0 | 173 | /** |
michael@0 | 174 | * Returns count of policies. |
michael@0 | 175 | */ |
michael@0 | 176 | get numPolicies() { |
michael@0 | 177 | return this._policies.length; |
michael@0 | 178 | }, |
michael@0 | 179 | |
michael@0 | 180 | getAllowsInlineScript: function(shouldReportViolations) { |
michael@0 | 181 | // report it? (for each policy, is it violated?) |
michael@0 | 182 | shouldReportViolations.value = this._policies.some(function(a) { return !a.allowsInlineScripts; }); |
michael@0 | 183 | |
michael@0 | 184 | // allow it to execute? (Do all the policies allow it to execute)? |
michael@0 | 185 | return this._policies.every(function(a) { |
michael@0 | 186 | return a._reportOnlyMode || a.allowsInlineScripts; |
michael@0 | 187 | }); |
michael@0 | 188 | }, |
michael@0 | 189 | |
michael@0 | 190 | getAllowsEval: function(shouldReportViolations) { |
michael@0 | 191 | // report it? (for each policy, is it violated?) |
michael@0 | 192 | shouldReportViolations.value = this._policies.some(function(a) { return !a.allowsEvalInScripts; }); |
michael@0 | 193 | |
michael@0 | 194 | // allow it to execute? (Do all the policies allow it to execute)? |
michael@0 | 195 | return this._policies.every(function(a) { |
michael@0 | 196 | return a._reportOnlyMode || a.allowsEvalInScripts; |
michael@0 | 197 | }); |
michael@0 | 198 | }, |
michael@0 | 199 | |
michael@0 | 200 | getAllowsInlineStyle: function(shouldReportViolations) { |
michael@0 | 201 | // report it? (for each policy, is it violated?) |
michael@0 | 202 | shouldReportViolations.value = this._policies.some(function(a) { return !a.allowsInlineStyles; }); |
michael@0 | 203 | |
michael@0 | 204 | // allow it to execute? (Do all the policies allow it to execute)? |
michael@0 | 205 | return this._policies.every(function(a) { |
michael@0 | 206 | return a._reportOnlyMode || a.allowsInlineStyles; |
michael@0 | 207 | }); |
michael@0 | 208 | }, |
michael@0 | 209 | |
michael@0 | 210 | getAllowsNonce: function(aNonce, aContentType, shouldReportViolation) { |
michael@0 | 211 | if (!(aContentType == Ci.nsIContentPolicy.TYPE_SCRIPT || |
michael@0 | 212 | aContentType == Ci.nsIContentPolicy.TYPE_STYLESHEET)) { |
michael@0 | 213 | CSPdebug("Nonce check requested for an invalid content type (not script or style): " + aContentType); |
michael@0 | 214 | return false; |
michael@0 | 215 | } |
michael@0 | 216 | |
michael@0 | 217 | let directive = ContentSecurityPolicy._MAPPINGS[aContentType]; |
michael@0 | 218 | let policyAllowsNonce = [ policy.permitsNonce(aNonce, directive) |
michael@0 | 219 | for (policy of this._policies) ]; |
michael@0 | 220 | |
michael@0 | 221 | shouldReportViolation.value = this._policies.some(function(policy, i) { |
michael@0 | 222 | // Don't report a violation if the policy didn't use nonce-source |
michael@0 | 223 | return policy._directives.hasOwnProperty(directive) && |
michael@0 | 224 | policy._directives[directive]._hasNonceSource && |
michael@0 | 225 | !policyAllowsNonce[i]; |
michael@0 | 226 | }); |
michael@0 | 227 | |
michael@0 | 228 | // allow it to execute? (Do all the policies allow it to execute)? |
michael@0 | 229 | return this._policies.every(function(policy, i) { |
michael@0 | 230 | return policy._reportOnlyMode || policyAllowsNonce[i]; |
michael@0 | 231 | }); |
michael@0 | 232 | }, |
michael@0 | 233 | |
michael@0 | 234 | getAllowsHash: function(aContent, aContentType, shouldReportViolation) { |
michael@0 | 235 | if (!(aContentType == Ci.nsIContentPolicy.TYPE_SCRIPT || |
michael@0 | 236 | aContentType == Ci.nsIContentPolicy.TYPE_STYLESHEET)) { |
michael@0 | 237 | CSPdebug("Hash check requested for an invalid content type (not script or style): " + aContentType); |
michael@0 | 238 | return false; |
michael@0 | 239 | } |
michael@0 | 240 | |
michael@0 | 241 | let directive = ContentSecurityPolicy._MAPPINGS[aContentType]; |
michael@0 | 242 | let policyAllowsHash = [ policy.permitsHash(aContent, directive) |
michael@0 | 243 | for (policy of this._policies) ]; |
michael@0 | 244 | |
michael@0 | 245 | shouldReportViolation.value = this._policies.some(function(policy, i) { |
michael@0 | 246 | // Don't report a violation if the policy didn't use hash-source |
michael@0 | 247 | return policy._directives.hasOwnProperty(directive) && |
michael@0 | 248 | policy._directives[directive]._hasHashSource && |
michael@0 | 249 | !policyAllowsHash[i]; |
michael@0 | 250 | }); |
michael@0 | 251 | |
michael@0 | 252 | // allow it to execute? (Do all the policies allow it to execute)? |
michael@0 | 253 | return this._policies.every(function(policy, i) { |
michael@0 | 254 | return policy._reportOnlyMode || policyAllowsHash[i]; |
michael@0 | 255 | }); |
michael@0 | 256 | }, |
michael@0 | 257 | |
michael@0 | 258 | /** |
michael@0 | 259 | * For each policy, log any violation on the Error Console and send a report |
michael@0 | 260 | * if a report-uri is present in the policy |
michael@0 | 261 | * |
michael@0 | 262 | * @param aViolationType |
michael@0 | 263 | * one of the VIOLATION_TYPE_* constants, e.g. inline-script or eval |
michael@0 | 264 | * @param aSourceFile |
michael@0 | 265 | * name of the source file containing the violation (if available) |
michael@0 | 266 | * @param aContentSample |
michael@0 | 267 | * sample of the violating content (to aid debugging) |
michael@0 | 268 | * @param aLineNum |
michael@0 | 269 | * source line number of the violation (if available) |
michael@0 | 270 | * @param aNonce |
michael@0 | 271 | * (optional) If this is a nonce violation, include the nonce so we can |
michael@0 | 272 | * recheck to determine which policies were violated and send the |
michael@0 | 273 | * appropriate reports. |
michael@0 | 274 | * @param aContent |
michael@0 | 275 | * (optional) If this is a hash violation, include contents of the inline |
michael@0 | 276 | * resource in the question so we can recheck the hash in order to |
michael@0 | 277 | * determine which policies were violated and send the appropriate |
michael@0 | 278 | * reports. |
michael@0 | 279 | */ |
michael@0 | 280 | logViolationDetails: |
michael@0 | 281 | function(aViolationType, aSourceFile, aScriptSample, aLineNum, aNonce, aContent) { |
michael@0 | 282 | for (let policyIndex=0; policyIndex < this._policies.length; policyIndex++) { |
michael@0 | 283 | let policy = this._policies[policyIndex]; |
michael@0 | 284 | |
michael@0 | 285 | // call-sites to the eval/inline checks recieve two return values: allows |
michael@0 | 286 | // and violates. Policies that are report-only allow the |
michael@0 | 287 | // loads/compilations but violations should still be reported. Not all |
michael@0 | 288 | // policies in this nsIContentSecurityPolicy instance will be violated, |
michael@0 | 289 | // which is why we must check again here. |
michael@0 | 290 | switch (aViolationType) { |
michael@0 | 291 | case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_STYLE: |
michael@0 | 292 | if (!policy.allowsInlineStyles) { |
michael@0 | 293 | var violatedDirective = this._buildViolatedDirectiveString('STYLE_SRC', policy); |
michael@0 | 294 | this._asyncReportViolation('self', null, violatedDirective, policyIndex, |
michael@0 | 295 | INLINE_STYLE_VIOLATION_OBSERVER_SUBJECT, |
michael@0 | 296 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 297 | } |
michael@0 | 298 | break; |
michael@0 | 299 | case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_SCRIPT: |
michael@0 | 300 | if (!policy.allowsInlineScripts) { |
michael@0 | 301 | var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); |
michael@0 | 302 | this._asyncReportViolation('self', null, violatedDirective, policyIndex, |
michael@0 | 303 | INLINE_SCRIPT_VIOLATION_OBSERVER_SUBJECT, |
michael@0 | 304 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 305 | } |
michael@0 | 306 | break; |
michael@0 | 307 | case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL: |
michael@0 | 308 | if (!policy.allowsEvalInScripts) { |
michael@0 | 309 | var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); |
michael@0 | 310 | this._asyncReportViolation('self', null, violatedDirective, policyIndex, |
michael@0 | 311 | EVAL_VIOLATION_OBSERVER_SUBJECT, |
michael@0 | 312 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 313 | } |
michael@0 | 314 | break; |
michael@0 | 315 | case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_NONCE_SCRIPT: |
michael@0 | 316 | var scriptType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_SCRIPT]; |
michael@0 | 317 | if (!policy.permitsNonce(aNonce, scriptType)) { |
michael@0 | 318 | var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); |
michael@0 | 319 | this._asyncReportViolation('self', null, violatedDirective, policyIndex, |
michael@0 | 320 | SCRIPT_NONCE_VIOLATION_OBSERVER_SUBJECT, |
michael@0 | 321 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 322 | } |
michael@0 | 323 | break; |
michael@0 | 324 | case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_NONCE_STYLE: |
michael@0 | 325 | var styleType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_STYLE]; |
michael@0 | 326 | if (!policy.permitsNonce(aNonce, styleType)) { |
michael@0 | 327 | var violatedDirective = this._buildViolatedDirectiveString('STYLE_SRC', policy); |
michael@0 | 328 | this._asyncReportViolation('self', null, violatedDirective, policyIndex, |
michael@0 | 329 | STYLE_NONCE_VIOLATION_OBSERVER_SUBJECT, |
michael@0 | 330 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 331 | } |
michael@0 | 332 | break; |
michael@0 | 333 | case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_HASH_SCRIPT: |
michael@0 | 334 | var scriptType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_SCRIPT]; |
michael@0 | 335 | if (!policy.permitsHash(aContent, scriptType)) { |
michael@0 | 336 | var violatedDirective = this._buildViolatedDirectiveString('SCRIPT_SRC', policy); |
michael@0 | 337 | this._asyncReportViolation('self', null, violatedDirective, policyIndex, |
michael@0 | 338 | SCRIPT_HASH_VIOLATION_OBSERVER_SUBJECT, |
michael@0 | 339 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 340 | } |
michael@0 | 341 | break; |
michael@0 | 342 | case Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_HASH_STYLE: |
michael@0 | 343 | var styleType = ContentSecurityPolicy._MAPPINGS[Ci.nsIContentPolicy.TYPE_STYLE]; |
michael@0 | 344 | if (!policy.permitsHash(aContent, styleType)) { |
michael@0 | 345 | var violatedDirective = this._buildViolatedDirectiveString('STYLE_SRC', policy); |
michael@0 | 346 | this._asyncReportViolation('self', null, violatedDirective, policyIndex, |
michael@0 | 347 | STYLE_HASH_VIOLATION_OBSERVER_SUBJECT, |
michael@0 | 348 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 349 | } |
michael@0 | 350 | break; |
michael@0 | 351 | } |
michael@0 | 352 | } |
michael@0 | 353 | }, |
michael@0 | 354 | |
michael@0 | 355 | /** |
michael@0 | 356 | * Given an nsIHttpChannel, fill out the appropriate data. |
michael@0 | 357 | */ |
michael@0 | 358 | setRequestContext: |
michael@0 | 359 | function(aSelfURI, aReferrerURI, aPrincipal, aChannel) { |
michael@0 | 360 | |
michael@0 | 361 | // this requires either a self URI or a http channel. |
michael@0 | 362 | if (!aSelfURI && !aChannel) |
michael@0 | 363 | return; |
michael@0 | 364 | |
michael@0 | 365 | if (aChannel) { |
michael@0 | 366 | // Save the docRequest for fetching a policy-uri |
michael@0 | 367 | this._weakDocRequest = Cu.getWeakReference(aChannel); |
michael@0 | 368 | } |
michael@0 | 369 | |
michael@0 | 370 | // save the document URI (minus <fragment>) and referrer for reporting |
michael@0 | 371 | let uri = aSelfURI ? aSelfURI.cloneIgnoringRef() : aChannel.URI.cloneIgnoringRef(); |
michael@0 | 372 | try { // GetUserPass throws for some protocols without userPass |
michael@0 | 373 | uri.userPass = ''; |
michael@0 | 374 | } catch (ex) {} |
michael@0 | 375 | this._request = uri.asciiSpec; |
michael@0 | 376 | this._requestOrigin = uri; |
michael@0 | 377 | |
michael@0 | 378 | // store a reference to the principal, that can later be used in shouldLoad |
michael@0 | 379 | if (aPrincipal) { |
michael@0 | 380 | this._weakRequestPrincipal = Cu.getWeakReference(aPrincipal); |
michael@0 | 381 | } else if (aChannel) { |
michael@0 | 382 | this._weakRequestPrincipal = Cu.getWeakReference(Cc["@mozilla.org/scriptsecuritymanager;1"] |
michael@0 | 383 | .getService(Ci.nsIScriptSecurityManager) |
michael@0 | 384 | .getChannelPrincipal(aChannel)); |
michael@0 | 385 | } else { |
michael@0 | 386 | CSPdebug("No principal or channel for document context; violation reports may not work."); |
michael@0 | 387 | } |
michael@0 | 388 | |
michael@0 | 389 | // pick one: referrerURI, channel's referrer, or null (first available) |
michael@0 | 390 | let ref = null; |
michael@0 | 391 | if (aReferrerURI) |
michael@0 | 392 | ref = aReferrerURI; |
michael@0 | 393 | else if (aChannel instanceof Ci.nsIHttpChannel) |
michael@0 | 394 | ref = aChannel.referrer; |
michael@0 | 395 | |
michael@0 | 396 | if (ref) { |
michael@0 | 397 | let referrer = aChannel.referrer.cloneIgnoringRef(); |
michael@0 | 398 | try { // GetUserPass throws for some protocols without userPass |
michael@0 | 399 | referrer.userPass = ''; |
michael@0 | 400 | } catch (ex) {} |
michael@0 | 401 | this._referrer = referrer.asciiSpec; |
michael@0 | 402 | } |
michael@0 | 403 | }, |
michael@0 | 404 | |
michael@0 | 405 | /* ........ Methods .............. */ |
michael@0 | 406 | |
michael@0 | 407 | /** |
michael@0 | 408 | * Adds a new policy to our list of policies for this CSP context. |
michael@0 | 409 | * @returns the count of policies. |
michael@0 | 410 | */ |
michael@0 | 411 | appendPolicy: |
michael@0 | 412 | function csp_appendPolicy(aPolicy, selfURI, aReportOnly, aSpecCompliant) { |
michael@0 | 413 | return this._appendPolicyInternal(aPolicy, selfURI, aReportOnly, |
michael@0 | 414 | aSpecCompliant, true); |
michael@0 | 415 | }, |
michael@0 | 416 | |
michael@0 | 417 | /** |
michael@0 | 418 | * Adds a new policy to our list of policies for this CSP context. |
michael@0 | 419 | * Only to be called from this module (not exported) |
michael@0 | 420 | * @returns the count of policies. |
michael@0 | 421 | */ |
michael@0 | 422 | _appendPolicyInternal: |
michael@0 | 423 | function csp_appendPolicy(aPolicy, selfURI, aReportOnly, aSpecCompliant, |
michael@0 | 424 | aEnforceSelfChecks) { |
michael@0 | 425 | #ifndef MOZ_B2G |
michael@0 | 426 | CSPdebug("APPENDING POLICY: " + aPolicy); |
michael@0 | 427 | CSPdebug(" SELF: " + (selfURI ? selfURI.asciiSpec : " null")); |
michael@0 | 428 | CSPdebug("CSP 1.0 COMPLIANT : " + aSpecCompliant); |
michael@0 | 429 | #endif |
michael@0 | 430 | |
michael@0 | 431 | // For nested schemes such as view-source: make sure we are taking the |
michael@0 | 432 | // innermost URI to use as 'self' since that's where we will extract the |
michael@0 | 433 | // scheme, host and port from |
michael@0 | 434 | if (selfURI instanceof Ci.nsINestedURI) { |
michael@0 | 435 | #ifndef MOZ_B2G |
michael@0 | 436 | CSPdebug(" INNER: " + selfURI.innermostURI.asciiSpec); |
michael@0 | 437 | #endif |
michael@0 | 438 | selfURI = selfURI.innermostURI; |
michael@0 | 439 | } |
michael@0 | 440 | |
michael@0 | 441 | // stay uninitialized until policy setup is done |
michael@0 | 442 | var newpolicy; |
michael@0 | 443 | |
michael@0 | 444 | // If there is a policy-uri, fetch the policy, then re-call this function. |
michael@0 | 445 | // (1) parse and create a CSPRep object |
michael@0 | 446 | // Note that we pass the full URI since when it's parsed as 'self' to construct a |
michael@0 | 447 | // CSPSource only the scheme, host, and port are kept. |
michael@0 | 448 | |
michael@0 | 449 | // If we want to be CSP 1.0 spec compliant, use the new parser. |
michael@0 | 450 | // The old one will be deprecated in the future and will be |
michael@0 | 451 | // removed at that time. |
michael@0 | 452 | if (aSpecCompliant) { |
michael@0 | 453 | newpolicy = CSPRep.fromStringSpecCompliant(aPolicy, |
michael@0 | 454 | selfURI, |
michael@0 | 455 | aReportOnly, |
michael@0 | 456 | this._weakDocRequest.get(), |
michael@0 | 457 | this, |
michael@0 | 458 | aEnforceSelfChecks); |
michael@0 | 459 | } else { |
michael@0 | 460 | newpolicy = CSPRep.fromString(aPolicy, |
michael@0 | 461 | selfURI, |
michael@0 | 462 | aReportOnly, |
michael@0 | 463 | this._weakDocRequest.get(), |
michael@0 | 464 | this, |
michael@0 | 465 | aEnforceSelfChecks); |
michael@0 | 466 | } |
michael@0 | 467 | |
michael@0 | 468 | newpolicy._specCompliant = !!aSpecCompliant; |
michael@0 | 469 | newpolicy._isInitialized = true; |
michael@0 | 470 | this._policies.push(newpolicy); |
michael@0 | 471 | this._cache.clear(); // reset cache since effective policy changes |
michael@0 | 472 | }, |
michael@0 | 473 | |
michael@0 | 474 | /** |
michael@0 | 475 | * Removes a policy from the array of policies. |
michael@0 | 476 | */ |
michael@0 | 477 | removePolicy: |
michael@0 | 478 | function csp_removePolicy(index) { |
michael@0 | 479 | if (index < 0 || index >= this._policies.length) { |
michael@0 | 480 | CSPdebug("Cannot remove policy " + index + "; not enough policies."); |
michael@0 | 481 | return; |
michael@0 | 482 | } |
michael@0 | 483 | this._policies.splice(index, 1); |
michael@0 | 484 | this._cache.clear(); // reset cache since effective policy changes |
michael@0 | 485 | }, |
michael@0 | 486 | |
michael@0 | 487 | /** |
michael@0 | 488 | * Generates and sends a violation report to the specified report URIs. |
michael@0 | 489 | */ |
michael@0 | 490 | sendReports: |
michael@0 | 491 | function(blockedUri, originalUri, violatedDirective, |
michael@0 | 492 | violatedPolicyIndex, aSourceFile, |
michael@0 | 493 | aScriptSample, aLineNum) { |
michael@0 | 494 | |
michael@0 | 495 | let policy = this._getPolicyInternal(violatedPolicyIndex); |
michael@0 | 496 | if (!policy) { |
michael@0 | 497 | CSPdebug("ERROR in SendReports: policy " + violatedPolicyIndex + " is not defined."); |
michael@0 | 498 | return; |
michael@0 | 499 | } |
michael@0 | 500 | |
michael@0 | 501 | var uriString = policy.getReportURIs(); |
michael@0 | 502 | var uris = uriString.split(/\s+/); |
michael@0 | 503 | if (uris.length > 0) { |
michael@0 | 504 | // see if we need to sanitize the blocked-uri |
michael@0 | 505 | let blocked = ''; |
michael@0 | 506 | if (originalUri) { |
michael@0 | 507 | // We've redirected, only report the blocked origin |
michael@0 | 508 | try { |
michael@0 | 509 | let clone = blockedUri.clone(); |
michael@0 | 510 | clone.path = ''; |
michael@0 | 511 | blocked = clone.asciiSpec; |
michael@0 | 512 | } catch(e) { |
michael@0 | 513 | CSPdebug(".... blockedUri can't be cloned: " + blockedUri); |
michael@0 | 514 | } |
michael@0 | 515 | } |
michael@0 | 516 | else if (blockedUri instanceof Ci.nsIURI) { |
michael@0 | 517 | blocked = blockedUri.cloneIgnoringRef().asciiSpec; |
michael@0 | 518 | } |
michael@0 | 519 | else { |
michael@0 | 520 | // blockedUri is a string for eval/inline-script violations |
michael@0 | 521 | blocked = blockedUri; |
michael@0 | 522 | } |
michael@0 | 523 | |
michael@0 | 524 | // Generate report to send composed of |
michael@0 | 525 | // { |
michael@0 | 526 | // csp-report: { |
michael@0 | 527 | // document-uri: "http://example.com/file.html?params", |
michael@0 | 528 | // referrer: "...", |
michael@0 | 529 | // blocked-uri: "...", |
michael@0 | 530 | // violated-directive: "..." |
michael@0 | 531 | // } |
michael@0 | 532 | // } |
michael@0 | 533 | var report = { |
michael@0 | 534 | 'csp-report': { |
michael@0 | 535 | 'document-uri': this._request, |
michael@0 | 536 | 'referrer': this._referrer, |
michael@0 | 537 | 'blocked-uri': blocked, |
michael@0 | 538 | 'violated-directive': violatedDirective |
michael@0 | 539 | } |
michael@0 | 540 | } |
michael@0 | 541 | |
michael@0 | 542 | // extra report fields for script errors (if available) |
michael@0 | 543 | if (originalUri) |
michael@0 | 544 | report["csp-report"]["original-uri"] = originalUri.cloneIgnoringRef().asciiSpec; |
michael@0 | 545 | if (aSourceFile) |
michael@0 | 546 | report["csp-report"]["source-file"] = aSourceFile; |
michael@0 | 547 | if (aScriptSample) |
michael@0 | 548 | report["csp-report"]["script-sample"] = aScriptSample; |
michael@0 | 549 | if (aLineNum) |
michael@0 | 550 | report["csp-report"]["line-number"] = aLineNum; |
michael@0 | 551 | |
michael@0 | 552 | var reportString = JSON.stringify(report); |
michael@0 | 553 | CSPdebug("Constructed violation report:\n" + reportString); |
michael@0 | 554 | |
michael@0 | 555 | // For each URI in the report list, send out a report. |
michael@0 | 556 | // We make the assumption that all of the URIs are absolute URIs; this |
michael@0 | 557 | // should be taken care of in CSPRep.fromString (where it converts any |
michael@0 | 558 | // relative URIs into absolute ones based on "self"). |
michael@0 | 559 | for (let i in uris) { |
michael@0 | 560 | if (uris[i] === "") |
michael@0 | 561 | continue; |
michael@0 | 562 | |
michael@0 | 563 | try { |
michael@0 | 564 | var chan = Services.io.newChannel(uris[i], null, null); |
michael@0 | 565 | if (!chan) { |
michael@0 | 566 | CSPdebug("Error creating channel for " + uris[i]); |
michael@0 | 567 | continue; |
michael@0 | 568 | } |
michael@0 | 569 | |
michael@0 | 570 | var content = Cc["@mozilla.org/io/string-input-stream;1"] |
michael@0 | 571 | .createInstance(Ci.nsIStringInputStream); |
michael@0 | 572 | content.data = reportString + "\n\n"; |
michael@0 | 573 | |
michael@0 | 574 | // make sure this is an anonymous request (no cookies) so in case the |
michael@0 | 575 | // policy URI is injected, it can't be abused for CSRF. |
michael@0 | 576 | chan.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS; |
michael@0 | 577 | |
michael@0 | 578 | // we need to set an nsIChannelEventSink on the channel object |
michael@0 | 579 | // so we can tell it to not follow redirects when posting the reports |
michael@0 | 580 | chan.notificationCallbacks = new CSPReportRedirectSink(policy); |
michael@0 | 581 | if (this._weakDocRequest.get()) { |
michael@0 | 582 | chan.loadGroup = this._weakDocRequest.get().loadGroup; |
michael@0 | 583 | } |
michael@0 | 584 | |
michael@0 | 585 | chan.QueryInterface(Ci.nsIUploadChannel) |
michael@0 | 586 | .setUploadStream(content, "application/json", content.available()); |
michael@0 | 587 | |
michael@0 | 588 | try { |
michael@0 | 589 | // if this is an HTTP channel, set the request method to post |
michael@0 | 590 | chan.QueryInterface(Ci.nsIHttpChannel); |
michael@0 | 591 | chan.requestMethod = "POST"; |
michael@0 | 592 | } catch(e) {} // throws only if chan is not an nsIHttpChannel. |
michael@0 | 593 | |
michael@0 | 594 | // check with the content policy service to see if we're allowed to |
michael@0 | 595 | // send this request. |
michael@0 | 596 | try { |
michael@0 | 597 | var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"] |
michael@0 | 598 | .getService(Ci.nsIContentPolicy); |
michael@0 | 599 | if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_CSP_REPORT, |
michael@0 | 600 | chan.URI, this._requestOrigin, |
michael@0 | 601 | null, null, null, this._weakRequestPrincipal.get()) |
michael@0 | 602 | != Ci.nsIContentPolicy.ACCEPT) { |
michael@0 | 603 | continue; // skip unauthorized URIs |
michael@0 | 604 | } |
michael@0 | 605 | } catch(e) { |
michael@0 | 606 | continue; // refuse to load if we can't do a security check. |
michael@0 | 607 | } |
michael@0 | 608 | |
michael@0 | 609 | //send data (and set up error notifications) |
michael@0 | 610 | chan.asyncOpen(new CSPViolationReportListener(uris[i]), null); |
michael@0 | 611 | CSPdebug("Sent violation report to " + uris[i]); |
michael@0 | 612 | } catch(e) { |
michael@0 | 613 | // it's possible that the URI was invalid, just log a |
michael@0 | 614 | // warning and skip over that. |
michael@0 | 615 | policy.log(WARN_FLAG, CSPLocalizer.getFormatStr("triedToSendReport", [uris[i]])); |
michael@0 | 616 | policy.log(WARN_FLAG, CSPLocalizer.getFormatStr("errorWas", [e.toString()])); |
michael@0 | 617 | } |
michael@0 | 618 | } |
michael@0 | 619 | } |
michael@0 | 620 | }, |
michael@0 | 621 | |
michael@0 | 622 | /** |
michael@0 | 623 | * Logs a meaningful CSP warning to the developer console. |
michael@0 | 624 | */ |
michael@0 | 625 | logToConsole: |
michael@0 | 626 | function(blockedUri, originalUri, violatedDirective, aViolatedPolicyIndex, |
michael@0 | 627 | aSourceFile, aScriptSample, aLineNum, aObserverSubject) { |
michael@0 | 628 | let policy = this._policies[aViolatedPolicyIndex]; |
michael@0 | 629 | switch(aObserverSubject.data) { |
michael@0 | 630 | case INLINE_STYLE_VIOLATION_OBSERVER_SUBJECT: |
michael@0 | 631 | violatedDirective = CSPLocalizer.getStr("inlineStyleBlocked"); |
michael@0 | 632 | break; |
michael@0 | 633 | case INLINE_SCRIPT_VIOLATION_OBSERVER_SUBJECT: |
michael@0 | 634 | violatedDirective = CSPLocalizer.getStr("inlineScriptBlocked"); |
michael@0 | 635 | break; |
michael@0 | 636 | case EVAL_VIOLATION_OBSERVER_SUBJECT: |
michael@0 | 637 | violatedDirective = CSPLocalizer.getStr("scriptFromStringBlocked"); |
michael@0 | 638 | break; |
michael@0 | 639 | } |
michael@0 | 640 | var violationMessage = null; |
michael@0 | 641 | if (blockedUri["asciiSpec"]) { |
michael@0 | 642 | violationMessage = CSPLocalizer.getFormatStr("CSPViolationWithURI", [violatedDirective, blockedUri.asciiSpec]); |
michael@0 | 643 | } else { |
michael@0 | 644 | violationMessage = CSPLocalizer.getFormatStr("CSPViolation", [violatedDirective]); |
michael@0 | 645 | } |
michael@0 | 646 | policy.log(WARN_FLAG, violationMessage, |
michael@0 | 647 | (aSourceFile) ? aSourceFile : null, |
michael@0 | 648 | (aScriptSample) ? decodeURIComponent(aScriptSample) : null, |
michael@0 | 649 | (aLineNum) ? aLineNum : null); |
michael@0 | 650 | }, |
michael@0 | 651 | |
michael@0 | 652 | /** |
michael@0 | 653 | * Exposed Method to analyze docShell for approved frame ancestry. |
michael@0 | 654 | * NOTE: Also sends violation reports if necessary. |
michael@0 | 655 | * @param docShell |
michael@0 | 656 | * the docShell for this policy's resource. |
michael@0 | 657 | * @return |
michael@0 | 658 | * true if the frame ancestry is allowed by this policy and the load |
michael@0 | 659 | * should progress. |
michael@0 | 660 | */ |
michael@0 | 661 | permitsAncestry: |
michael@0 | 662 | function(docShell) { |
michael@0 | 663 | // Cannot shortcut checking all the policies since violation reports have |
michael@0 | 664 | // to be triggered if any policy wants it. |
michael@0 | 665 | var permitted = true; |
michael@0 | 666 | for (let i = 0; i < this._policies.length; i++) { |
michael@0 | 667 | if (!this._permitsAncestryInternal(docShell, this._policies[i], i)) { |
michael@0 | 668 | permitted = false; |
michael@0 | 669 | } |
michael@0 | 670 | } |
michael@0 | 671 | return permitted; |
michael@0 | 672 | }, |
michael@0 | 673 | |
michael@0 | 674 | _permitsAncestryInternal: |
michael@0 | 675 | function(docShell, policy, policyIndex) { |
michael@0 | 676 | if (!docShell) { return false; } |
michael@0 | 677 | |
michael@0 | 678 | // walk up this docShell tree until we hit chrome |
michael@0 | 679 | var dst = docShell.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 680 | .getInterface(Ci.nsIDocShellTreeItem); |
michael@0 | 681 | |
michael@0 | 682 | // collect ancestors and make sure they're allowed. |
michael@0 | 683 | var ancestors = []; |
michael@0 | 684 | while (dst.parent) { |
michael@0 | 685 | dst = dst.parent; |
michael@0 | 686 | let it = dst.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 687 | .getInterface(Ci.nsIWebNavigation); |
michael@0 | 688 | if (it.currentURI) { |
michael@0 | 689 | if (it.currentURI.scheme === "chrome") { |
michael@0 | 690 | break; |
michael@0 | 691 | } |
michael@0 | 692 | // delete any userpass |
michael@0 | 693 | let ancestor = it.currentURI.cloneIgnoringRef(); |
michael@0 | 694 | try { // GetUserPass throws for some protocols without userPass |
michael@0 | 695 | ancestor.userPass = ''; |
michael@0 | 696 | } catch (ex) {} |
michael@0 | 697 | |
michael@0 | 698 | #ifndef MOZ_B2G |
michael@0 | 699 | CSPdebug(" found frame ancestor " + ancestor.asciiSpec); |
michael@0 | 700 | #endif |
michael@0 | 701 | ancestors.push(ancestor); |
michael@0 | 702 | } |
michael@0 | 703 | } |
michael@0 | 704 | |
michael@0 | 705 | // scan the discovered ancestors |
michael@0 | 706 | // frame-ancestors is the same in both old and new source directives, |
michael@0 | 707 | // so don't need to differentiate here. |
michael@0 | 708 | let cspContext = CSPRep.SRC_DIRECTIVES_NEW.FRAME_ANCESTORS; |
michael@0 | 709 | for (let i in ancestors) { |
michael@0 | 710 | let ancestor = ancestors[i]; |
michael@0 | 711 | if (!policy.permits(ancestor, cspContext)) { |
michael@0 | 712 | // report the frame-ancestor violation |
michael@0 | 713 | let directive = policy._directives[cspContext]; |
michael@0 | 714 | let violatedPolicy = 'frame-ancestors ' + directive.toString(); |
michael@0 | 715 | |
michael@0 | 716 | this._asyncReportViolation(ancestors[i], null, violatedPolicy, |
michael@0 | 717 | policyIndex); |
michael@0 | 718 | |
michael@0 | 719 | // need to lie if we are testing in report-only mode |
michael@0 | 720 | return policy._reportOnlyMode; |
michael@0 | 721 | } |
michael@0 | 722 | } |
michael@0 | 723 | return true; |
michael@0 | 724 | }, |
michael@0 | 725 | |
michael@0 | 726 | /** |
michael@0 | 727 | * Creates a cache key from content location and content type. |
michael@0 | 728 | */ |
michael@0 | 729 | _createCacheKey: |
michael@0 | 730 | function (aContentLocation, aContentType) { |
michael@0 | 731 | if (aContentType != Ci.nsIContentPolicy.TYPE_SCRIPT && |
michael@0 | 732 | aContentLocation.scheme == "data") { |
michael@0 | 733 | // For non-script data: URI, use ("data:", aContentType) as the cache key. |
michael@0 | 734 | return aContentLocation.scheme + ":" + aContentType; |
michael@0 | 735 | } |
michael@0 | 736 | |
michael@0 | 737 | let uri = aContentLocation.spec; |
michael@0 | 738 | if (uri.length > CSP_CACHE_URI_CUTOFF_SIZE) { |
michael@0 | 739 | // Don't cache for a URI longer than the cutoff size. |
michael@0 | 740 | return null; |
michael@0 | 741 | } |
michael@0 | 742 | return uri + "!" + aContentType; |
michael@0 | 743 | }, |
michael@0 | 744 | |
michael@0 | 745 | /** |
michael@0 | 746 | * Delegate method called by the service when sub-elements of the protected |
michael@0 | 747 | * document are being loaded. Given a bit of information about the request, |
michael@0 | 748 | * decides whether or not the policy is satisfied. |
michael@0 | 749 | */ |
michael@0 | 750 | shouldLoad: |
michael@0 | 751 | function csp_shouldLoad(aContentType, |
michael@0 | 752 | aContentLocation, |
michael@0 | 753 | aRequestOrigin, |
michael@0 | 754 | aContext, |
michael@0 | 755 | aMimeTypeGuess, |
michael@0 | 756 | aOriginalUri) { |
michael@0 | 757 | let key = this._createCacheKey(aContentLocation, aContentType); |
michael@0 | 758 | if (key && this._cache.has(key)) { |
michael@0 | 759 | return this._cache.get(key); |
michael@0 | 760 | } |
michael@0 | 761 | |
michael@0 | 762 | #ifndef MOZ_B2G |
michael@0 | 763 | // Try to remove as much as possible from the hot path on b2g. |
michael@0 | 764 | CSPdebug("shouldLoad location = " + aContentLocation.asciiSpec); |
michael@0 | 765 | CSPdebug("shouldLoad content type = " + aContentType); |
michael@0 | 766 | #endif |
michael@0 | 767 | |
michael@0 | 768 | // The mapping for XHR and websockets is different between our original |
michael@0 | 769 | // implementation and the 1.0 spec, we handle this here. |
michael@0 | 770 | var cspContext; |
michael@0 | 771 | |
michael@0 | 772 | let cp = Ci.nsIContentPolicy; |
michael@0 | 773 | |
michael@0 | 774 | // Infer if this is a preload for elements that use nonce-source. Since, |
michael@0 | 775 | // for preloads, aContext is the document and not the element associated |
michael@0 | 776 | // with the resource, we cannot determine the nonce. See Bug 612921 and |
michael@0 | 777 | // Bug 855326. |
michael@0 | 778 | let nonceSourceValid = aContentType == cp.TYPE_SCRIPT || |
michael@0 | 779 | aContentType == cp.TYPE_STYLESHEET; |
michael@0 | 780 | var possiblePreloadNonceConflict = nonceSourceValid && |
michael@0 | 781 | aContext instanceof Ci.nsIDOMHTMLDocument; |
michael@0 | 782 | |
michael@0 | 783 | // iterate through all the _policies and send reports where a policy is |
michael@0 | 784 | // violated. After the check, determine the overall effect (blocked or |
michael@0 | 785 | // loaded?) and cache it. |
michael@0 | 786 | let policyAllowsLoadArray = []; |
michael@0 | 787 | for (let policyIndex=0; policyIndex < this._policies.length; policyIndex++) { |
michael@0 | 788 | let policy = this._policies[policyIndex]; |
michael@0 | 789 | |
michael@0 | 790 | #ifndef MOZ_B2G |
michael@0 | 791 | CSPdebug("policy is " + (policy._specCompliant ? |
michael@0 | 792 | "1.0 compliant" : "pre-1.0")); |
michael@0 | 793 | CSPdebug("policy is " + (policy._reportOnlyMode ? |
michael@0 | 794 | "report-only" : "blocking")); |
michael@0 | 795 | #endif |
michael@0 | 796 | |
michael@0 | 797 | if (aContentType == cp.TYPE_XMLHTTPREQUEST && this._policies[policyIndex]._specCompliant) { |
michael@0 | 798 | cspContext = ContentSecurityPolicy._MAPPINGS[CSP_TYPE_XMLHTTPREQUEST_SPEC_COMPLIANT]; |
michael@0 | 799 | } else if (aContentType == cp.TYPE_WEBSOCKET && this._policies[policyIndex]._specCompliant) { |
michael@0 | 800 | cspContext = ContentSecurityPolicy._MAPPINGS[CSP_TYPE_WEBSOCKET_SPEC_COMPLIANT]; |
michael@0 | 801 | } else { |
michael@0 | 802 | cspContext = ContentSecurityPolicy._MAPPINGS[aContentType]; |
michael@0 | 803 | } |
michael@0 | 804 | |
michael@0 | 805 | #ifndef MOZ_B2G |
michael@0 | 806 | CSPdebug("shouldLoad cspContext = " + cspContext); |
michael@0 | 807 | #endif |
michael@0 | 808 | |
michael@0 | 809 | // if the mapping is null, there's no policy, let it through. |
michael@0 | 810 | if (!cspContext) { |
michael@0 | 811 | return Ci.nsIContentPolicy.ACCEPT; |
michael@0 | 812 | } |
michael@0 | 813 | |
michael@0 | 814 | // check if location is permitted |
michael@0 | 815 | let permitted = policy.permits(aContentLocation, cspContext); |
michael@0 | 816 | |
michael@0 | 817 | // check any valid content type for nonce if location is not permitted |
michael@0 | 818 | if (!permitted && nonceSourceValid && |
michael@0 | 819 | aContext instanceof Ci.nsIDOMHTMLElement && |
michael@0 | 820 | aContext.hasAttribute('nonce')) { |
michael@0 | 821 | permitted = policy.permitsNonce(aContext.getAttribute('nonce'), |
michael@0 | 822 | cspContext); |
michael@0 | 823 | } |
michael@0 | 824 | |
michael@0 | 825 | // record whether the thing should be blocked or just reported. |
michael@0 | 826 | policyAllowsLoadArray.push(permitted || policy._reportOnlyMode); |
michael@0 | 827 | let res = permitted ? cp.ACCEPT : cp.REJECT_SERVER; |
michael@0 | 828 | |
michael@0 | 829 | // frame-ancestors is taken care of early on (as this document is loaded) |
michael@0 | 830 | |
michael@0 | 831 | // If the result is *NOT* ACCEPT, then send report |
michael@0 | 832 | // Do not send report if this is a nonce-source preload - the decision may |
michael@0 | 833 | // be wrong and will incorrectly fail the unit tests. |
michael@0 | 834 | if (res != cp.ACCEPT && !possiblePreloadNonceConflict) { |
michael@0 | 835 | CSPdebug("blocking request for " + aContentLocation.asciiSpec); |
michael@0 | 836 | try { |
michael@0 | 837 | let directive = "unknown directive", |
michael@0 | 838 | violatedPolicy = "unknown policy"; |
michael@0 | 839 | |
michael@0 | 840 | // The policy might not explicitly declare each source directive (so |
michael@0 | 841 | // the cspContext may be implicit). If so, we have to report |
michael@0 | 842 | // violations as appropriate: specific or the default-src directive. |
michael@0 | 843 | if (policy._directives.hasOwnProperty(cspContext)) { |
michael@0 | 844 | directive = policy._directives[cspContext]; |
michael@0 | 845 | violatedPolicy = cspContext + ' ' + directive.toString(); |
michael@0 | 846 | } else if (policy._directives.hasOwnProperty("default-src")) { |
michael@0 | 847 | directive = policy._directives["default-src"]; |
michael@0 | 848 | violatedPolicy = "default-src " + directive.toString(); |
michael@0 | 849 | } else { |
michael@0 | 850 | violatedPolicy = "unknown directive"; |
michael@0 | 851 | CSPdebug('ERROR in blocking content: ' + |
michael@0 | 852 | 'CSP is not sure which part of the policy caused this block'); |
michael@0 | 853 | } |
michael@0 | 854 | |
michael@0 | 855 | this._asyncReportViolation(aContentLocation, |
michael@0 | 856 | aOriginalUri, |
michael@0 | 857 | violatedPolicy, |
michael@0 | 858 | policyIndex); |
michael@0 | 859 | } catch(e) { |
michael@0 | 860 | CSPdebug('---------------- ERROR: ' + e); |
michael@0 | 861 | } |
michael@0 | 862 | } |
michael@0 | 863 | } // end for-each loop over policies |
michael@0 | 864 | |
michael@0 | 865 | // the ultimate decision is based on whether any policies want to reject |
michael@0 | 866 | // the load. The array keeps track of whether the policies allowed the |
michael@0 | 867 | // loads. If any doesn't, we'll reject the load (and cache the result). |
michael@0 | 868 | let ret = (policyAllowsLoadArray.some(function(a,b) { return !a; }) ? |
michael@0 | 869 | cp.REJECT_SERVER : cp.ACCEPT); |
michael@0 | 870 | |
michael@0 | 871 | // Do not cache the result if this is a nonce-source preload |
michael@0 | 872 | if (key && !possiblePreloadNonceConflict) { |
michael@0 | 873 | this._cache.set(key, ret); |
michael@0 | 874 | } |
michael@0 | 875 | return ret; |
michael@0 | 876 | }, |
michael@0 | 877 | |
michael@0 | 878 | shouldProcess: |
michael@0 | 879 | function csp_shouldProcess(aContentType, |
michael@0 | 880 | aContentLocation, |
michael@0 | 881 | aRequestOrigin, |
michael@0 | 882 | aContext, |
michael@0 | 883 | aMimeType, |
michael@0 | 884 | aExtra) { |
michael@0 | 885 | // frame-ancestors check is done outside the ContentPolicy |
michael@0 | 886 | var res = Ci.nsIContentPolicy.ACCEPT; |
michael@0 | 887 | CSPdebug("shouldProcess aContext=" + aContext); |
michael@0 | 888 | return res; |
michael@0 | 889 | }, |
michael@0 | 890 | |
michael@0 | 891 | /** |
michael@0 | 892 | * Asynchronously notifies any nsIObservers listening to the CSP violation |
michael@0 | 893 | * topic that a violation occurred. Also triggers report sending. All |
michael@0 | 894 | * asynchronous on the main thread. |
michael@0 | 895 | * |
michael@0 | 896 | * @param aBlockedContentSource |
michael@0 | 897 | * Either a CSP Source (like 'self', as string) or nsIURI: the source |
michael@0 | 898 | * of the violation. |
michael@0 | 899 | * @param aOriginalUri |
michael@0 | 900 | * The original URI if the blocked content is a redirect, else null |
michael@0 | 901 | * @param aViolatedDirective |
michael@0 | 902 | * the directive that was violated (string). |
michael@0 | 903 | * @param aViolatedPolicyIndex |
michael@0 | 904 | * the index of the policy that was violated (so we know where to send |
michael@0 | 905 | * the reports). |
michael@0 | 906 | * @param aObserverSubject |
michael@0 | 907 | * optional, subject sent to the nsIObservers listening to the CSP |
michael@0 | 908 | * violation topic. |
michael@0 | 909 | * @param aSourceFile |
michael@0 | 910 | * name of the file containing the inline script violation |
michael@0 | 911 | * @param aScriptSample |
michael@0 | 912 | * a sample of the violating inline script |
michael@0 | 913 | * @param aLineNum |
michael@0 | 914 | * source line number of the violation (if available) |
michael@0 | 915 | */ |
michael@0 | 916 | _asyncReportViolation: |
michael@0 | 917 | function(aBlockedContentSource, aOriginalUri, aViolatedDirective, |
michael@0 | 918 | aViolatedPolicyIndex, aObserverSubject, |
michael@0 | 919 | aSourceFile, aScriptSample, aLineNum) { |
michael@0 | 920 | // if optional observerSubject isn't specified, default to the source of |
michael@0 | 921 | // the violation. |
michael@0 | 922 | if (!aObserverSubject) |
michael@0 | 923 | aObserverSubject = aBlockedContentSource; |
michael@0 | 924 | |
michael@0 | 925 | // gotta wrap things that aren't nsISupports, since it's sent out to |
michael@0 | 926 | // observers as such. Objects that are not nsISupports are converted to |
michael@0 | 927 | // strings and then wrapped into a nsISupportsCString. |
michael@0 | 928 | if (!(aObserverSubject instanceof Ci.nsISupports)) { |
michael@0 | 929 | let d = aObserverSubject; |
michael@0 | 930 | aObserverSubject = Cc["@mozilla.org/supports-cstring;1"] |
michael@0 | 931 | .createInstance(Ci.nsISupportsCString); |
michael@0 | 932 | aObserverSubject.data = d; |
michael@0 | 933 | } |
michael@0 | 934 | |
michael@0 | 935 | var reportSender = this; |
michael@0 | 936 | Services.tm.mainThread.dispatch( |
michael@0 | 937 | function() { |
michael@0 | 938 | Services.obs.notifyObservers(aObserverSubject, |
michael@0 | 939 | CSP_VIOLATION_TOPIC, |
michael@0 | 940 | aViolatedDirective); |
michael@0 | 941 | reportSender.sendReports(aBlockedContentSource, aOriginalUri, |
michael@0 | 942 | aViolatedDirective, aViolatedPolicyIndex, |
michael@0 | 943 | aSourceFile, aScriptSample, aLineNum); |
michael@0 | 944 | reportSender.logToConsole(aBlockedContentSource, aOriginalUri, |
michael@0 | 945 | aViolatedDirective, aViolatedPolicyIndex, |
michael@0 | 946 | aSourceFile, aScriptSample, |
michael@0 | 947 | aLineNum, aObserverSubject); |
michael@0 | 948 | |
michael@0 | 949 | }, Ci.nsIThread.DISPATCH_NORMAL); |
michael@0 | 950 | }, |
michael@0 | 951 | |
michael@0 | 952 | /* ........ nsISerializable Methods: .............. */ |
michael@0 | 953 | |
michael@0 | 954 | read: |
michael@0 | 955 | function(aStream) { |
michael@0 | 956 | |
michael@0 | 957 | this._requestOrigin = aStream.readObject(true); |
michael@0 | 958 | this._requestOrigin.QueryInterface(Ci.nsIURI); |
michael@0 | 959 | |
michael@0 | 960 | for (let pCount = aStream.read32(); pCount > 0; pCount--) { |
michael@0 | 961 | let polStr = aStream.readString(); |
michael@0 | 962 | let reportOnly = aStream.readBoolean(); |
michael@0 | 963 | let specCompliant = aStream.readBoolean(); |
michael@0 | 964 | // don't need self info because when the policy is turned back into a |
michael@0 | 965 | // string, 'self' is replaced with the explicit source expression. |
michael@0 | 966 | this._appendPolicyInternal(polStr, null, reportOnly, specCompliant, |
michael@0 | 967 | false); |
michael@0 | 968 | } |
michael@0 | 969 | |
michael@0 | 970 | // NOTE: the document instance that's deserializing this object (via its |
michael@0 | 971 | // principal) should hook itself into this._principal manually. If they |
michael@0 | 972 | // don't, the CSP reports will likely be blocked by nsMixedContentBlocker. |
michael@0 | 973 | }, |
michael@0 | 974 | |
michael@0 | 975 | write: |
michael@0 | 976 | function(aStream) { |
michael@0 | 977 | // need to serialize the context: request origin and such. They are |
michael@0 | 978 | // used when sending reports. Since _request and _requestOrigin are just |
michael@0 | 979 | // different representations of the same thing, only save _requestOrigin |
michael@0 | 980 | // (an nsIURI). |
michael@0 | 981 | aStream.writeCompoundObject(this._requestOrigin, Ci.nsIURI, true); |
michael@0 | 982 | |
michael@0 | 983 | // we can't serialize a reference to the principal that triggered this |
michael@0 | 984 | // instance to serialize, so when this is deserialized by the principal the |
michael@0 | 985 | // caller must hook it up manually by calling setRequestContext on this |
michael@0 | 986 | // instance with the appropriate nsIChannel. |
michael@0 | 987 | |
michael@0 | 988 | // Finally, serialize all the policies. |
michael@0 | 989 | aStream.write32(this._policies.length); |
michael@0 | 990 | |
michael@0 | 991 | for each (var policy in this._policies) { |
michael@0 | 992 | aStream.writeWStringZ(policy.toString()); |
michael@0 | 993 | aStream.writeBoolean(policy._reportOnlyMode); |
michael@0 | 994 | aStream.writeBoolean(policy._specCompliant); |
michael@0 | 995 | } |
michael@0 | 996 | }, |
michael@0 | 997 | }; |
michael@0 | 998 | |
michael@0 | 999 | // The POST of the violation report (if it happens) should not follow |
michael@0 | 1000 | // redirects, per the spec. hence, we implement an nsIChannelEventSink |
michael@0 | 1001 | // with an object so we can tell XHR to abort if a redirect happens. |
michael@0 | 1002 | function CSPReportRedirectSink(policy) { |
michael@0 | 1003 | this._policy = policy; |
michael@0 | 1004 | } |
michael@0 | 1005 | |
michael@0 | 1006 | CSPReportRedirectSink.prototype = { |
michael@0 | 1007 | QueryInterface: function requestor_qi(iid) { |
michael@0 | 1008 | if (iid.equals(Ci.nsISupports) || |
michael@0 | 1009 | iid.equals(Ci.nsIInterfaceRequestor) || |
michael@0 | 1010 | iid.equals(Ci.nsIChannelEventSink)) |
michael@0 | 1011 | return this; |
michael@0 | 1012 | throw Cr.NS_ERROR_NO_INTERFACE; |
michael@0 | 1013 | }, |
michael@0 | 1014 | |
michael@0 | 1015 | // nsIInterfaceRequestor |
michael@0 | 1016 | getInterface: function requestor_gi(iid) { |
michael@0 | 1017 | if (iid.equals(Ci.nsIChannelEventSink)) |
michael@0 | 1018 | return this; |
michael@0 | 1019 | |
michael@0 | 1020 | throw Components.results.NS_ERROR_NO_INTERFACE; |
michael@0 | 1021 | }, |
michael@0 | 1022 | |
michael@0 | 1023 | // nsIChannelEventSink |
michael@0 | 1024 | asyncOnChannelRedirect: function channel_redirect(oldChannel, newChannel, |
michael@0 | 1025 | flags, callback) { |
michael@0 | 1026 | this._policy.log(WARN_FLAG, CSPLocalizer.getFormatStr("reportPostRedirect", [oldChannel.URI.asciiSpec])); |
michael@0 | 1027 | |
michael@0 | 1028 | // cancel the old channel so XHR failure callback happens |
michael@0 | 1029 | oldChannel.cancel(Cr.NS_ERROR_ABORT); |
michael@0 | 1030 | |
michael@0 | 1031 | // notify an observer that we have blocked the report POST due to a redirect, |
michael@0 | 1032 | // used in testing, do this async since we're in an async call now to begin with |
michael@0 | 1033 | Services.tm.mainThread.dispatch( |
michael@0 | 1034 | function() { |
michael@0 | 1035 | observerSubject = Cc["@mozilla.org/supports-cstring;1"] |
michael@0 | 1036 | .createInstance(Ci.nsISupportsCString); |
michael@0 | 1037 | observerSubject.data = oldChannel.URI.asciiSpec; |
michael@0 | 1038 | |
michael@0 | 1039 | Services.obs.notifyObservers(observerSubject, |
michael@0 | 1040 | CSP_VIOLATION_TOPIC, |
michael@0 | 1041 | "denied redirect while sending violation report"); |
michael@0 | 1042 | }, Ci.nsIThread.DISPATCH_NORMAL); |
michael@0 | 1043 | |
michael@0 | 1044 | // throw to stop the redirect happening |
michael@0 | 1045 | throw Cr.NS_BINDING_REDIRECTED; |
michael@0 | 1046 | } |
michael@0 | 1047 | }; |
michael@0 | 1048 | |
michael@0 | 1049 | this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentSecurityPolicy]); |