content/base/src/CSPUtils.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 * Content Security Policy Utilities
michael@0 7 *
michael@0 8 * Overview
michael@0 9 * This contains a set of classes and utilities for CSP. It is in this
michael@0 10 * separate file for testing purposes.
michael@0 11 */
michael@0 12
michael@0 13 const Cu = Components.utils;
michael@0 14 const Ci = Components.interfaces;
michael@0 15
michael@0 16 const WARN_FLAG = Ci.nsIScriptError.warningFlag;
michael@0 17 const ERROR_FLAG = Ci.nsIScriptError.ERROR_FLAG;
michael@0 18
michael@0 19 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 20 Cu.import("resource://gre/modules/Services.jsm");
michael@0 21
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "Services",
michael@0 23 "resource://gre/modules/Services.jsm");
michael@0 24
michael@0 25 // Module stuff
michael@0 26 this.EXPORTED_SYMBOLS = ["CSPRep", "CSPSourceList", "CSPSource", "CSPHost",
michael@0 27 "CSPdebug", "CSPViolationReportListener", "CSPLocalizer",
michael@0 28 "CSPPrefObserver"];
michael@0 29
michael@0 30 var STRINGS_URI = "chrome://global/locale/security/csp.properties";
michael@0 31
michael@0 32 // these are not exported
michael@0 33 var gIoService = Components.classes["@mozilla.org/network/io-service;1"]
michael@0 34 .getService(Ci.nsIIOService);
michael@0 35
michael@0 36 var gETLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"]
michael@0 37 .getService(Ci.nsIEffectiveTLDService);
michael@0 38
michael@0 39 // These regexps represent the concrete syntax on the w3 spec as of 7-5-2012
michael@0 40 // scheme = <scheme production from RFC 3986>
michael@0 41 const R_SCHEME = new RegExp ("([a-zA-Z0-9\\-]+)", 'i');
michael@0 42 const R_GETSCHEME = new RegExp ("^" + R_SCHEME.source + "(?=\\:)", 'i');
michael@0 43
michael@0 44 // scheme-source = scheme ":"
michael@0 45 const R_SCHEMESRC = new RegExp ("^" + R_SCHEME.source + "\\:$", 'i');
michael@0 46
michael@0 47 // host-char = ALPHA / DIGIT / "-"
michael@0 48 // For the app: protocol, we need to add {} to the valid character set
michael@0 49 const HOSTCHAR = "{}a-zA-Z0-9\\-";
michael@0 50 const R_HOSTCHAR = new RegExp ("[" + HOSTCHAR + "]", 'i');
michael@0 51
michael@0 52 // Complementary character set of HOSTCHAR (characters that can't appear)
michael@0 53 const R_COMP_HCHAR = new RegExp ("[^" + HOSTCHAR + "]", "i");
michael@0 54
michael@0 55 // Invalid character set for host strings (which can include dots and star)
michael@0 56 const R_INV_HCHAR = new RegExp ("[^" + HOSTCHAR + "\\.\\*]", 'i');
michael@0 57
michael@0 58
michael@0 59 // host = "*" / [ "*." ] 1*host-char *( "." 1*host-char )
michael@0 60 const R_HOST = new RegExp ("\\*|(((\\*\\.)?" + R_HOSTCHAR.source +
michael@0 61 "+)" + "(\\." + R_HOSTCHAR.source + "+)*)", 'i');
michael@0 62
michael@0 63 // port = ":" ( 1*DIGIT / "*" )
michael@0 64 const R_PORT = new RegExp ("(\\:([0-9]+|\\*))", 'i');
michael@0 65
michael@0 66 // host-source = [ scheme "://" ] host [ port path file ]
michael@0 67 const R_HOSTSRC = new RegExp ("^((" + R_SCHEME.source + "\\:\\/\\/)?("
michael@0 68 + R_HOST.source + ")"
michael@0 69 + R_PORT.source + "?)$", 'i');
michael@0 70
michael@0 71 function STRIP_INPUTDELIM(re) {
michael@0 72 return re.replace(/(^\^)|(\$$)/g, "");
michael@0 73 }
michael@0 74
michael@0 75 // ext-host-source = host-source "/" *( <VCHAR except ";" and ","> )
michael@0 76 // ; ext-host-source is reserved for future use.
michael@0 77 const R_VCHAR_EXCEPT = new RegExp("[!-+--:<-~]"); // ranges exclude , and ;
michael@0 78 const R_EXTHOSTSRC = new RegExp ("^" + STRIP_INPUTDELIM(R_HOSTSRC.source)
michael@0 79 + "\\/"
michael@0 80 + R_VCHAR_EXCEPT.source + "*$", 'i');
michael@0 81
michael@0 82 // keyword-source = "'self'" / "'unsafe-inline'" / "'unsafe-eval'"
michael@0 83 const R_KEYWORDSRC = new RegExp ("^('self'|'unsafe-inline'|'unsafe-eval')$", 'i');
michael@0 84
michael@0 85 const R_BASE64 = new RegExp ("([a-zA-Z0-9+/]+={0,2})");
michael@0 86
michael@0 87 // nonce-source = "'nonce-" nonce-value "'"
michael@0 88 // nonce-value = 1*( ALPHA / DIGIT / "+" / "/" )
michael@0 89 const R_NONCESRC = new RegExp ("^'nonce-" + R_BASE64.source + "'$");
michael@0 90
michael@0 91 // hash-source = "'" hash-algo "-" hash-value "'"
michael@0 92 // hash-algo = "sha256" / "sha384" / "sha512"
michael@0 93 // hash-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" )
michael@0 94 // Each algo must be a valid argument to nsICryptoHash.init
michael@0 95 const R_HASH_ALGOS = new RegExp ("(sha256|sha384|sha512)");
michael@0 96 const R_HASHSRC = new RegExp ("^'" + R_HASH_ALGOS.source + "-" + R_BASE64.source + "'$");
michael@0 97
michael@0 98 // source-exp = scheme-source / host-source / keyword-source
michael@0 99 const R_SOURCEEXP = new RegExp (R_SCHEMESRC.source + "|" +
michael@0 100 R_HOSTSRC.source + "|" +
michael@0 101 R_EXTHOSTSRC.source + "|" +
michael@0 102 R_KEYWORDSRC.source + "|" +
michael@0 103 R_NONCESRC.source + "|" +
michael@0 104 R_HASHSRC.source, 'i');
michael@0 105
michael@0 106 const R_QUOTELESS_KEYWORDS = new RegExp ("^(self|unsafe-inline|unsafe-eval|" +
michael@0 107 "inline-script|eval-script|none)$", 'i');
michael@0 108
michael@0 109 this.CSPPrefObserver = {
michael@0 110 get debugEnabled () {
michael@0 111 if (!this._branch)
michael@0 112 this._initialize();
michael@0 113 return this._debugEnabled;
michael@0 114 },
michael@0 115
michael@0 116 get experimentalEnabled () {
michael@0 117 if (!this._branch)
michael@0 118 this._initialize();
michael@0 119 return this._experimentalEnabled;
michael@0 120 },
michael@0 121
michael@0 122 _initialize: function() {
michael@0 123 var prefSvc = Components.classes["@mozilla.org/preferences-service;1"]
michael@0 124 .getService(Ci.nsIPrefService);
michael@0 125 this._branch = prefSvc.getBranch("security.csp.");
michael@0 126 this._branch.addObserver("", this, false);
michael@0 127 this._debugEnabled = this._branch.getBoolPref("debug");
michael@0 128 this._experimentalEnabled = this._branch.getBoolPref("experimentalEnabled");
michael@0 129 },
michael@0 130
michael@0 131 unregister: function() {
michael@0 132 if (!this._branch) return;
michael@0 133 this._branch.removeObserver("", this);
michael@0 134 },
michael@0 135
michael@0 136 observe: function(aSubject, aTopic, aData) {
michael@0 137 if (aTopic != "nsPref:changed") return;
michael@0 138 if (aData === "debug")
michael@0 139 this._debugEnabled = this._branch.getBoolPref("debug");
michael@0 140 if (aData === "experimentalEnabled")
michael@0 141 this._experimentalEnabled = this._branch.getBoolPref("experimentalEnabled");
michael@0 142 },
michael@0 143 };
michael@0 144
michael@0 145 this.CSPdebug = function CSPdebug(aMsg) {
michael@0 146 if (!CSPPrefObserver.debugEnabled) return;
michael@0 147
michael@0 148 aMsg = 'CSP debug: ' + aMsg + "\n";
michael@0 149 Components.classes["@mozilla.org/consoleservice;1"]
michael@0 150 .getService(Ci.nsIConsoleService)
michael@0 151 .logStringMessage(aMsg);
michael@0 152 }
michael@0 153
michael@0 154 // Callback to resume a request once the policy-uri has been fetched
michael@0 155 function CSPPolicyURIListener(policyURI, docRequest, csp, reportOnly) {
michael@0 156 this._policyURI = policyURI; // location of remote policy
michael@0 157 this._docRequest = docRequest; // the parent document request
michael@0 158 this._csp = csp; // parent document's CSP
michael@0 159 this._policy = ""; // contents fetched from policyURI
michael@0 160 this._wrapper = null; // nsIScriptableInputStream
michael@0 161 this._docURI = docRequest.QueryInterface(Ci.nsIChannel)
michael@0 162 .URI; // parent document URI (to be used as 'self')
michael@0 163 this._reportOnly = reportOnly;
michael@0 164 }
michael@0 165
michael@0 166 CSPPolicyURIListener.prototype = {
michael@0 167
michael@0 168 QueryInterface: function(iid) {
michael@0 169 if (iid.equals(Ci.nsIStreamListener) ||
michael@0 170 iid.equals(Ci.nsIRequestObserver) ||
michael@0 171 iid.equals(Ci.nsISupports))
michael@0 172 return this;
michael@0 173 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 174 },
michael@0 175
michael@0 176 onStartRequest:
michael@0 177 function(request, context) {},
michael@0 178
michael@0 179 onDataAvailable:
michael@0 180 function(request, context, inputStream, offset, count) {
michael@0 181 if (this._wrapper == null) {
michael@0 182 this._wrapper = Components.classes["@mozilla.org/scriptableinputstream;1"]
michael@0 183 .createInstance(Ci.nsIScriptableInputStream);
michael@0 184 this._wrapper.init(inputStream);
michael@0 185 }
michael@0 186 // store the remote policy as it becomes available
michael@0 187 this._policy += this._wrapper.read(count);
michael@0 188 },
michael@0 189
michael@0 190 onStopRequest:
michael@0 191 function(request, context, status) {
michael@0 192 if (Components.isSuccessCode(status)) {
michael@0 193 // send the policy we received back to the parent document's CSP
michael@0 194 // for parsing
michael@0 195 this._csp.appendPolicy(this._policy, this._docURI,
michael@0 196 this._reportOnly, this._csp._specCompliant);
michael@0 197 }
michael@0 198 else {
michael@0 199 // problem fetching policy so fail closed by appending a "block it all"
michael@0 200 // policy. Also toss an error into the console so developers can see why
michael@0 201 // this policy is used.
michael@0 202 this._csp.log(WARN_FLAG, CSPLocalizer.getFormatStr("errorFetchingPolicy",
michael@0 203 [status]));
michael@0 204 this._csp.appendPolicy("default-src 'none'", this._docURI,
michael@0 205 this._reportOnly, this._csp._specCompliant);
michael@0 206 }
michael@0 207 // resume the parent document request
michael@0 208 this._docRequest.resume();
michael@0 209 }
michael@0 210 };
michael@0 211
michael@0 212 //:::::::::::::::::::::::: CLASSES :::::::::::::::::::::::::://
michael@0 213
michael@0 214 /**
michael@0 215 * Class that represents a parsed policy structure.
michael@0 216 *
michael@0 217 * @param aSpecCompliant: true: this policy is a CSP 1.0 spec
michael@0 218 * compliant policy and should be parsed as such.
michael@0 219 * false or undefined: this is a policy using
michael@0 220 * our original implementation's CSP syntax.
michael@0 221 */
michael@0 222 this.CSPRep = function CSPRep(aSpecCompliant) {
michael@0 223 // this gets set to true when the policy is done parsing, or when a
michael@0 224 // URI-borne policy has finished loading.
michael@0 225 this._isInitialized = false;
michael@0 226
michael@0 227 this._allowEval = false;
michael@0 228 this._allowInlineScripts = false;
michael@0 229 this._reportOnlyMode = false;
michael@0 230
michael@0 231 // don't auto-populate _directives, so it is easier to find bugs
michael@0 232 this._directives = {};
michael@0 233
michael@0 234 // Is this a 1.0 spec compliant CSPRep ?
michael@0 235 // Default to false if not specified.
michael@0 236 this._specCompliant = (aSpecCompliant !== undefined) ? aSpecCompliant : false;
michael@0 237
michael@0 238 // Only CSP 1.0 spec compliant policies block inline styles by default.
michael@0 239 this._allowInlineStyles = !aSpecCompliant;
michael@0 240 }
michael@0 241
michael@0 242 // Source directives for our original CSP implementation.
michael@0 243 // These can be removed when the original implementation is deprecated.
michael@0 244 CSPRep.SRC_DIRECTIVES_OLD = {
michael@0 245 DEFAULT_SRC: "default-src",
michael@0 246 SCRIPT_SRC: "script-src",
michael@0 247 STYLE_SRC: "style-src",
michael@0 248 MEDIA_SRC: "media-src",
michael@0 249 IMG_SRC: "img-src",
michael@0 250 OBJECT_SRC: "object-src",
michael@0 251 FRAME_SRC: "frame-src",
michael@0 252 FRAME_ANCESTORS: "frame-ancestors",
michael@0 253 FONT_SRC: "font-src",
michael@0 254 XHR_SRC: "xhr-src"
michael@0 255 };
michael@0 256
michael@0 257 // Source directives for our CSP 1.0 spec compliant implementation.
michael@0 258 CSPRep.SRC_DIRECTIVES_NEW = {
michael@0 259 DEFAULT_SRC: "default-src",
michael@0 260 SCRIPT_SRC: "script-src",
michael@0 261 STYLE_SRC: "style-src",
michael@0 262 MEDIA_SRC: "media-src",
michael@0 263 IMG_SRC: "img-src",
michael@0 264 OBJECT_SRC: "object-src",
michael@0 265 FRAME_SRC: "frame-src",
michael@0 266 FRAME_ANCESTORS: "frame-ancestors",
michael@0 267 FONT_SRC: "font-src",
michael@0 268 CONNECT_SRC: "connect-src"
michael@0 269 };
michael@0 270
michael@0 271 CSPRep.URI_DIRECTIVES = {
michael@0 272 REPORT_URI: "report-uri", /* list of URIs */
michael@0 273 POLICY_URI: "policy-uri" /* single URI */
michael@0 274 };
michael@0 275
michael@0 276 // These directives no longer exist in CSP 1.0 and
michael@0 277 // later and will eventually be removed when we no longer
michael@0 278 // support our original implementation's syntax.
michael@0 279 CSPRep.OPTIONS_DIRECTIVE = "options";
michael@0 280 CSPRep.ALLOW_DIRECTIVE = "allow";
michael@0 281
michael@0 282 /**
michael@0 283 * Factory to create a new CSPRep, parsed from a string.
michael@0 284 *
michael@0 285 * @param aStr
michael@0 286 * string rep of a CSP
michael@0 287 * @param self (optional)
michael@0 288 * URI representing the "self" source
michael@0 289 * @param reportOnly (optional)
michael@0 290 * whether or not this CSP is report-only (defaults to false)
michael@0 291 * @param docRequest (optional)
michael@0 292 * request for the parent document which may need to be suspended
michael@0 293 * while the policy-uri is asynchronously fetched
michael@0 294 * @param csp (optional)
michael@0 295 * the CSP object to update once the policy has been fetched
michael@0 296 * @param enforceSelfChecks (optional)
michael@0 297 * if present, and "true", will check to be sure "self" has the
michael@0 298 * appropriate values to inherit when they are omitted from the source.
michael@0 299 * @returns
michael@0 300 * an instance of CSPRep
michael@0 301 */
michael@0 302 CSPRep.fromString = function(aStr, self, reportOnly, docRequest, csp,
michael@0 303 enforceSelfChecks) {
michael@0 304 var SD = CSPRep.SRC_DIRECTIVES_OLD;
michael@0 305 var UD = CSPRep.URI_DIRECTIVES;
michael@0 306 var aCSPR = new CSPRep();
michael@0 307 aCSPR._originalText = aStr;
michael@0 308 aCSPR._innerWindowID = innerWindowFromRequest(docRequest);
michael@0 309 if (typeof reportOnly === 'undefined') reportOnly = false;
michael@0 310 aCSPR._reportOnlyMode = reportOnly;
michael@0 311
michael@0 312 var selfUri = null;
michael@0 313 if (self instanceof Ci.nsIURI) {
michael@0 314 selfUri = self.cloneIgnoringRef();
michael@0 315 // clean userpass out of the URI (not used for CSP origin checking, but
michael@0 316 // shows up in prePath).
michael@0 317 try {
michael@0 318 // GetUserPass throws for some protocols without userPass
michael@0 319 selfUri.userPass = '';
michael@0 320 } catch (ex) {}
michael@0 321 }
michael@0 322
michael@0 323 var dirs = aStr.split(";");
michael@0 324
michael@0 325 directive:
michael@0 326 for each(var dir in dirs) {
michael@0 327 dir = dir.trim();
michael@0 328 if (dir.length < 1) continue;
michael@0 329
michael@0 330 var dirname = dir.split(/\s+/)[0].toLowerCase();
michael@0 331 var dirvalue = dir.substring(dirname.length).trim();
michael@0 332
michael@0 333 if (aCSPR._directives.hasOwnProperty(dirname)) {
michael@0 334 // Check for (most) duplicate directives
michael@0 335 cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective",
michael@0 336 [dirname]));
michael@0 337 CSPdebug("Skipping duplicate directive: \"" + dir + "\"");
michael@0 338 continue directive;
michael@0 339 }
michael@0 340
michael@0 341 // OPTIONS DIRECTIVE ////////////////////////////////////////////////
michael@0 342 if (dirname === CSPRep.OPTIONS_DIRECTIVE) {
michael@0 343 if (aCSPR._allowInlineScripts || aCSPR._allowEval) {
michael@0 344 // Check for duplicate options directives
michael@0 345 cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective",
michael@0 346 [dirname]));
michael@0 347 CSPdebug("Skipping duplicate directive: \"" + dir + "\"");
michael@0 348 continue directive;
michael@0 349 }
michael@0 350
michael@0 351 // grab value tokens and interpret them
michael@0 352 var options = dirvalue.split(/\s+/);
michael@0 353 for each (var opt in options) {
michael@0 354 if (opt === "inline-script")
michael@0 355 aCSPR._allowInlineScripts = true;
michael@0 356 else if (opt === "eval-script")
michael@0 357 aCSPR._allowEval = true;
michael@0 358 else
michael@0 359 cspWarn(aCSPR, CSPLocalizer.getFormatStr("ignoringUnknownOption",
michael@0 360 [opt]));
michael@0 361 }
michael@0 362 continue directive;
michael@0 363 }
michael@0 364
michael@0 365 // ALLOW DIRECTIVE //////////////////////////////////////////////////
michael@0 366 // parse "allow" as equivalent to "default-src", at least until the spec
michael@0 367 // stabilizes, at which time we can stop parsing "allow"
michael@0 368 if (dirname === CSPRep.ALLOW_DIRECTIVE) {
michael@0 369 cspWarn(aCSPR, CSPLocalizer.getStr("allowDirectiveIsDeprecated"));
michael@0 370 if (aCSPR._directives.hasOwnProperty(SD.DEFAULT_SRC)) {
michael@0 371 // Check for duplicate default-src and allow directives
michael@0 372 cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective",
michael@0 373 [dirname]));
michael@0 374 CSPdebug("Skipping duplicate directive: \"" + dir + "\"");
michael@0 375 continue directive;
michael@0 376 }
michael@0 377 var dv = CSPSourceList.fromString(dirvalue, aCSPR, selfUri,
michael@0 378 enforceSelfChecks);
michael@0 379 if (dv) {
michael@0 380 aCSPR._directives[SD.DEFAULT_SRC] = dv;
michael@0 381 continue directive;
michael@0 382 }
michael@0 383 }
michael@0 384
michael@0 385 // SOURCE DIRECTIVES ////////////////////////////////////////////////
michael@0 386 for each(var sdi in SD) {
michael@0 387 if (dirname === sdi) {
michael@0 388 // process dirs, and enforce that 'self' is defined.
michael@0 389 var dv = CSPSourceList.fromString(dirvalue, aCSPR, selfUri,
michael@0 390 enforceSelfChecks);
michael@0 391 if (dv) {
michael@0 392 aCSPR._directives[sdi] = dv;
michael@0 393 continue directive;
michael@0 394 }
michael@0 395 }
michael@0 396 }
michael@0 397
michael@0 398 // REPORT URI ///////////////////////////////////////////////////////
michael@0 399 if (dirname === UD.REPORT_URI) {
michael@0 400 // might be space-separated list of URIs
michael@0 401 var uriStrings = dirvalue.split(/\s+/);
michael@0 402 var okUriStrings = [];
michael@0 403
michael@0 404 for (let i in uriStrings) {
michael@0 405 var uri = null;
michael@0 406 try {
michael@0 407 // Relative URIs are okay, but to ensure we send the reports to the
michael@0 408 // right spot, the relative URIs are expanded here during parsing.
michael@0 409 // The resulting CSPRep instance will have only absolute URIs.
michael@0 410 uri = gIoService.newURI(uriStrings[i],null,selfUri);
michael@0 411
michael@0 412 // if there's no host, this will throw NS_ERROR_FAILURE, causing a
michael@0 413 // parse failure.
michael@0 414 uri.host;
michael@0 415
michael@0 416 // warn about, but do not prohibit non-http and non-https schemes for
michael@0 417 // reporting URIs. The spec allows unrestricted URIs resolved
michael@0 418 // relative to "self", but we should let devs know if the scheme is
michael@0 419 // abnormal and may fail a POST.
michael@0 420 if (!uri.schemeIs("http") && !uri.schemeIs("https")) {
michael@0 421 cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotHttpsOrHttp2",
michael@0 422 [uri.asciiSpec]));
michael@0 423 }
michael@0 424 } catch(e) {
michael@0 425 switch (e.result) {
michael@0 426 case Components.results.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS:
michael@0 427 case Components.results.NS_ERROR_HOST_IS_IP_ADDRESS:
michael@0 428 if (uri.host !== selfUri.host) {
michael@0 429 cspWarn(aCSPR,
michael@0 430 CSPLocalizer.getFormatStr("pageCannotSendReportsTo",
michael@0 431 [selfUri.host, uri.host]));
michael@0 432 continue;
michael@0 433 }
michael@0 434 break;
michael@0 435
michael@0 436 default:
michael@0 437 cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotParseReportURI",
michael@0 438 [uriStrings[i]]));
michael@0 439 continue;
michael@0 440 }
michael@0 441 }
michael@0 442 // all verification passed
michael@0 443 okUriStrings.push(uri.asciiSpec);
michael@0 444 }
michael@0 445 aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' ');
michael@0 446 continue directive;
michael@0 447 }
michael@0 448
michael@0 449 // POLICY URI //////////////////////////////////////////////////////////
michael@0 450 if (dirname === UD.POLICY_URI) {
michael@0 451 // POLICY_URI can only be alone
michael@0 452 if (aCSPR._directives.length > 0 || dirs.length > 1) {
michael@0 453 cspError(aCSPR, CSPLocalizer.getStr("policyURINotAlone"));
michael@0 454 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 455 }
michael@0 456 // if we were called without a reference to the parent document request
michael@0 457 // we won't be able to suspend it while we fetch the policy -> fail closed
michael@0 458 if (!docRequest || !csp) {
michael@0 459 cspError(aCSPR, CSPLocalizer.getStr("noParentRequest"));
michael@0 460 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 461 }
michael@0 462
michael@0 463 var uri = '';
michael@0 464 try {
michael@0 465 uri = gIoService.newURI(dirvalue, null, selfUri);
michael@0 466 } catch(e) {
michael@0 467 cspError(aCSPR, CSPLocalizer.getFormatStr("policyURIParseError",
michael@0 468 [dirvalue]));
michael@0 469 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 470 }
michael@0 471
michael@0 472 // Verify that policy URI comes from the same origin
michael@0 473 if (selfUri) {
michael@0 474 if (selfUri.host !== uri.host) {
michael@0 475 cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingHost",
michael@0 476 [uri.host]));
michael@0 477 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 478 }
michael@0 479 if (selfUri.port !== uri.port) {
michael@0 480 cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingPort",
michael@0 481 [uri.port.toString()]));
michael@0 482 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 483 }
michael@0 484 if (selfUri.scheme !== uri.scheme) {
michael@0 485 cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingScheme",
michael@0 486 [uri.scheme]));
michael@0 487 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 488 }
michael@0 489 }
michael@0 490
michael@0 491 // suspend the parent document request while we fetch the policy-uri
michael@0 492 try {
michael@0 493 docRequest.suspend();
michael@0 494 var chan = gIoService.newChannel(uri.asciiSpec, null, null);
michael@0 495 // make request anonymous (no cookies, etc.) so the request for the
michael@0 496 // policy-uri can't be abused for CSRF
michael@0 497 chan.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
michael@0 498 chan.loadGroup = docRequest.loadGroup;
michael@0 499 chan.asyncOpen(new CSPPolicyURIListener(uri, docRequest, csp, reportOnly), null);
michael@0 500 }
michael@0 501 catch (e) {
michael@0 502 // resume the document request and apply most restrictive policy
michael@0 503 docRequest.resume();
michael@0 504 cspError(aCSPR, CSPLocalizer.getFormatStr("errorFetchingPolicy",
michael@0 505 [e.toString()]));
michael@0 506 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 507 }
michael@0 508
michael@0 509 // return a fully-open policy to be used until the contents of the
michael@0 510 // policy-uri come back.
michael@0 511 return CSPRep.fromString("default-src *", null, reportOnly);
michael@0 512 }
michael@0 513
michael@0 514 // UNIDENTIFIED DIRECTIVE /////////////////////////////////////////////
michael@0 515 cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotProcessUnknownDirective",
michael@0 516 [dirname]));
michael@0 517
michael@0 518 } // end directive: loop
michael@0 519
michael@0 520 // the X-Content-Security-Policy syntax requires an allow or default-src
michael@0 521 // directive to be present.
michael@0 522 if (!aCSPR._directives[SD.DEFAULT_SRC]) {
michael@0 523 cspWarn(aCSPR, CSPLocalizer.getStr("allowOrDefaultSrcRequired"));
michael@0 524 return CSPRep.fromString("default-src 'none'", null, reportOnly);
michael@0 525 }
michael@0 526
michael@0 527 // If this is a Report-Only header and report-uri is not in the directive
michael@0 528 // list, tell developer either specify report-uri directive or use
michael@0 529 // a non-Report-Only CSP header.
michael@0 530 if (aCSPR._reportOnlyMode && !aCSPR._directives.hasOwnProperty(UD.REPORT_URI)) {
michael@0 531 cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotInReportOnlyHeader",
michael@0 532 [selfUri ? selfUri.prePath : "undefined"]))
michael@0 533 }
michael@0 534
michael@0 535 return aCSPR;
michael@0 536 };
michael@0 537
michael@0 538 /**
michael@0 539 * Factory to create a new CSPRep, parsed from a string, compliant
michael@0 540 * with the CSP 1.0 spec.
michael@0 541 *
michael@0 542 * @param aStr
michael@0 543 * string rep of a CSP
michael@0 544 * @param self (optional)
michael@0 545 * URI representing the "self" source
michael@0 546 * @param reportOnly (optional)
michael@0 547 * whether or not this CSP is report-only (defaults to false)
michael@0 548 * @param docRequest (optional)
michael@0 549 * request for the parent document which may need to be suspended
michael@0 550 * while the policy-uri is asynchronously fetched
michael@0 551 * @param csp (optional)
michael@0 552 * the CSP object to update once the policy has been fetched
michael@0 553 * @param enforceSelfChecks (optional)
michael@0 554 * if present, and "true", will check to be sure "self" has the
michael@0 555 * appropriate values to inherit when they are omitted from the source.
michael@0 556 * @returns
michael@0 557 * an instance of CSPRep
michael@0 558 */
michael@0 559 // When we deprecate our original CSP implementation, we rename this to
michael@0 560 // CSPRep.fromString and remove the existing CSPRep.fromString above.
michael@0 561 CSPRep.fromStringSpecCompliant = function(aStr, self, reportOnly, docRequest, csp,
michael@0 562 enforceSelfChecks) {
michael@0 563 var SD = CSPRep.SRC_DIRECTIVES_NEW;
michael@0 564 var UD = CSPRep.URI_DIRECTIVES;
michael@0 565 var aCSPR = new CSPRep(true);
michael@0 566 aCSPR._originalText = aStr;
michael@0 567 aCSPR._innerWindowID = innerWindowFromRequest(docRequest);
michael@0 568 if (typeof reportOnly === 'undefined') reportOnly = false;
michael@0 569 aCSPR._reportOnlyMode = reportOnly;
michael@0 570
michael@0 571 var selfUri = null;
michael@0 572 if (self instanceof Ci.nsIURI) {
michael@0 573 selfUri = self.cloneIgnoringRef();
michael@0 574 // clean userpass out of the URI (not used for CSP origin checking, but
michael@0 575 // shows up in prePath).
michael@0 576 try {
michael@0 577 // GetUserPass throws for some protocols without userPass
michael@0 578 selfUri.userPass = '';
michael@0 579 } catch (ex) {}
michael@0 580 }
michael@0 581
michael@0 582 var dirs_list = aStr.split(";");
michael@0 583 var dirs = {};
michael@0 584 for each(var dir in dirs_list) {
michael@0 585 dir = dir.trim();
michael@0 586 if (dir.length < 1) continue;
michael@0 587
michael@0 588 var dirname = dir.split(/\s+/)[0].toLowerCase();
michael@0 589 var dirvalue = dir.substring(dirname.length).trim();
michael@0 590 dirs[dirname] = dirvalue;
michael@0 591 }
michael@0 592
michael@0 593 // Spec compliant policies have different default behavior for inline
michael@0 594 // scripts, styles, and eval. Bug 885433
michael@0 595 aCSPR._allowEval = true;
michael@0 596 aCSPR._allowInlineScripts = true;
michael@0 597 aCSPR._allowInlineStyles = true;
michael@0 598
michael@0 599 // In CSP 1.0, you need to opt-in to blocking inline scripts and eval by
michael@0 600 // specifying either default-src or script-src, and to blocking inline
michael@0 601 // styles by specifying either default-src or style-src.
michael@0 602 if ("default-src" in dirs) {
michael@0 603 // Parse the source list (look ahead) so we can set the defaults properly,
michael@0 604 // honoring the 'unsafe-inline' and 'unsafe-eval' keywords
michael@0 605 var defaultSrcValue = CSPSourceList.fromString(dirs["default-src"], null, self);
michael@0 606 if (!defaultSrcValue._allowUnsafeInline) {
michael@0 607 aCSPR._allowInlineScripts = false;
michael@0 608 aCSPR._allowInlineStyles = false;
michael@0 609 }
michael@0 610 if (!defaultSrcValue._allowUnsafeEval) {
michael@0 611 aCSPR._allowEval = false;
michael@0 612 }
michael@0 613 }
michael@0 614 if ("script-src" in dirs) {
michael@0 615 aCSPR._allowInlineScripts = false;
michael@0 616 aCSPR._allowEval = false;
michael@0 617 }
michael@0 618 if ("style-src" in dirs) {
michael@0 619 aCSPR._allowInlineStyles = false;
michael@0 620 }
michael@0 621
michael@0 622 directive:
michael@0 623 for (var dirname in dirs) {
michael@0 624 var dirvalue = dirs[dirname];
michael@0 625
michael@0 626 if (aCSPR._directives.hasOwnProperty(dirname)) {
michael@0 627 // Check for (most) duplicate directives
michael@0 628 cspError(aCSPR, CSPLocalizer.getFormatStr("duplicateDirective",
michael@0 629 [dirname]));
michael@0 630 CSPdebug("Skipping duplicate directive: \"" + dir + "\"");
michael@0 631 continue directive;
michael@0 632 }
michael@0 633
michael@0 634 // SOURCE DIRECTIVES ////////////////////////////////////////////////
michael@0 635 for each(var sdi in SD) {
michael@0 636 if (dirname === sdi) {
michael@0 637 // process dirs, and enforce that 'self' is defined.
michael@0 638 var dv = CSPSourceList.fromString(dirvalue, aCSPR, self,
michael@0 639 enforceSelfChecks);
michael@0 640 if (dv) {
michael@0 641 // Check for unsafe-inline in style-src
michael@0 642 if (sdi === "style-src" && dv._allowUnsafeInline) {
michael@0 643 aCSPR._allowInlineStyles = true;
michael@0 644 } else if (sdi === "script-src") {
michael@0 645 // Check for unsafe-inline and unsafe-eval in script-src
michael@0 646 if (dv._allowUnsafeInline) {
michael@0 647 aCSPR._allowInlineScripts = true;
michael@0 648 }
michael@0 649 if (dv._allowUnsafeEval) {
michael@0 650 aCSPR._allowEval = true;
michael@0 651 }
michael@0 652 }
michael@0 653
michael@0 654 aCSPR._directives[sdi] = dv;
michael@0 655 continue directive;
michael@0 656 }
michael@0 657 }
michael@0 658 }
michael@0 659
michael@0 660 // REPORT URI ///////////////////////////////////////////////////////
michael@0 661 if (dirname === UD.REPORT_URI) {
michael@0 662 // might be space-separated list of URIs
michael@0 663 var uriStrings = dirvalue.split(/\s+/);
michael@0 664 var okUriStrings = [];
michael@0 665
michael@0 666 for (let i in uriStrings) {
michael@0 667 var uri = null;
michael@0 668 try {
michael@0 669 // Relative URIs are okay, but to ensure we send the reports to the
michael@0 670 // right spot, the relative URIs are expanded here during parsing.
michael@0 671 // The resulting CSPRep instance will have only absolute URIs.
michael@0 672 uri = gIoService.newURI(uriStrings[i],null,selfUri);
michael@0 673
michael@0 674 // if there's no host, this will throw NS_ERROR_FAILURE, causing a
michael@0 675 // parse failure.
michael@0 676 uri.host;
michael@0 677
michael@0 678 // warn about, but do not prohibit non-http and non-https schemes for
michael@0 679 // reporting URIs. The spec allows unrestricted URIs resolved
michael@0 680 // relative to "self", but we should let devs know if the scheme is
michael@0 681 // abnormal and may fail a POST.
michael@0 682 if (!uri.schemeIs("http") && !uri.schemeIs("https")) {
michael@0 683 cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotHttpsOrHttp2",
michael@0 684 [uri.asciiSpec]));
michael@0 685 }
michael@0 686 } catch(e) {
michael@0 687 switch (e.result) {
michael@0 688 case Components.results.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS:
michael@0 689 case Components.results.NS_ERROR_HOST_IS_IP_ADDRESS:
michael@0 690 if (uri.host !== selfUri.host) {
michael@0 691 cspWarn(aCSPR, CSPLocalizer.getFormatStr("pageCannotSendReportsTo",
michael@0 692 [selfUri.host, uri.host]));
michael@0 693 continue;
michael@0 694 }
michael@0 695 break;
michael@0 696
michael@0 697 default:
michael@0 698 cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotParseReportURI",
michael@0 699 [uriStrings[i]]));
michael@0 700 continue;
michael@0 701 }
michael@0 702 }
michael@0 703 // all verification passed.
michael@0 704 okUriStrings.push(uri.asciiSpec);
michael@0 705 }
michael@0 706 aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' ');
michael@0 707 continue directive;
michael@0 708 }
michael@0 709
michael@0 710 // POLICY URI //////////////////////////////////////////////////////////
michael@0 711 if (dirname === UD.POLICY_URI) {
michael@0 712 // POLICY_URI can only be alone
michael@0 713 if (aCSPR._directives.length > 0 || dirs.length > 1) {
michael@0 714 cspError(aCSPR, CSPLocalizer.getStr("policyURINotAlone"));
michael@0 715 return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly);
michael@0 716 }
michael@0 717 // if we were called without a reference to the parent document request
michael@0 718 // we won't be able to suspend it while we fetch the policy -> fail closed
michael@0 719 if (!docRequest || !csp) {
michael@0 720 cspError(aCSPR, CSPLocalizer.getStr("noParentRequest"));
michael@0 721 return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly);
michael@0 722 }
michael@0 723
michael@0 724 var uri = '';
michael@0 725 try {
michael@0 726 uri = gIoService.newURI(dirvalue, null, selfUri);
michael@0 727 } catch(e) {
michael@0 728 cspError(aCSPR, CSPLocalizer.getFormatStr("policyURIParseError", [dirvalue]));
michael@0 729 return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly);
michael@0 730 }
michael@0 731
michael@0 732 // Verify that policy URI comes from the same origin
michael@0 733 if (selfUri) {
michael@0 734 if (selfUri.host !== uri.host){
michael@0 735 cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingHost", [uri.host]));
michael@0 736 return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly);
michael@0 737 }
michael@0 738 if (selfUri.port !== uri.port){
michael@0 739 cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingPort", [uri.port.toString()]));
michael@0 740 return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly);
michael@0 741 }
michael@0 742 if (selfUri.scheme !== uri.scheme){
michael@0 743 cspError(aCSPR, CSPLocalizer.getFormatStr("nonMatchingScheme", [uri.scheme]));
michael@0 744 return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly);
michael@0 745 }
michael@0 746 }
michael@0 747
michael@0 748 // suspend the parent document request while we fetch the policy-uri
michael@0 749 try {
michael@0 750 docRequest.suspend();
michael@0 751 var chan = gIoService.newChannel(uri.asciiSpec, null, null);
michael@0 752 // make request anonymous (no cookies, etc.) so the request for the
michael@0 753 // policy-uri can't be abused for CSRF
michael@0 754 chan.loadFlags |= Components.interfaces.nsIChannel.LOAD_ANONYMOUS;
michael@0 755 chan.loadGroup = docRequest.loadGroup;
michael@0 756 chan.asyncOpen(new CSPPolicyURIListener(uri, docRequest, csp, reportOnly), null);
michael@0 757 }
michael@0 758 catch (e) {
michael@0 759 // resume the document request and apply most restrictive policy
michael@0 760 docRequest.resume();
michael@0 761 cspError(aCSPR, CSPLocalizer.getFormatStr("errorFetchingPolicy", [e.toString()]));
michael@0 762 return CSPRep.fromStringSpecCompliant("default-src 'none'", null, reportOnly);
michael@0 763 }
michael@0 764
michael@0 765 // return a fully-open policy to be used until the contents of the
michael@0 766 // policy-uri come back
michael@0 767 return CSPRep.fromStringSpecCompliant("default-src *", null, reportOnly);
michael@0 768 }
michael@0 769
michael@0 770 // UNIDENTIFIED DIRECTIVE /////////////////////////////////////////////
michael@0 771 cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotProcessUnknownDirective", [dirname]));
michael@0 772
michael@0 773 } // end directive: loop
michael@0 774
michael@0 775 // If this is a Report-Only header and report-uri is not in the directive
michael@0 776 // list, tell developer either specify report-uri directive or use
michael@0 777 // a non-Report-Only CSP header.
michael@0 778 if (aCSPR._reportOnlyMode && !aCSPR._directives.hasOwnProperty(UD.REPORT_URI)) {
michael@0 779 cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotInReportOnlyHeader",
michael@0 780 [selfUri ? selfUri.prePath : "undefined"]));
michael@0 781 }
michael@0 782
michael@0 783 return aCSPR;
michael@0 784 };
michael@0 785
michael@0 786 CSPRep.prototype = {
michael@0 787 /**
michael@0 788 * Returns a space-separated list of all report uris defined, or 'none' if there are none.
michael@0 789 */
michael@0 790 getReportURIs:
michael@0 791 function() {
michael@0 792 if (!this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI])
michael@0 793 return "";
michael@0 794 return this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI];
michael@0 795 },
michael@0 796
michael@0 797 /**
michael@0 798 * Compares this CSPRep instance to another.
michael@0 799 */
michael@0 800 equals:
michael@0 801 function(that) {
michael@0 802 if (this._directives.length != that._directives.length) {
michael@0 803 return false;
michael@0 804 }
michael@0 805 for (var i in this._directives) {
michael@0 806 if (!that._directives[i] || !this._directives[i].equals(that._directives[i])) {
michael@0 807 return false;
michael@0 808 }
michael@0 809 }
michael@0 810 return (this.allowsInlineScripts === that.allowsInlineScripts)
michael@0 811 && (this.allowsEvalInScripts === that.allowsEvalInScripts)
michael@0 812 && (this.allowsInlineStyles === that.allowsInlineStyles);
michael@0 813 },
michael@0 814
michael@0 815 /**
michael@0 816 * Generates canonical string representation of the policy.
michael@0 817 */
michael@0 818 toString:
michael@0 819 function csp_toString() {
michael@0 820 var dirs = [];
michael@0 821
michael@0 822 if (!this._specCompliant && (this._allowEval || this._allowInlineScripts)) {
michael@0 823 dirs.push("options" + (this._allowEval ? " eval-script" : "")
michael@0 824 + (this._allowInlineScripts ? " inline-script" : ""));
michael@0 825 }
michael@0 826 for (var i in this._directives) {
michael@0 827 if (this._directives[i]) {
michael@0 828 dirs.push(i + " " + this._directives[i].toString());
michael@0 829 }
michael@0 830 }
michael@0 831 return dirs.join("; ");
michael@0 832 },
michael@0 833
michael@0 834 permitsNonce:
michael@0 835 function csp_permitsNonce(aNonce, aDirective) {
michael@0 836 if (!this._directives.hasOwnProperty(aDirective)) return false;
michael@0 837 return this._directives[aDirective]._sources.some(function (source) {
michael@0 838 return source instanceof CSPNonceSource && source.permits(aNonce);
michael@0 839 });
michael@0 840 },
michael@0 841
michael@0 842 permitsHash:
michael@0 843 function csp_permitsHash(aContent, aDirective) {
michael@0 844 if (!this._directives.hasOwnProperty(aDirective)) return false;
michael@0 845 return this._directives[aDirective]._sources.some(function (source) {
michael@0 846 return source instanceof CSPHashSource && source.permits(aContent);
michael@0 847 });
michael@0 848 },
michael@0 849
michael@0 850 /**
michael@0 851 * Determines if this policy accepts a URI.
michael@0 852 * @param aURI
michael@0 853 * URI of the requested resource
michael@0 854 * @param aDirective
michael@0 855 * one of the SRC_DIRECTIVES defined above
michael@0 856 * @returns
michael@0 857 * true if the policy permits the URI in given context.
michael@0 858 */
michael@0 859 permits:
michael@0 860 function csp_permits(aURI, aDirective) {
michael@0 861 if (!aURI) return false;
michael@0 862
michael@0 863 // GLOBALLY ALLOW "about:" SCHEME
michael@0 864 if (aURI instanceof String && aURI.substring(0,6) === "about:")
michael@0 865 return true;
michael@0 866 if (aURI instanceof Ci.nsIURI && aURI.scheme === "about")
michael@0 867 return true;
michael@0 868
michael@0 869 // make sure the right directive set is used
michael@0 870 let DIRS = this._specCompliant ? CSPRep.SRC_DIRECTIVES_NEW : CSPRep.SRC_DIRECTIVES_OLD;
michael@0 871
michael@0 872 let directiveInPolicy = false;
michael@0 873 for (var i in DIRS) {
michael@0 874 if (DIRS[i] === aDirective) {
michael@0 875 // for catching calls with invalid contexts (below)
michael@0 876 directiveInPolicy = true;
michael@0 877 if (this._directives.hasOwnProperty(aDirective)) {
michael@0 878 return this._directives[aDirective].permits(aURI);
michael@0 879 }
michael@0 880 //found matching dir, can stop looking
michael@0 881 break;
michael@0 882 }
michael@0 883 }
michael@0 884
michael@0 885 // frame-ancestors is a special case; it doesn't fall back to default-src.
michael@0 886 if (aDirective === DIRS.FRAME_ANCESTORS)
michael@0 887 return true;
michael@0 888
michael@0 889 // All directives that don't fall back to default-src should have an escape
michael@0 890 // hatch above (like frame-ancestors).
michael@0 891 if (!directiveInPolicy) {
michael@0 892 // if this code runs, there's probably something calling permits() that
michael@0 893 // shouldn't be calling permits().
michael@0 894 CSPdebug("permits called with invalid load type: " + aDirective);
michael@0 895 return false;
michael@0 896 }
michael@0 897
michael@0 898 // no directives specifically matched, fall back to default-src.
michael@0 899 // (default-src may not be present for CSP 1.0-compliant policies, and
michael@0 900 // indicates no relevant directives were present and the load should be
michael@0 901 // permitted).
michael@0 902 if (this._directives.hasOwnProperty(DIRS.DEFAULT_SRC)) {
michael@0 903 return this._directives[DIRS.DEFAULT_SRC].permits(aURI);
michael@0 904 }
michael@0 905
michael@0 906 // no relevant directives present -- this means for CSP 1.0 that the load
michael@0 907 // should be permitted (and for the old CSP, to block it).
michael@0 908 return this._specCompliant;
michael@0 909 },
michael@0 910
michael@0 911 /**
michael@0 912 * Returns true if "eval" is enabled through the "eval" keyword.
michael@0 913 */
michael@0 914 get allowsEvalInScripts () {
michael@0 915 return this._allowEval;
michael@0 916 },
michael@0 917
michael@0 918 /**
michael@0 919 * Returns true if inline scripts are enabled through the "inline"
michael@0 920 * keyword.
michael@0 921 */
michael@0 922 get allowsInlineScripts () {
michael@0 923 return this._allowInlineScripts;
michael@0 924 },
michael@0 925
michael@0 926 /**
michael@0 927 * Returns true if inline styles are enabled through the "inline-style"
michael@0 928 * keyword.
michael@0 929 */
michael@0 930 get allowsInlineStyles () {
michael@0 931 return this._allowInlineStyles;
michael@0 932 },
michael@0 933
michael@0 934 /**
michael@0 935 * Sends a message to the error console and web developer console.
michael@0 936 * @param aFlag
michael@0 937 * The nsIScriptError flag constant indicating severity
michael@0 938 * @param aMsg
michael@0 939 * The message to send
michael@0 940 * @param aSource (optional)
michael@0 941 * The URL of the file in which the error occurred
michael@0 942 * @param aScriptLine (optional)
michael@0 943 * The line in the source file which the error occurred
michael@0 944 * @param aLineNum (optional)
michael@0 945 * The number of the line where the error occurred
michael@0 946 */
michael@0 947 log:
michael@0 948 function cspd_log(aFlag, aMsg, aSource, aScriptLine, aLineNum) {
michael@0 949 var textMessage = "Content Security Policy: " + aMsg;
michael@0 950 var consoleMsg = Components.classes["@mozilla.org/scripterror;1"]
michael@0 951 .createInstance(Ci.nsIScriptError);
michael@0 952 if (this._innerWindowID) {
michael@0 953 consoleMsg.initWithWindowID(textMessage, aSource, aScriptLine, aLineNum,
michael@0 954 0, aFlag,
michael@0 955 "CSP",
michael@0 956 this._innerWindowID);
michael@0 957 } else {
michael@0 958 consoleMsg.init(textMessage, aSource, aScriptLine, aLineNum, 0,
michael@0 959 aFlag,
michael@0 960 "CSP");
michael@0 961 }
michael@0 962 Components.classes["@mozilla.org/consoleservice;1"]
michael@0 963 .getService(Ci.nsIConsoleService).logMessage(consoleMsg);
michael@0 964 },
michael@0 965
michael@0 966 };
michael@0 967
michael@0 968 //////////////////////////////////////////////////////////////////////
michael@0 969 /**
michael@0 970 * Class to represent a list of sources
michael@0 971 */
michael@0 972 this.CSPSourceList = function CSPSourceList() {
michael@0 973 this._sources = [];
michael@0 974 this._permitAllSources = false;
michael@0 975
michael@0 976 // When this is true, the source list contains 'unsafe-inline'.
michael@0 977 this._allowUnsafeInline = false;
michael@0 978
michael@0 979 // When this is true, the source list contains 'unsafe-eval'.
michael@0 980 this._allowUnsafeEval = false;
michael@0 981
michael@0 982 // When this is true, the source list contains at least one nonce-source
michael@0 983 this._hasNonceSource = false;
michael@0 984
michael@0 985 // When this is true, the source list contains at least one hash-source
michael@0 986 this._hasHashSource = false;
michael@0 987 }
michael@0 988
michael@0 989 /**
michael@0 990 * Factory to create a new CSPSourceList, parsed from a string.
michael@0 991 *
michael@0 992 * @param aStr
michael@0 993 * string rep of a CSP Source List
michael@0 994 * @param aCSPRep
michael@0 995 * the CSPRep to which this souce list belongs. If null, CSP errors and
michael@0 996 * warnings will not be sent to the web console.
michael@0 997 * @param self (optional)
michael@0 998 * URI or CSPSource representing the "self" source
michael@0 999 * @param enforceSelfChecks (optional)
michael@0 1000 * if present, and "true", will check to be sure "self" has the
michael@0 1001 * appropriate values to inherit when they are omitted from the source.
michael@0 1002 * @returns
michael@0 1003 * an instance of CSPSourceList
michael@0 1004 */
michael@0 1005 CSPSourceList.fromString = function(aStr, aCSPRep, self, enforceSelfChecks) {
michael@0 1006 // source-list = *WSP [ source-expression *( 1*WSP source-expression ) *WSP ]
michael@0 1007 // / *WSP "'none'" *WSP
michael@0 1008
michael@0 1009 /* If self parameter is passed, convert to CSPSource,
michael@0 1010 unless it is already a CSPSource. */
michael@0 1011 if (self && !(self instanceof CSPSource)) {
michael@0 1012 self = CSPSource.create(self, aCSPRep);
michael@0 1013 }
michael@0 1014
michael@0 1015 var slObj = new CSPSourceList();
michael@0 1016 slObj._CSPRep = aCSPRep;
michael@0 1017 aStr = aStr.trim();
michael@0 1018 // w3 specifies case insensitive equality
michael@0 1019 if (aStr.toLowerCase() === "'none'") {
michael@0 1020 slObj._permitAllSources = false;
michael@0 1021 return slObj;
michael@0 1022 }
michael@0 1023
michael@0 1024 var tokens = aStr.split(/\s+/);
michael@0 1025 for (var i in tokens) {
michael@0 1026 if (!R_SOURCEEXP.test(tokens[i])) {
michael@0 1027 cspWarn(aCSPRep,
michael@0 1028 CSPLocalizer.getFormatStr("failedToParseUnrecognizedSource",
michael@0 1029 [tokens[i]]));
michael@0 1030 continue;
michael@0 1031 }
michael@0 1032 var src = CSPSource.create(tokens[i], aCSPRep, self, enforceSelfChecks);
michael@0 1033 if (!src) {
michael@0 1034 cspWarn(aCSPRep,
michael@0 1035 CSPLocalizer.getFormatStr("failedToParseUnrecognizedSource",
michael@0 1036 [tokens[i]]));
michael@0 1037 continue;
michael@0 1038 }
michael@0 1039
michael@0 1040 // if a source allows unsafe-inline, set our flag to indicate this.
michael@0 1041 if (src._allowUnsafeInline)
michael@0 1042 slObj._allowUnsafeInline = true;
michael@0 1043
michael@0 1044 // if a source allows unsafe-eval, set our flag to indicate this.
michael@0 1045 if (src._allowUnsafeEval)
michael@0 1046 slObj._allowUnsafeEval = true;
michael@0 1047
michael@0 1048 if (src instanceof CSPNonceSource)
michael@0 1049 slObj._hasNonceSource = true;
michael@0 1050
michael@0 1051 if (src instanceof CSPHashSource)
michael@0 1052 slObj._hasHashSource = true;
michael@0 1053
michael@0 1054 // if a source is a *, then we can permit all sources
michael@0 1055 if (src.permitAll) {
michael@0 1056 slObj._permitAllSources = true;
michael@0 1057 } else {
michael@0 1058 slObj._sources.push(src);
michael@0 1059 }
michael@0 1060 }
michael@0 1061
michael@0 1062 return slObj;
michael@0 1063 };
michael@0 1064
michael@0 1065 CSPSourceList.prototype = {
michael@0 1066 /**
michael@0 1067 * Compares one CSPSourceList to another.
michael@0 1068 *
michael@0 1069 * @param that
michael@0 1070 * another CSPSourceList
michael@0 1071 * @returns
michael@0 1072 * true if they have the same data
michael@0 1073 */
michael@0 1074 equals:
michael@0 1075 function(that) {
michael@0 1076 // special case to default-src * and 'none' to look different
michael@0 1077 // (both have a ._sources.length of 0).
michael@0 1078 if (that._permitAllSources != this._permitAllSources) {
michael@0 1079 return false;
michael@0 1080 }
michael@0 1081 if (that._sources.length != this._sources.length) {
michael@0 1082 return false;
michael@0 1083 }
michael@0 1084 // sort both arrays and compare like a zipper
michael@0 1085 // XXX (sid): I think we can make this more efficient
michael@0 1086 var sortfn = function(a,b) {
michael@0 1087 return a.toString.toLowerCase() > b.toString.toLowerCase();
michael@0 1088 };
michael@0 1089 var a_sorted = this._sources.sort(sortfn);
michael@0 1090 var b_sorted = that._sources.sort(sortfn);
michael@0 1091 for (var i in a_sorted) {
michael@0 1092 if (!a_sorted[i].equals(b_sorted[i])) {
michael@0 1093 return false;
michael@0 1094 }
michael@0 1095 }
michael@0 1096 return true;
michael@0 1097 },
michael@0 1098
michael@0 1099 /**
michael@0 1100 * Generates canonical string representation of the Source List.
michael@0 1101 */
michael@0 1102 toString:
michael@0 1103 function() {
michael@0 1104 if (this.isNone()) {
michael@0 1105 return "'none'";
michael@0 1106 }
michael@0 1107 if (this._permitAllSources) {
michael@0 1108 return "*";
michael@0 1109 }
michael@0 1110 return this._sources.map(function(x) { return x.toString(); }).join(" ");
michael@0 1111 },
michael@0 1112
michael@0 1113 /**
michael@0 1114 * Returns whether or not this source list represents the "'none'" special
michael@0 1115 * case.
michael@0 1116 */
michael@0 1117 isNone:
michael@0 1118 function() {
michael@0 1119 return (!this._permitAllSources) && (this._sources.length < 1);
michael@0 1120 },
michael@0 1121
michael@0 1122 /**
michael@0 1123 * Returns whether or not this source list permits all sources (*).
michael@0 1124 */
michael@0 1125 isAll:
michael@0 1126 function() {
michael@0 1127 return this._permitAllSources;
michael@0 1128 },
michael@0 1129
michael@0 1130 /**
michael@0 1131 * Makes a new deep copy of this object.
michael@0 1132 * @returns
michael@0 1133 * a new CSPSourceList
michael@0 1134 */
michael@0 1135 clone:
michael@0 1136 function() {
michael@0 1137 var aSL = new CSPSourceList();
michael@0 1138 aSL._permitAllSources = this._permitAllSources;
michael@0 1139 aSL._CSPRep = this._CSPRep;
michael@0 1140 for (var i in this._sources) {
michael@0 1141 aSL._sources[i] = this._sources[i].clone();
michael@0 1142 }
michael@0 1143 return aSL;
michael@0 1144 },
michael@0 1145
michael@0 1146 /**
michael@0 1147 * Determines if this directive accepts a URI.
michael@0 1148 * @param aURI
michael@0 1149 * the URI in question
michael@0 1150 * @returns
michael@0 1151 * true if the URI matches a source in this source list.
michael@0 1152 */
michael@0 1153 permits:
michael@0 1154 function cspsd_permits(aURI) {
michael@0 1155 if (this.isNone()) return false;
michael@0 1156 if (this.isAll()) return true;
michael@0 1157
michael@0 1158 for (var i in this._sources) {
michael@0 1159 if (this._sources[i].permits(aURI)) {
michael@0 1160 return true;
michael@0 1161 }
michael@0 1162 }
michael@0 1163 return false;
michael@0 1164 }
michael@0 1165 }
michael@0 1166
michael@0 1167 //////////////////////////////////////////////////////////////////////
michael@0 1168 /**
michael@0 1169 * Class to model a source (scheme, host, port)
michael@0 1170 */
michael@0 1171 this.CSPSource = function CSPSource() {
michael@0 1172 this._scheme = undefined;
michael@0 1173 this._port = undefined;
michael@0 1174 this._host = undefined;
michael@0 1175
michael@0 1176 //when set to true, this allows all source
michael@0 1177 this._permitAll = false;
michael@0 1178
michael@0 1179 // when set to true, this source represents 'self'
michael@0 1180 this._isSelf = false;
michael@0 1181
michael@0 1182 // when set to true, this source allows inline scripts or styles
michael@0 1183 this._allowUnsafeInline = false;
michael@0 1184
michael@0 1185 // when set to true, this source allows eval to be used
michael@0 1186 this._allowUnsafeEval = false;
michael@0 1187 }
michael@0 1188
michael@0 1189 /**
michael@0 1190 * General factory method to create a new source from one of the following
michael@0 1191 * types:
michael@0 1192 * - nsURI
michael@0 1193 * - string
michael@0 1194 * - CSPSource (clone)
michael@0 1195 * @param aData
michael@0 1196 * string, nsURI, or CSPSource
michael@0 1197 * @param aCSPRep
michael@0 1198 * The CSPRep this source belongs to. If null, CSP errors and warnings
michael@0 1199 * will not be sent to the web console.
michael@0 1200 * @param self (optional)
michael@0 1201 * if present, string, URI, or CSPSource representing the "self" resource
michael@0 1202 * @param enforceSelfChecks (optional)
michael@0 1203 * if present, and "true", will check to be sure "self" has the
michael@0 1204 * appropriate values to inherit when they are omitted from the source.
michael@0 1205 * @returns
michael@0 1206 * an instance of CSPSource
michael@0 1207 */
michael@0 1208 CSPSource.create = function(aData, aCSPRep, self, enforceSelfChecks) {
michael@0 1209 if (typeof aData === 'string')
michael@0 1210 return CSPSource.fromString(aData, aCSPRep, self, enforceSelfChecks);
michael@0 1211
michael@0 1212 if (aData instanceof Ci.nsIURI) {
michael@0 1213 // clean userpass out of the URI (not used for CSP origin checking, but
michael@0 1214 // shows up in prePath).
michael@0 1215 let cleanedUri = aData.cloneIgnoringRef();
michael@0 1216 try {
michael@0 1217 // GetUserPass throws for some protocols without userPass
michael@0 1218 cleanedUri.userPass = '';
michael@0 1219 } catch (ex) {}
michael@0 1220
michael@0 1221 return CSPSource.fromURI(cleanedUri, aCSPRep, self, enforceSelfChecks);
michael@0 1222 }
michael@0 1223
michael@0 1224 if (aData instanceof CSPSource) {
michael@0 1225 var ns = aData.clone();
michael@0 1226 ns._self = CSPSource.create(self);
michael@0 1227 return ns;
michael@0 1228 }
michael@0 1229
michael@0 1230 return null;
michael@0 1231 }
michael@0 1232
michael@0 1233 /**
michael@0 1234 * Factory to create a new CSPSource, from a nsIURI.
michael@0 1235 *
michael@0 1236 * Don't use this if you want to wildcard ports!
michael@0 1237 *
michael@0 1238 * @param aURI
michael@0 1239 * nsIURI rep of a URI
michael@0 1240 * @param aCSPRep
michael@0 1241 * The policy this source belongs to. If null, CSP errors and warnings
michael@0 1242 * will not be sent to the web console.
michael@0 1243 * @param self (optional)
michael@0 1244 * string or CSPSource representing the "self" source
michael@0 1245 * @param enforceSelfChecks (optional)
michael@0 1246 * if present, and "true", will check to be sure "self" has the
michael@0 1247 * appropriate values to inherit when they are omitted from aURI.
michael@0 1248 * @returns
michael@0 1249 * an instance of CSPSource
michael@0 1250 */
michael@0 1251 CSPSource.fromURI = function(aURI, aCSPRep, self, enforceSelfChecks) {
michael@0 1252 if (!(aURI instanceof Ci.nsIURI)) {
michael@0 1253 cspError(aCSPRep, CSPLocalizer.getStr("cspSourceNotURI"));
michael@0 1254 return null;
michael@0 1255 }
michael@0 1256
michael@0 1257 if (!self && enforceSelfChecks) {
michael@0 1258 cspError(aCSPRep, CSPLocalizer.getStr("selfDataNotProvided"));
michael@0 1259 return null;
michael@0 1260 }
michael@0 1261
michael@0 1262 if (self && !(self instanceof CSPSource)) {
michael@0 1263 self = CSPSource.create(self, aCSPRep, undefined, false);
michael@0 1264 }
michael@0 1265
michael@0 1266 var sObj = new CSPSource();
michael@0 1267 sObj._self = self;
michael@0 1268 sObj._CSPRep = aCSPRep;
michael@0 1269
michael@0 1270 // PARSE
michael@0 1271 // If 'self' is undefined, then use default port for scheme if there is one.
michael@0 1272
michael@0 1273 // grab scheme (if there is one)
michael@0 1274 try {
michael@0 1275 sObj._scheme = aURI.scheme;
michael@0 1276 } catch(e) {
michael@0 1277 sObj._scheme = undefined;
michael@0 1278 cspError(aCSPRep, CSPLocalizer.getFormatStr("uriWithoutScheme",
michael@0 1279 [aURI.asciiSpec]));
michael@0 1280 return null;
michael@0 1281 }
michael@0 1282
michael@0 1283 // grab host (if there is one)
michael@0 1284 try {
michael@0 1285 // if there's no host, an exception will get thrown
michael@0 1286 // (NS_ERROR_FAILURE)
michael@0 1287 sObj._host = CSPHost.fromString(aURI.host);
michael@0 1288 } catch(e) {
michael@0 1289 sObj._host = undefined;
michael@0 1290 }
michael@0 1291
michael@0 1292 // grab port (if there is one)
michael@0 1293 // creating a source from an nsURI is limited in that one cannot specify "*"
michael@0 1294 // for port. In fact, there's no way to represent "*" differently than
michael@0 1295 // a blank port in an nsURI, since "*" turns into -1, and so does an
michael@0 1296 // absence of port declaration.
michael@0 1297
michael@0 1298 // port is never inherited from self -- this gets too confusing.
michael@0 1299 // Instead, whatever scheme is used (an explicit one or the inherited
michael@0 1300 // one) dictates the port if no port is explicitly stated.
michael@0 1301 // Set it to undefined here and the default port will be resolved in the
michael@0 1302 // getter for .port.
michael@0 1303 sObj._port = undefined;
michael@0 1304 try {
michael@0 1305 // if there's no port, an exception will get thrown
michael@0 1306 // (NS_ERROR_FAILURE)
michael@0 1307 if (aURI.port > 0) {
michael@0 1308 sObj._port = aURI.port;
michael@0 1309 }
michael@0 1310 } catch(e) {
michael@0 1311 sObj._port = undefined;
michael@0 1312 }
michael@0 1313
michael@0 1314 return sObj;
michael@0 1315 };
michael@0 1316
michael@0 1317 /**
michael@0 1318 * Factory to create a new CSPSource, parsed from a string.
michael@0 1319 *
michael@0 1320 * @param aStr
michael@0 1321 * string rep of a CSP Source
michael@0 1322 * @param aCSPRep
michael@0 1323 * the CSPRep this CSPSource belongs to
michael@0 1324 * @param self (optional)
michael@0 1325 * string, URI, or CSPSource representing the "self" source
michael@0 1326 * @param enforceSelfChecks (optional)
michael@0 1327 * if present, and "true", will check to be sure "self" has the
michael@0 1328 * appropriate values to inherit when they are omitted from aURI.
michael@0 1329 * @returns
michael@0 1330 * an instance of CSPSource
michael@0 1331 */
michael@0 1332 CSPSource.fromString = function(aStr, aCSPRep, self, enforceSelfChecks) {
michael@0 1333 if (!aStr)
michael@0 1334 return null;
michael@0 1335
michael@0 1336 if (!(typeof aStr === 'string')) {
michael@0 1337 cspError(aCSPRep, CSPLocalizer.getStr("argumentIsNotString"));
michael@0 1338 return null;
michael@0 1339 }
michael@0 1340
michael@0 1341 var sObj = new CSPSource();
michael@0 1342 sObj._self = self;
michael@0 1343 sObj._CSPRep = aCSPRep;
michael@0 1344
michael@0 1345
michael@0 1346 // if equal, return does match
michael@0 1347 if (aStr === "*") {
michael@0 1348 sObj._permitAll = true;
michael@0 1349 return sObj;
michael@0 1350 }
michael@0 1351
michael@0 1352 if (!self && enforceSelfChecks) {
michael@0 1353 cspError(aCSPRep, CSPLocalizer.getStr("selfDataNotProvided"));
michael@0 1354 return null;
michael@0 1355 }
michael@0 1356
michael@0 1357 if (self && !(self instanceof CSPSource)) {
michael@0 1358 self = CSPSource.create(self, aCSPRep, undefined, false);
michael@0 1359 }
michael@0 1360
michael@0 1361 // check for 'unsafe-inline' (case insensitive)
michael@0 1362 if (aStr.toLowerCase() === "'unsafe-inline'"){
michael@0 1363 sObj._allowUnsafeInline = true;
michael@0 1364 return sObj;
michael@0 1365 }
michael@0 1366
michael@0 1367 // check for 'unsafe-eval' (case insensitive)
michael@0 1368 if (aStr.toLowerCase() === "'unsafe-eval'"){
michael@0 1369 sObj._allowUnsafeEval = true;
michael@0 1370 return sObj;
michael@0 1371 }
michael@0 1372
michael@0 1373 // Check for scheme-source match - this only matches if the source
michael@0 1374 // string is just a scheme with no host.
michael@0 1375 if (R_SCHEMESRC.test(aStr)) {
michael@0 1376 var schemeSrcMatch = R_GETSCHEME.exec(aStr);
michael@0 1377 sObj._scheme = schemeSrcMatch[0];
michael@0 1378 if (!sObj._host) sObj._host = CSPHost.fromString("*");
michael@0 1379 if (!sObj._port) sObj._port = "*";
michael@0 1380 return sObj;
michael@0 1381 }
michael@0 1382
michael@0 1383 // check for host-source or ext-host-source match
michael@0 1384 if (R_HOSTSRC.test(aStr) || R_EXTHOSTSRC.test(aStr)) {
michael@0 1385 var schemeMatch = R_GETSCHEME.exec(aStr);
michael@0 1386 // check that the scheme isn't accidentally matching the host. There should
michael@0 1387 // be '://' if there is a valid scheme in an (EXT)HOSTSRC
michael@0 1388 if (!schemeMatch || aStr.indexOf("://") == -1) {
michael@0 1389 sObj._scheme = self.scheme;
michael@0 1390 schemeMatch = null;
michael@0 1391 } else {
michael@0 1392 sObj._scheme = schemeMatch[0];
michael@0 1393 }
michael@0 1394
michael@0 1395 // Bug 916054: in CSP 1.0, source-expressions that are paths should have
michael@0 1396 // the path after the origin ignored and only the origin enforced.
michael@0 1397 if (R_EXTHOSTSRC.test(aStr)) {
michael@0 1398 var extHostMatch = R_EXTHOSTSRC.exec(aStr);
michael@0 1399 aStr = extHostMatch[1];
michael@0 1400 }
michael@0 1401
michael@0 1402 var hostMatch = R_HOSTSRC.exec(aStr);
michael@0 1403 if (!hostMatch) {
michael@0 1404 cspError(aCSPRep, CSPLocalizer.getFormatStr("couldntParseInvalidSource",
michael@0 1405 [aStr]));
michael@0 1406 return null;
michael@0 1407 }
michael@0 1408 // Host regex gets scheme, so remove scheme from aStr. Add 3 for '://'
michael@0 1409 if (schemeMatch) {
michael@0 1410 hostMatch = R_HOSTSRC.exec(aStr.substring(schemeMatch[0].length + 3));
michael@0 1411 }
michael@0 1412
michael@0 1413 var portMatch = R_PORT.exec(hostMatch);
michael@0 1414 // Host regex also gets port, so remove the port here.
michael@0 1415 if (portMatch) {
michael@0 1416 hostMatch = R_HOSTSRC.exec(hostMatch[0].substring(0, hostMatch[0].length - portMatch[0].length));
michael@0 1417 }
michael@0 1418
michael@0 1419 sObj._host = CSPHost.fromString(hostMatch[0]);
michael@0 1420 if (!portMatch) {
michael@0 1421 // gets the default port for the given scheme
michael@0 1422 var defPort = Services.io.getProtocolHandler(sObj._scheme).defaultPort;
michael@0 1423 if (!defPort) {
michael@0 1424 cspError(aCSPRep,
michael@0 1425 CSPLocalizer.getFormatStr("couldntParseInvalidSource",
michael@0 1426 [aStr]));
michael@0 1427 return null;
michael@0 1428 }
michael@0 1429 sObj._port = defPort;
michael@0 1430 }
michael@0 1431 else {
michael@0 1432 // strip the ':' from the port
michael@0 1433 sObj._port = portMatch[0].substr(1);
michael@0 1434 }
michael@0 1435 // A CSP keyword without quotes is a valid hostname, but this can also be a mistake.
michael@0 1436 // Raise a CSP warning in the web console to developer to check his/her intent.
michael@0 1437 if (R_QUOTELESS_KEYWORDS.test(aStr)) {
michael@0 1438 cspWarn(aCSPRep, CSPLocalizer.getFormatStr("hostNameMightBeKeyword",
michael@0 1439 [aStr, aStr.toLowerCase()]));
michael@0 1440 }
michael@0 1441 return sObj;
michael@0 1442 }
michael@0 1443
michael@0 1444 // check for a nonce-source match
michael@0 1445 if (R_NONCESRC.test(aStr)) {
michael@0 1446 return CSPNonceSource.fromString(aStr, aCSPRep);
michael@0 1447 }
michael@0 1448
michael@0 1449 // check for a hash-source match
michael@0 1450 if (R_HASHSRC.test(aStr)) {
michael@0 1451 return CSPHashSource.fromString(aStr, aCSPRep);
michael@0 1452 }
michael@0 1453
michael@0 1454 // check for 'self' (case insensitive)
michael@0 1455 if (aStr.toLowerCase() === "'self'") {
michael@0 1456 if (!self) {
michael@0 1457 cspError(aCSPRep, CSPLocalizer.getStr("selfKeywordNoSelfData"));
michael@0 1458 return null;
michael@0 1459 }
michael@0 1460 sObj._self = self.clone();
michael@0 1461 sObj._isSelf = true;
michael@0 1462 return sObj;
michael@0 1463 }
michael@0 1464
michael@0 1465 cspError(aCSPRep, CSPLocalizer.getFormatStr("couldntParseInvalidSource",
michael@0 1466 [aStr]));
michael@0 1467 return null;
michael@0 1468 };
michael@0 1469
michael@0 1470 CSPSource.validSchemeName = function(aStr) {
michael@0 1471 // <scheme-name> ::= <alpha><scheme-suffix>
michael@0 1472 // <scheme-suffix> ::= <scheme-chr>
michael@0 1473 // | <scheme-suffix><scheme-chr>
michael@0 1474 // <scheme-chr> ::= <letter> | <digit> | "+" | "." | "-"
michael@0 1475
michael@0 1476 return aStr.match(/^[a-zA-Z][a-zA-Z0-9+.-]*$/);
michael@0 1477 };
michael@0 1478
michael@0 1479 CSPSource.prototype = {
michael@0 1480
michael@0 1481 get scheme () {
michael@0 1482 if (this._isSelf && this._self)
michael@0 1483 return this._self.scheme;
michael@0 1484 if (!this._scheme && this._self)
michael@0 1485 return this._self.scheme;
michael@0 1486 return this._scheme;
michael@0 1487 },
michael@0 1488
michael@0 1489 get host () {
michael@0 1490 if (this._isSelf && this._self)
michael@0 1491 return this._self.host;
michael@0 1492 if (!this._host && this._self)
michael@0 1493 return this._self.host;
michael@0 1494 return this._host;
michael@0 1495 },
michael@0 1496
michael@0 1497 get permitAll () {
michael@0 1498 if (this._isSelf && this._self)
michael@0 1499 return this._self.permitAll;
michael@0 1500 return this._permitAll;
michael@0 1501 },
michael@0 1502
michael@0 1503 /**
michael@0 1504 * If this doesn't have a nonstandard port (hard-defined), use the default
michael@0 1505 * port for this source's scheme. Should never inherit port from 'self'.
michael@0 1506 */
michael@0 1507 get port () {
michael@0 1508 if (this._isSelf && this._self)
michael@0 1509 return this._self.port;
michael@0 1510 if (this._port) return this._port;
michael@0 1511 // if no port, get the default port for the scheme
michael@0 1512 // (which may be inherited from 'self')
michael@0 1513 if (this.scheme) {
michael@0 1514 try {
michael@0 1515 var port = gIoService.getProtocolHandler(this.scheme).defaultPort;
michael@0 1516 if (port > 0) return port;
michael@0 1517 } catch(e) {
michael@0 1518 // if any errors happen, fail gracefully.
michael@0 1519 }
michael@0 1520 }
michael@0 1521
michael@0 1522 return undefined;
michael@0 1523 },
michael@0 1524
michael@0 1525 /**
michael@0 1526 * Generates canonical string representation of the Source.
michael@0 1527 */
michael@0 1528 toString:
michael@0 1529 function() {
michael@0 1530 if (this._isSelf)
michael@0 1531 return this._self.toString();
michael@0 1532
michael@0 1533 if (this._allowUnsafeInline)
michael@0 1534 return "'unsafe-inline'";
michael@0 1535
michael@0 1536 if (this._allowUnsafeEval)
michael@0 1537 return "'unsafe-eval'";
michael@0 1538
michael@0 1539 var s = "";
michael@0 1540 if (this.scheme)
michael@0 1541 s = s + this.scheme + "://";
michael@0 1542 if (this._host)
michael@0 1543 s = s + this._host;
michael@0 1544 if (this.port)
michael@0 1545 s = s + ":" + this.port;
michael@0 1546 return s;
michael@0 1547 },
michael@0 1548
michael@0 1549 /**
michael@0 1550 * Makes a new deep copy of this object.
michael@0 1551 * @returns
michael@0 1552 * a new CSPSource
michael@0 1553 */
michael@0 1554 clone:
michael@0 1555 function() {
michael@0 1556 var aClone = new CSPSource();
michael@0 1557 aClone._self = this._self ? this._self.clone() : undefined;
michael@0 1558 aClone._scheme = this._scheme;
michael@0 1559 aClone._port = this._port;
michael@0 1560 aClone._host = this._host ? this._host.clone() : undefined;
michael@0 1561 aClone._isSelf = this._isSelf;
michael@0 1562 aClone._CSPRep = this._CSPRep;
michael@0 1563 return aClone;
michael@0 1564 },
michael@0 1565
michael@0 1566 /**
michael@0 1567 * Determines if this Source accepts a URI.
michael@0 1568 * @param aSource
michael@0 1569 * the URI, or CSPSource in question
michael@0 1570 * @returns
michael@0 1571 * true if the URI matches a source in this source list.
michael@0 1572 */
michael@0 1573 permits:
michael@0 1574 function(aSource) {
michael@0 1575 if (!aSource) return false;
michael@0 1576
michael@0 1577 if (!(aSource instanceof CSPSource))
michael@0 1578 aSource = CSPSource.create(aSource, this._CSPRep);
michael@0 1579
michael@0 1580 // verify scheme
michael@0 1581 if (this.scheme.toLowerCase() != aSource.scheme.toLowerCase())
michael@0 1582 return false;
michael@0 1583
michael@0 1584 // port is defined in 'this' (undefined means it may not be relevant
michael@0 1585 // to the scheme) AND this port (implicit or explicit) matches
michael@0 1586 // aSource's port
michael@0 1587 if (this.port && this.port !== "*" && this.port != aSource.port)
michael@0 1588 return false;
michael@0 1589
michael@0 1590 // host is defined in 'this' (undefined means it may not be relevant
michael@0 1591 // to the scheme) AND this host (implicit or explicit) permits
michael@0 1592 // aSource's host.
michael@0 1593 if (this.host && !this.host.permits(aSource.host))
michael@0 1594 return false;
michael@0 1595
michael@0 1596 // all scheme, host and port matched!
michael@0 1597 return true;
michael@0 1598 },
michael@0 1599
michael@0 1600 /**
michael@0 1601 * Compares one CSPSource to another.
michael@0 1602 *
michael@0 1603 * @param that
michael@0 1604 * another CSPSource
michael@0 1605 * @param resolveSelf (optional)
michael@0 1606 * if present, and 'true', implied values are obtained from 'self'
michael@0 1607 * instead of assumed to be "anything"
michael@0 1608 * @returns
michael@0 1609 * true if they have the same data
michael@0 1610 */
michael@0 1611 equals:
michael@0 1612 function(that, resolveSelf) {
michael@0 1613 // 1. schemes match
michael@0 1614 // 2. ports match
michael@0 1615 // 3. either both hosts are undefined, or one equals the other.
michael@0 1616 if (resolveSelf)
michael@0 1617 return this.scheme.toLowerCase() === that.scheme.toLowerCase()
michael@0 1618 && this.port === that.port
michael@0 1619 && (!(this.host || that.host) ||
michael@0 1620 (this.host && this.host.equals(that.host)));
michael@0 1621
michael@0 1622 // otherwise, compare raw (non-self-resolved values)
michael@0 1623 return this._scheme.toLowerCase() === that._scheme.toLowerCase()
michael@0 1624 && this._port === that._port
michael@0 1625 && (!(this._host || that._host) ||
michael@0 1626 (this._host && this._host.equals(that._host)));
michael@0 1627 },
michael@0 1628
michael@0 1629 };
michael@0 1630
michael@0 1631 //////////////////////////////////////////////////////////////////////
michael@0 1632 /**
michael@0 1633 * Class to model a host *.x.y.
michael@0 1634 */
michael@0 1635 this.CSPHost = function CSPHost() {
michael@0 1636 this._segments = [];
michael@0 1637 }
michael@0 1638
michael@0 1639 /**
michael@0 1640 * Factory to create a new CSPHost, parsed from a string.
michael@0 1641 *
michael@0 1642 * @param aStr
michael@0 1643 * string rep of a CSP Host
michael@0 1644 * @returns
michael@0 1645 * an instance of CSPHost
michael@0 1646 */
michael@0 1647 CSPHost.fromString = function(aStr) {
michael@0 1648 if (!aStr) return null;
michael@0 1649
michael@0 1650 // host string must be LDH with dots and stars.
michael@0 1651 var invalidChar = aStr.match(R_INV_HCHAR);
michael@0 1652 if (invalidChar) {
michael@0 1653 CSPdebug("Invalid character '" + invalidChar + "' in host " + aStr);
michael@0 1654 return null;
michael@0 1655 }
michael@0 1656
michael@0 1657 var hObj = new CSPHost();
michael@0 1658 hObj._segments = aStr.split(/\./);
michael@0 1659 if (hObj._segments.length < 1)
michael@0 1660 return null;
michael@0 1661
michael@0 1662 // validate data in segments
michael@0 1663 for (var i in hObj._segments) {
michael@0 1664 var seg = hObj._segments[i];
michael@0 1665 if (seg == "*") {
michael@0 1666 if (i > 0) {
michael@0 1667 // Wildcard must be FIRST
michael@0 1668 CSPdebug("Wildcard char located at invalid position in '" + aStr + "'");
michael@0 1669 return null;
michael@0 1670 }
michael@0 1671 }
michael@0 1672 else if (seg.match(R_COMP_HCHAR)) {
michael@0 1673 // Non-wildcard segment must be LDH string
michael@0 1674 CSPdebug("Invalid segment '" + seg + "' in host value");
michael@0 1675 return null;
michael@0 1676 }
michael@0 1677 }
michael@0 1678 return hObj;
michael@0 1679 };
michael@0 1680
michael@0 1681 CSPHost.prototype = {
michael@0 1682 /**
michael@0 1683 * Generates canonical string representation of the Host.
michael@0 1684 */
michael@0 1685 toString:
michael@0 1686 function() {
michael@0 1687 return this._segments.join(".");
michael@0 1688 },
michael@0 1689
michael@0 1690 /**
michael@0 1691 * Makes a new deep copy of this object.
michael@0 1692 * @returns
michael@0 1693 * a new CSPHost
michael@0 1694 */
michael@0 1695 clone:
michael@0 1696 function() {
michael@0 1697 var aHost = new CSPHost();
michael@0 1698 for (var i in this._segments) {
michael@0 1699 aHost._segments[i] = this._segments[i];
michael@0 1700 }
michael@0 1701 return aHost;
michael@0 1702 },
michael@0 1703
michael@0 1704 /**
michael@0 1705 * Returns true if this host accepts the provided host (or the other way
michael@0 1706 * around).
michael@0 1707 * @param aHost
michael@0 1708 * the FQDN in question (CSPHost or String)
michael@0 1709 * @returns
michael@0 1710 */
michael@0 1711 permits:
michael@0 1712 function(aHost) {
michael@0 1713 if (!aHost) {
michael@0 1714 aHost = CSPHost.fromString("*");
michael@0 1715 }
michael@0 1716
michael@0 1717 if (!(aHost instanceof CSPHost)) {
michael@0 1718 // -- compare CSPHost to String
michael@0 1719 aHost = CSPHost.fromString(aHost);
michael@0 1720 }
michael@0 1721 var thislen = this._segments.length;
michael@0 1722 var thatlen = aHost._segments.length;
michael@0 1723
michael@0 1724 // don't accept a less specific host:
michael@0 1725 // \--> *.b.a doesn't accept b.a.
michael@0 1726 if (thatlen < thislen) { return false; }
michael@0 1727
michael@0 1728 // check for more specific host (and wildcard):
michael@0 1729 // \--> *.b.a accepts d.c.b.a.
michael@0 1730 // \--> c.b.a doesn't accept d.c.b.a.
michael@0 1731 if ((thatlen > thislen) && this._segments[0] != "*") {
michael@0 1732 return false;
michael@0 1733 }
michael@0 1734
michael@0 1735 // Given the wildcard condition (from above),
michael@0 1736 // only necessary to compare elements that are present
michael@0 1737 // in this host. Extra tokens in aHost are ok.
michael@0 1738 // * Compare from right to left.
michael@0 1739 for (var i=1; i <= thislen; i++) {
michael@0 1740 if (this._segments[thislen-i] != "*" &&
michael@0 1741 (this._segments[thislen-i].toLowerCase() !=
michael@0 1742 aHost._segments[thatlen-i].toLowerCase())) {
michael@0 1743 return false;
michael@0 1744 }
michael@0 1745 }
michael@0 1746
michael@0 1747 // at this point, all conditions are met, so the host is allowed
michael@0 1748 return true;
michael@0 1749 },
michael@0 1750
michael@0 1751 /**
michael@0 1752 * Compares one CSPHost to another.
michael@0 1753 *
michael@0 1754 * @param that
michael@0 1755 * another CSPHost
michael@0 1756 * @returns
michael@0 1757 * true if they have the same data
michael@0 1758 */
michael@0 1759 equals:
michael@0 1760 function(that) {
michael@0 1761 if (this._segments.length != that._segments.length)
michael@0 1762 return false;
michael@0 1763
michael@0 1764 for (var i=0; i<this._segments.length; i++) {
michael@0 1765 if (this._segments[i].toLowerCase() !=
michael@0 1766 that._segments[i].toLowerCase()) {
michael@0 1767 return false;
michael@0 1768 }
michael@0 1769 }
michael@0 1770 return true;
michael@0 1771 }
michael@0 1772 };
michael@0 1773
michael@0 1774 this.CSPNonceSource = function CSPNonceSource() {
michael@0 1775 this._nonce = undefined;
michael@0 1776 }
michael@0 1777
michael@0 1778 CSPNonceSource.fromString = function(aStr, aCSPRep) {
michael@0 1779 let nonce = R_NONCESRC.exec(aStr)[1];
michael@0 1780 if (!nonce) {
michael@0 1781 cspError(aCSPRep, "Error in parsing nonce-source from string: nonce was empty");
michael@0 1782 return null;
michael@0 1783 }
michael@0 1784
michael@0 1785 let nonceSourceObj = new CSPNonceSource();
michael@0 1786 nonceSourceObj._nonce = nonce;
michael@0 1787 return nonceSourceObj;
michael@0 1788 };
michael@0 1789
michael@0 1790 CSPNonceSource.prototype = {
michael@0 1791
michael@0 1792 permits: function(aContext) {
michael@0 1793 if (aContext instanceof Ci.nsIDOMHTMLElement) {
michael@0 1794 return this._nonce === aContext.getAttribute('nonce');
michael@0 1795 } else if (typeof aContext === 'string') {
michael@0 1796 return this._nonce === aContext;
michael@0 1797 }
michael@0 1798 CSPdebug("permits called on nonce-source, but aContext was not nsIDOMHTMLElement or string (was " + typeof(aContext) + ")");
michael@0 1799 return false;
michael@0 1800 },
michael@0 1801
michael@0 1802 toString: function() {
michael@0 1803 return "'nonce-" + this._nonce + "'";
michael@0 1804 },
michael@0 1805
michael@0 1806 clone: function() {
michael@0 1807 let clone = new CSPNonceSource();
michael@0 1808 clone._nonce = this._nonce;
michael@0 1809 return clone;
michael@0 1810 },
michael@0 1811
michael@0 1812 equals: function(that) {
michael@0 1813 return this._nonce === that._nonce;
michael@0 1814 }
michael@0 1815
michael@0 1816 };
michael@0 1817
michael@0 1818 this.CSPHashSource = function CSPHashSource() {
michael@0 1819 this._algo = undefined;
michael@0 1820 this._hash = undefined;
michael@0 1821 }
michael@0 1822
michael@0 1823 CSPHashSource.fromString = function(aStr, aCSPRep) {
michael@0 1824 let hashSrcMatch = R_HASHSRC.exec(aStr);
michael@0 1825 let algo = hashSrcMatch[1];
michael@0 1826 let hash = hashSrcMatch[2];
michael@0 1827 if (!algo) {
michael@0 1828 cspError(aCSPRep, "Error parsing hash-source from string: algo was empty");
michael@0 1829 return null;
michael@0 1830 }
michael@0 1831 if (!hash) {
michael@0 1832 cspError(aCSPRep, "Error parsing hash-source from string: hash was empty");
michael@0 1833 return null;
michael@0 1834 }
michael@0 1835
michael@0 1836 let hashSourceObj = new CSPHashSource();
michael@0 1837 hashSourceObj._algo = algo;
michael@0 1838 hashSourceObj._hash = hash;
michael@0 1839 return hashSourceObj;
michael@0 1840 };
michael@0 1841
michael@0 1842 CSPHashSource.prototype = {
michael@0 1843
michael@0 1844 permits: function(aContext) {
michael@0 1845 let ScriptableUnicodeConverter =
michael@0 1846 Components.Constructor("@mozilla.org/intl/scriptableunicodeconverter",
michael@0 1847 "nsIScriptableUnicodeConverter");
michael@0 1848 let converter = new ScriptableUnicodeConverter();
michael@0 1849 converter.charset = 'utf8';
michael@0 1850 let utf8InnerHTML = converter.convertToByteArray(aContext);
michael@0 1851
michael@0 1852 let CryptoHash =
michael@0 1853 Components.Constructor("@mozilla.org/security/hash;1",
michael@0 1854 "nsICryptoHash",
michael@0 1855 "initWithString");
michael@0 1856 let hash = new CryptoHash(this._algo);
michael@0 1857 hash.update(utf8InnerHTML, utf8InnerHTML.length);
michael@0 1858 // passing true causes a base64-encoded hash to be returned
michael@0 1859 let contentHash = hash.finish(true);
michael@0 1860
michael@0 1861 // The NSS Base64 encoder automatically adds linebreaks "\r\n" every 64
michael@0 1862 // characters. We need to remove these so we can properly validate longer
michael@0 1863 // (SHA-512) base64-encoded hashes
michael@0 1864 contentHash = contentHash.replace('\r\n', '');
michael@0 1865
michael@0 1866 return contentHash === this._hash;
michael@0 1867 },
michael@0 1868
michael@0 1869 toString: function() {
michael@0 1870 return "'" + this._algo + '-' + this._hash + "'";
michael@0 1871 },
michael@0 1872
michael@0 1873 clone: function() {
michael@0 1874 let clone = new CSPHashSource();
michael@0 1875 clone._algo = this._algo;
michael@0 1876 clone._hash = this._hash;
michael@0 1877 return clone;
michael@0 1878 },
michael@0 1879
michael@0 1880 equals: function(that) {
michael@0 1881 return this._algo === that._algo && this._hash === that._hash;
michael@0 1882 }
michael@0 1883
michael@0 1884 };
michael@0 1885
michael@0 1886 //////////////////////////////////////////////////////////////////////
michael@0 1887 /**
michael@0 1888 * Class that listens to violation report transmission and logs errors.
michael@0 1889 */
michael@0 1890 this.CSPViolationReportListener = function CSPViolationReportListener(reportURI) {
michael@0 1891 this._reportURI = reportURI;
michael@0 1892 }
michael@0 1893
michael@0 1894 CSPViolationReportListener.prototype = {
michael@0 1895 _reportURI: null,
michael@0 1896
michael@0 1897 QueryInterface: function(iid) {
michael@0 1898 if (iid.equals(Ci.nsIStreamListener) ||
michael@0 1899 iid.equals(Ci.nsIRequestObserver) ||
michael@0 1900 iid.equals(Ci.nsISupports))
michael@0 1901 return this;
michael@0 1902 throw Components.results.NS_ERROR_NO_INTERFACE;
michael@0 1903 },
michael@0 1904
michael@0 1905 onStopRequest:
michael@0 1906 function(request, context, status) {
michael@0 1907 if (!Components.isSuccessCode(status)) {
michael@0 1908 CSPdebug("error " + status.toString(16) +
michael@0 1909 " while sending violation report to " +
michael@0 1910 this._reportURI);
michael@0 1911 }
michael@0 1912 },
michael@0 1913
michael@0 1914 onStartRequest:
michael@0 1915 function(request, context) { },
michael@0 1916
michael@0 1917 onDataAvailable:
michael@0 1918 function(request, context, inputStream, offset, count) {
michael@0 1919 // We MUST read equal to count from the inputStream to avoid an assertion.
michael@0 1920 var input = Components.classes['@mozilla.org/scriptableinputstream;1']
michael@0 1921 .createInstance(Ci.nsIScriptableInputStream);
michael@0 1922
michael@0 1923 input.init(inputStream);
michael@0 1924 input.read(count);
michael@0 1925 },
michael@0 1926
michael@0 1927 };
michael@0 1928
michael@0 1929 //////////////////////////////////////////////////////////////////////
michael@0 1930
michael@0 1931 function innerWindowFromRequest(docRequest) {
michael@0 1932 let win = null;
michael@0 1933 let loadContext = null;
michael@0 1934
michael@0 1935 try {
michael@0 1936 loadContext = docRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
michael@0 1937 } catch (ex) {
michael@0 1938 try {
michael@0 1939 loadContext = docRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
michael@0 1940 } catch (ex) {
michael@0 1941 return null;
michael@0 1942 }
michael@0 1943 }
michael@0 1944
michael@0 1945 if (loadContext) {
michael@0 1946 win = loadContext.associatedWindow;
michael@0 1947 }
michael@0 1948 if (win) {
michael@0 1949 try {
michael@0 1950 let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 1951 return winUtils.currentInnerWindowID;
michael@0 1952 } catch (ex) {
michael@0 1953 return null;
michael@0 1954 }
michael@0 1955 }
michael@0 1956 return null;
michael@0 1957 }
michael@0 1958
michael@0 1959 function cspError(aCSPRep, aMessage) {
michael@0 1960 if (aCSPRep) {
michael@0 1961 aCSPRep.log(ERROR_FLAG, aMessage);
michael@0 1962 } else {
michael@0 1963 (new CSPRep()).log(ERROR_FLAG, aMessage);
michael@0 1964 }
michael@0 1965 }
michael@0 1966
michael@0 1967 function cspWarn(aCSPRep, aMessage) {
michael@0 1968 if (aCSPRep) {
michael@0 1969 aCSPRep.log(WARN_FLAG, aMessage);
michael@0 1970 } else {
michael@0 1971 (new CSPRep()).log(WARN_FLAG, aMessage);
michael@0 1972 }
michael@0 1973 }
michael@0 1974
michael@0 1975 //////////////////////////////////////////////////////////////////////
michael@0 1976
michael@0 1977 this.CSPLocalizer = {
michael@0 1978 /**
michael@0 1979 * Retrieve a localized string.
michael@0 1980 *
michael@0 1981 * @param string aName
michael@0 1982 * The string name you want from the CSP string bundle.
michael@0 1983 * @return string
michael@0 1984 * The localized string.
michael@0 1985 */
michael@0 1986 getStr: function CSPLoc_getStr(aName)
michael@0 1987 {
michael@0 1988 let result;
michael@0 1989 try {
michael@0 1990 result = this.stringBundle.GetStringFromName(aName);
michael@0 1991 }
michael@0 1992 catch (ex) {
michael@0 1993 Cu.reportError("Failed to get string: " + aName);
michael@0 1994 throw ex;
michael@0 1995 }
michael@0 1996 return result;
michael@0 1997 },
michael@0 1998
michael@0 1999 /**
michael@0 2000 * Retrieve a localized string formatted with values coming from the given
michael@0 2001 * array.
michael@0 2002 *
michael@0 2003 * @param string aName
michael@0 2004 * The string name you want from the CSP string bundle.
michael@0 2005 * @param array aArray
michael@0 2006 * The array of values you want in the formatted string.
michael@0 2007 * @return string
michael@0 2008 * The formatted local string.
michael@0 2009 */
michael@0 2010 getFormatStr: function CSPLoc_getFormatStr(aName, aArray)
michael@0 2011 {
michael@0 2012 let result;
michael@0 2013 try {
michael@0 2014 result = this.stringBundle.formatStringFromName(aName, aArray, aArray.length);
michael@0 2015 }
michael@0 2016 catch (ex) {
michael@0 2017 Cu.reportError("Failed to format string: " + aName);
michael@0 2018 throw ex;
michael@0 2019 }
michael@0 2020 return result;
michael@0 2021 },
michael@0 2022 };
michael@0 2023
michael@0 2024 XPCOMUtils.defineLazyGetter(CSPLocalizer, "stringBundle", function() {
michael@0 2025 return Services.strings.createBundle(STRINGS_URI);
michael@0 2026 });

mercurial