|
|
|
|
| 1 |
/* ***** BEGIN LICENSE BLOCK ***** |
| 2 |
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
| 3 |
* |
| 4 |
* The contents of this file are subject to the Mozilla Public License Version |
| 5 |
* 1.1 (the "License"); you may not use this file except in compliance with |
| 6 |
* the License. You may obtain a copy of the License at |
| 7 |
* http://www.mozilla.org/MPL/ |
| 8 |
* |
| 9 |
* Software distributed under the License is distributed on an "AS IS" basis, |
| 10 |
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
| 11 |
* for the specific language governing rights and limitations under the |
| 12 |
* License. |
| 13 |
* |
| 14 |
* The Original Code is the Content Security Policy data structures. |
| 15 |
* |
| 16 |
* The Initial Developer of the Original Code is |
| 17 |
* Mozilla Corporation |
| 18 |
* |
| 19 |
* Contributor(s): |
| 20 |
* Sid Stamm <sid@mozilla.com> |
| 21 |
* |
| 22 |
* Alternatively, the contents of this file may be used under the terms of |
| 23 |
* either the GNU General Public License Version 2 or later (the "GPL"), or |
| 24 |
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
| 25 |
* in which case the provisions of the GPL or the LGPL are applicable instead |
| 26 |
* of those above. If you wish to allow use of your version of this file only |
| 27 |
* under the terms of either the GPL or the LGPL, and not to allow others to |
| 28 |
* use your version of this file under the terms of the MPL, indicate your |
| 29 |
* decision by deleting the provisions above and replace them with the notice |
| 30 |
* and other provisions required by the GPL or the LGPL. If you do not delete |
| 31 |
* the provisions above, a recipient may use your version of this file under |
| 32 |
* the terms of any one of the MPL, the GPL or the LGPL. |
| 33 |
* |
| 34 |
* ***** END LICENSE BLOCK ***** */ |
| 35 |
|
| 36 |
/** |
| 37 |
* Content Security Policy Utilities |
| 38 |
* |
| 39 |
* Overview |
| 40 |
* This contains a set of classes and utilities for CSP. It is in this |
| 41 |
* separate file for testing purposes. |
| 42 |
*/ |
| 43 |
|
| 44 |
// Module stuff |
| 45 |
var EXPORTED_SYMBOLS = ["CSPRep", "CSPSourceList", "CSPSource", |
| 46 |
"CSPHost", "CSPWarning", "CSPError", "CSPdebug"]; |
| 47 |
|
| 48 |
|
| 49 |
//these are not exported |
| 50 |
var gIoService = Components.classes["@mozilla.org/network/io-service;1"] |
| 51 |
.getService(Components.interfaces.nsIIOService); |
| 52 |
|
| 53 |
var gETLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"] |
| 54 |
.getService(Components.interfaces.nsIEffectiveTLDService); |
| 55 |
|
| 56 |
|
| 57 |
function CSPWarning(aMsg) { |
| 58 |
//customize this to redirect output. |
| 59 |
aMsg = 'CSP WARN: ' + aMsg + "\n"; |
| 60 |
dump(aMsg); |
| 61 |
Components.classes["@mozilla.org/consoleservice;1"] |
| 62 |
.getService(Components.interfaces.nsIConsoleService) |
| 63 |
.logStringMessage(aMsg); |
| 64 |
} |
| 65 |
function CSPError(aMsg) { |
| 66 |
aMsg = 'CSP ERROR: ' + aMsg + "\n"; |
| 67 |
dump(aMsg); |
| 68 |
Components.classes["@mozilla.org/consoleservice;1"] |
| 69 |
.getService(Components.interfaces.nsIConsoleService) |
| 70 |
.logStringMessage(aMsg); |
| 71 |
} |
| 72 |
function CSPdebug(aMsg) { |
| 73 |
aMsg = 'CSP debug: ' + aMsg + "\n"; |
| 74 |
dump(aMsg); |
| 75 |
Components.classes["@mozilla.org/consoleservice;1"] |
| 76 |
.getService(Components.interfaces.nsIConsoleService) |
| 77 |
.logStringMessage(aMsg); |
| 78 |
} |
| 79 |
|
| 80 |
|
| 81 |
//:::::::::::::::::::::::: CLASSES ::::::::::::::::::::::::::// |
| 82 |
|
| 83 |
/** |
| 84 |
* Class that represents a parsed policy structure. |
| 85 |
*/ |
| 86 |
function CSPRep() { |
| 87 |
// this gets set to true when the policy is done parsing, or when a |
| 88 |
// URI-borne policy has finished loading. |
| 89 |
this._isInitialized = false; |
| 90 |
|
| 91 |
this._allowEval = false; |
| 92 |
this._allowInlineScripts = false; |
| 93 |
|
| 94 |
// don't auto-populate _directives, so it is easier to find bugs |
| 95 |
this._directives = {}; |
| 96 |
|
| 97 |
} |
| 98 |
CSPRep.SRC_DIRECTIVES = { |
| 99 |
ALLOW: "allow", |
| 100 |
SCRIPT_SRC: "script-src", |
| 101 |
STYLE_SRC: "style-src", |
| 102 |
MEDIA_SRC: "media-src", |
| 103 |
IMG_SRC: "img-src", |
| 104 |
OBJECT_SRC: "object-src", |
| 105 |
FRAME_SRC: "frame-src", |
| 106 |
FRAME_ANCESTORS: "frame-ancestors", |
| 107 |
FONT_SRC: "font-src", |
| 108 |
XHR_SRC: "xhr-src" |
| 109 |
}; |
| 110 |
CSPRep.URI_DIRECTIVES = { |
| 111 |
REPORT_URI: "report-uri", |
| 112 |
POLICY_URI: "policy-uri" |
| 113 |
}; |
| 114 |
|
| 115 |
|
| 116 |
/** |
| 117 |
* Factory to create a new CSPRep, parsed from a string. |
| 118 |
* |
| 119 |
* @param aStr |
| 120 |
* string rep of a CSP |
| 121 |
* @param self (optional) |
| 122 |
* string or CSPSource representing the "self" source |
| 123 |
* @returns |
| 124 |
* an instance of CSPRep |
| 125 |
*/ |
| 126 |
CSPRep.fromString = function(aStr, self) { |
| 127 |
var SD = CSPRep.SRC_DIRECTIVES; |
| 128 |
var UD = CSPRep.URI_DIRECTIVES; |
| 129 |
var pieces = aStr.split(/;\s*/); |
| 130 |
var aCSPR = new CSPRep(); |
| 131 |
|
| 132 |
// NOTE: currently, every time a directive is encountered, its value |
| 133 |
// overwrites any previous values for the same directive. So the policy |
| 134 |
// "dir foo; dir bar" gets parsed as "dir bar". |
| 135 |
|
| 136 |
piece:for(var i in pieces) { |
| 137 |
//split off first token and figure out what to do |
| 138 |
var dirname = pieces[i].match(/(\S+)/)[0]; |
| 139 |
|
| 140 |
//careful to trim the stuff so there's no extra tokens |
| 141 |
var dirvalue = pieces[i].substring(dirname.length) |
| 142 |
.replace(/^\s+/,'') |
| 143 |
.replace(/\s+$/,''); |
| 144 |
|
| 145 |
//If it is the script-src directive, pull out "eval" and "inline" |
| 146 |
if(dirname == SD.SCRIPT_SRC) { |
| 147 |
let listchunks = dirvalue.split(/\s+/); |
| 148 |
for(let i in listchunks) { |
| 149 |
if(listchunks[i] == "'eval'") { |
| 150 |
listchunks.splice(i,1); |
| 151 |
aCSPR._allowEval = true; |
| 152 |
} |
| 153 |
if(listchunks[i] == "'inline'") { |
| 154 |
listchunks.splice(i,1); |
| 155 |
aCSPR._allowInlineScripts = true; |
| 156 |
} |
| 157 |
} |
| 158 |
// repair source list without the keywords |
| 159 |
dirvalue = listchunks.join(" "); |
| 160 |
} |
| 161 |
|
| 162 |
//is it a SRC directive? |
| 163 |
for(var sdi in SD) { |
| 164 |
if(dirname == SD[sdi]) { |
| 165 |
var dv = CSPSourceList.fromString(dirvalue, self); |
| 166 |
if(dv) { // fromString() can return null if the list is bad |
| 167 |
aCSPR._directives[SD[sdi]] = dv; |
| 168 |
continue piece; |
| 169 |
} |
| 170 |
} |
| 171 |
} |
| 172 |
|
| 173 |
//not a SRC directive, check URI directives |
| 174 |
|
| 175 |
// POLICY URI ////////////////////////////////////////////////////////// |
| 176 |
if(dirname == UD.POLICY_URI) { |
| 177 |
// POLICY_URI can only be alone |
| 178 |
if(aCSPR._directives.length > 0 || pieces.length > 1) { |
| 179 |
CSPError("policy-uri directive can only appear alone"); |
| 180 |
return CSPRep.fromString("allow 'none'"); |
| 181 |
} |
| 182 |
|
| 183 |
var uri = ''; |
| 184 |
try { |
| 185 |
uri = gIoService.newURI(dirvalue, null, null); |
| 186 |
} catch(e) { |
| 187 |
CSPError("could not parse URI in policy URI: " + dirvalue); |
| 188 |
return CSPRep.fromString("allow 'none'"); |
| 189 |
} |
| 190 |
|
| 191 |
// Verify that policy URI comes from the same origin |
| 192 |
if(self) { |
| 193 |
var selfUri = gIoService.newURI(self.toString(), null, null); |
| 194 |
if(selfUri.host !== uri.host){ |
| 195 |
CSPError("can't fetch policy uri from non-matching hostname: " + uri.host); |
| 196 |
return CSPRep.fromString("allow 'none'"); |
| 197 |
} |
| 198 |
if(selfUri.port !== uri.port){ |
| 199 |
CSPError("can't fetch policy uri from non-matching port: " + uri.port); |
| 200 |
return CSPRep.fromString("allow 'none'"); |
| 201 |
} |
| 202 |
if(selfUri.scheme !== uri.scheme){ |
| 203 |
CSPError("can't fetch policy uri from non-matching scheme: " + uri.scheme); |
| 204 |
return CSPRep.fromString("allow 'none'"); |
| 205 |
} |
| 206 |
} |
| 207 |
|
| 208 |
var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] |
| 209 |
.createInstance(Components.interfaces.nsIXMLHttpRequest); |
| 210 |
|
| 211 |
// insert error hook |
| 212 |
req.onerror = CSPError; |
| 213 |
|
| 214 |
// synchronous -- otherwise we need to architect a callback into the |
| 215 |
// xpcom component so that whomever creates the policy object gets |
| 216 |
// notified when it's loaded and ready to go. |
| 217 |
req.open("GET", dirvalue, false); |
| 218 |
|
| 219 |
// make request anonymous |
| 220 |
// This prevents sending cookies with the request, in case the policy URI |
| 221 |
// is injected, it can't be abused for CSRF. |
| 222 |
req.channel.loadFlags |= Components.interfaces.nsIChannel.LOAD_ANONYMOUS; |
| 223 |
|
| 224 |
//CSPdebug("Fetching URI " + dirvalue); |
| 225 |
req.send(null); |
| 226 |
if(req.status == 200) { |
| 227 |
//CSPdebug("XHR returned: " + req.responseText); |
| 228 |
aCSPR = CSPRep.fromString(req.responseText, self); |
| 229 |
//remember where we got the policy |
| 230 |
aCSPR._directives[UD.POLICY_URI] = dirvalue; |
| 231 |
return aCSPR; |
| 232 |
} |
| 233 |
CSPError("Error fetching policy URI: server response was " + req.status); |
| 234 |
return CSPRep.fromString("allow 'none'"); |
| 235 |
} |
| 236 |
|
| 237 |
// REPORT URI ////////////////////////////////////////////////////////// |
| 238 |
if(dirname == UD.REPORT_URI) { |
| 239 |
// supporting a space-separated list of URIs |
| 240 |
var uriStrings = dirvalue.split(/\s+/); |
| 241 |
var okUriStrings = []; |
| 242 |
var selfUri = self ? gIoService.newURI(self.toString(),null,null) : null; |
| 243 |
|
| 244 |
// Verify that each report URI is in the same etld + 1 |
| 245 |
// if "self" is defined, and just that it's valid otherwise. |
| 246 |
for(let i in uriStrings) { |
| 247 |
try { |
| 248 |
var uri = gIoService.newURI(uriStrings[i]); |
| 249 |
if(self) { |
| 250 |
if( gETLDService.getBaseDomain(uri) === |
| 251 |
gETLDService.getBaseDomain(selfUri) ) { |
| 252 |
okUriStrings.push(uriStrings[i]); |
| 253 |
} else { |
| 254 |
CSPWarning("can't use report URI from non-matching eTLD+1: " |
| 255 |
+ gETLDService.getBaseDomain(uri)); |
| 256 |
} |
| 257 |
} |
| 258 |
} catch(e) { |
| 259 |
CSPWarning("couldn't parse report URI: " + dirvalue); |
| 260 |
} |
| 261 |
} |
| 262 |
aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' '); |
| 263 |
|
| 264 |
continue piece; |
| 265 |
} |
| 266 |
|
| 267 |
//no directives matched |
| 268 |
CSPWarning("Couldn't process unknown directive '" + dirname + "'"); |
| 269 |
} |
| 270 |
|
| 271 |
if(aCSPR._directives.length < 1) { |
| 272 |
CSPError("invalid policy -- no recognized directives"); |
| 273 |
return CSPRep.fromString("allow 'none'"); |
| 274 |
} |
| 275 |
|
| 276 |
if(!aCSPR._directives[SD.ALLOW]) { |
| 277 |
CSPWarning("allow directive is not present, using \"allow 'none'\""); |
| 278 |
aCSPR._directives[SD.ALLOW] = CSPSourceList.fromString("'none'"); |
| 279 |
} |
| 280 |
|
| 281 |
aCSPR.makeExplicit(); |
| 282 |
return aCSPR; |
| 283 |
}; |
| 284 |
|
| 285 |
CSPRep.prototype = { |
| 286 |
/** |
| 287 |
* Returns array of all report uris defined, or 'none' if there are none. |
| 288 |
*/ |
| 289 |
getReportURIs: |
| 290 |
function() { |
| 291 |
if(!this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI]) |
| 292 |
return []; |
| 293 |
|
| 294 |
var uris = this._directives[CSPRep.URI_DIRECTIVES.REPORT_URI].split(","); |
| 295 |
for(let i in uris) { |
| 296 |
//trim off surrounding whitespace |
| 297 |
uris[i] = uris[i].replace(/^\s+/, "").replace(/\s+$/, ""); |
| 298 |
} |
| 299 |
return uris; |
| 300 |
}, |
| 301 |
|
| 302 |
/** |
| 303 |
* Compares this CSPRep instance to another. |
| 304 |
*/ |
| 305 |
equals: |
| 306 |
function(that) { |
| 307 |
if(this._directives.length != that._directives.length) { |
| 308 |
return false; |
| 309 |
} |
| 310 |
for(var i in this._directives) { |
| 311 |
if(!that._directives[i] || !this._directives[i].equals(that._directives[i])) { |
| 312 |
return false; |
| 313 |
} |
| 314 |
} |
| 315 |
return true; |
| 316 |
}, |
| 317 |
|
| 318 |
/** |
| 319 |
* Generates string representation of the policy. Should be fairly similar |
| 320 |
* to the original. |
| 321 |
*/ |
| 322 |
toString: |
| 323 |
function csp_toString() { |
| 324 |
var dirs = []; |
| 325 |
for(var i in this._directives) { |
| 326 |
if(this._directives[i]) { |
| 327 |
dirs.push(i + " " + this._directives[i].toString()); |
| 328 |
} |
| 329 |
} |
| 330 |
return dirs.join("; "); |
| 331 |
}, |
| 332 |
|
| 333 |
|
| 334 |
/** |
| 335 |
* Determines if this policy accepts a URI. |
| 336 |
* @param aContext |
| 337 |
* one of the SRC_DIRECTIVES defined above |
| 338 |
* @returns |
| 339 |
* true if the policy permits the URI in given context. |
| 340 |
*/ |
| 341 |
permits: |
| 342 |
function csp_permits(aURI, aContext) { |
| 343 |
|
| 344 |
// GLOBALLY ALLOW "about:" SCHEME |
| 345 |
if(aURI instanceof String && aURI.substring(0,6) == "about:") { |
| 346 |
return true; |
| 347 |
} |
| 348 |
if(aURI instanceof Components.interfaces.nsIURI |
| 349 |
&& aURI.scheme == "about") { |
| 350 |
return true; |
| 351 |
} |
| 352 |
|
| 353 |
//make sure the context is valid |
| 354 |
for(var i in CSPRep.SRC_DIRECTIVES) { |
| 355 |
if(CSPRep.SRC_DIRECTIVES[i] == aContext) { |
| 356 |
return this._directives[aContext].permits(aURI); |
| 357 |
} |
| 358 |
} |
| 359 |
return false; |
| 360 |
}, |
| 361 |
|
| 362 |
/** |
| 363 |
* Intersects with another CSPRep, deciding the subset policy |
| 364 |
* that should be enforced, and returning a new instance. |
| 365 |
* @param aCSPRep |
| 366 |
* a CSPRep instance to use as "other" CSP |
| 367 |
* @returns |
| 368 |
* a new CSPRep instance of the intersection |
| 369 |
*/ |
| 370 |
intersectWith: |
| 371 |
function cspsd_intersectWith(aCSPRep) { |
| 372 |
var newRep = new CSPRep(); |
| 373 |
|
| 374 |
for(var dir in CSPRep.SRC_DIRECTIVES) { |
| 375 |
var dirv = CSPRep.SRC_DIRECTIVES[dir]; |
| 376 |
newRep._directives[dirv] = this._directives[dirv] |
| 377 |
.intersectWith(aCSPRep._directives[dirv]); |
| 378 |
} |
| 379 |
return newRep; |
| 380 |
}, |
| 381 |
|
| 382 |
|
| 383 |
/** |
| 384 |
* Copies default source list to each unspecified directive. |
| 385 |
* @returns |
| 386 |
* true if the makeExplicit succeeds |
| 387 |
* false if it fails (for some weird reason) |
| 388 |
*/ |
| 389 |
makeExplicit: |
| 390 |
function cspsd_makeExplicit() { |
| 391 |
var SD = CSPRep.SRC_DIRECTIVES; |
| 392 |
var allowDir = this._directives[SD.ALLOW]; |
| 393 |
if(!allowDir) { |
| 394 |
return false; |
| 395 |
} |
| 396 |
|
| 397 |
for(var dir in SD) { |
| 398 |
var dirv = SD[dir]; |
| 399 |
if(dirv == SD.ALLOW) { |
| 400 |
continue; |
| 401 |
} |
| 402 |
if(!this._directives[dirv]) { |
| 403 |
//implicit directive, make explicit |
| 404 |
this._directives[dirv] = allowDir.clone(); |
| 405 |
} |
| 406 |
} |
| 407 |
this._isInitialized = true; |
| 408 |
return true; |
| 409 |
}, |
| 410 |
|
| 411 |
/** |
| 412 |
* Returns true if "eval" is enabled through the "eval" keyword. |
| 413 |
*/ |
| 414 |
get allowsEvalInScripts () { |
| 415 |
return this._allowEval; |
| 416 |
}, |
| 417 |
|
| 418 |
/** |
| 419 |
* Returns true if inline scripts are enabled through the "inline" |
| 420 |
* keyword. |
| 421 |
*/ |
| 422 |
get allowsInlineScripts () { |
| 423 |
return this._allowInlineScripts; |
| 424 |
}, |
| 425 |
}; |
| 426 |
|
| 427 |
|
| 428 |
////////////////////////////////////////////////////////////////////// |
| 429 |
/** |
| 430 |
* Class to represent a list of sources |
| 431 |
*/ |
| 432 |
function CSPSourceList() { |
| 433 |
this._sources = []; |
| 434 |
this._permitAllSources = false; |
| 435 |
} |
| 436 |
|
| 437 |
/** |
| 438 |
* Factory to create a new CSPSourceList, parsed from a string. |
| 439 |
* |
| 440 |
* @param aStr |
| 441 |
* string rep of a CSP Source List |
| 442 |
* @param self (optional) |
| 443 |
* string or CSPSource representing the "self" source |
| 444 |
* @returns |
| 445 |
* an instance of CSPSourceList |
| 446 |
*/ |
| 447 |
CSPSourceList.fromString = function(aStr, self) { |
| 448 |
//Source list is: |
| 449 |
// <host-dir-value> ::= <source-list> |
| 450 |
// | "'none'" |
| 451 |
// <source-list> ::= <source> |
| 452 |
// | <source-list>" "<source> |
| 453 |
|
| 454 |
var slObj = new CSPSourceList(); |
| 455 |
if(aStr == "'none'") { |
| 456 |
return slObj; |
| 457 |
} |
| 458 |
|
| 459 |
var ss = aStr.split(/\s+/); |
| 460 |
for(var i in ss) { |
| 461 |
var o = CSPSource.fromString(ss[i], self); |
| 462 |
|
| 463 |
//skip sources that don't parse |
| 464 |
if(o) |
| 465 |
slObj._sources[i] = o; |
| 466 |
else |
| 467 |
CSPWarning("Failed to parse invalid source: " + ss[i]); |
| 468 |
} |
| 469 |
return slObj; |
| 470 |
}; |
| 471 |
|
| 472 |
CSPSourceList.prototype = { |
| 473 |
|
| 474 |
/** |
| 475 |
* Compares one CSPSourceList to another. |
| 476 |
* |
| 477 |
* @param that |
| 478 |
* another CSPSourceList |
| 479 |
* @returns |
| 480 |
* true if they have the same data |
| 481 |
*/ |
| 482 |
equals: |
| 483 |
function(that) { |
| 484 |
if(that._sources.length != this._sources.length) { |
| 485 |
return false; |
| 486 |
} |
| 487 |
//sort both arrays and compare like a zipper |
| 488 |
//XXX (sid): I think we can make this more efficient |
| 489 |
var sortfn = function(a,b) { |
| 490 |
return a.toString() > b.toString(); |
| 491 |
}; |
| 492 |
var a_sorted = this._sources.sort(sortfn); |
| 493 |
var b_sorted = that._sources.sort(sortfn); |
| 494 |
for(var i in a_sorted) { |
| 495 |
if(!a_sorted[i].equals(b_sorted[i])) { |
| 496 |
return false; |
| 497 |
} |
| 498 |
} |
| 499 |
return true; |
| 500 |
}, |
| 501 |
|
| 502 |
/** |
| 503 |
* Generates string representation of the Source List. |
| 504 |
* Should be fairly similar to the original. |
| 505 |
*/ |
| 506 |
toString: |
| 507 |
function() { |
| 508 |
if(this.isNone()) { |
| 509 |
return "'none'"; |
| 510 |
} |
| 511 |
return this._sources.map(function(x) { return x.toString(); }).join(" "); |
| 512 |
}, |
| 513 |
|
| 514 |
/** |
| 515 |
* Returns whether or not this source list represents the "'none'" special |
| 516 |
* case. |
| 517 |
*/ |
| 518 |
isNone: |
| 519 |
function() { |
| 520 |
return (!this._permitAllSources) && (this._sources.length < 1); |
| 521 |
}, |
| 522 |
|
| 523 |
/** |
| 524 |
* Makes a new instance that resembles this object. |
| 525 |
* @returns |
| 526 |
* a new CSPSourceList |
| 527 |
*/ |
| 528 |
clone: |
| 529 |
function() { |
| 530 |
var aSL = new CSPSourceList(); |
| 531 |
aSL._permitAllSources = this._permitAllSources; |
| 532 |
for(var i in this._sources) { |
| 533 |
aSL._sources[i] = this._sources[i].clone(); |
| 534 |
} |
| 535 |
return aSL; |
| 536 |
}, |
| 537 |
|
| 538 |
|
| 539 |
/** |
| 540 |
* Determines if this directive accepts a URI. |
| 541 |
* @param aURI |
| 542 |
* the URI in question |
| 543 |
* @returns |
| 544 |
* true if the URI matches a source in this source list. |
| 545 |
*/ |
| 546 |
permits: |
| 547 |
function cspsd_permits(aURI) { |
| 548 |
if(this.isNone()) { |
| 549 |
return false; |
| 550 |
} |
| 551 |
for(var i in this._sources) { |
| 552 |
if(this._sources[i].permits(aURI)) { |
| 553 |
return true; |
| 554 |
} |
| 555 |
} |
| 556 |
return false; |
| 557 |
}, |
| 558 |
|
| 559 |
|
| 560 |
/** |
| 561 |
* Intersects with another CSPSourceList, deciding the subset directive |
| 562 |
* that should be enforced, and returning a new instance. |
| 563 |
* @param that |
| 564 |
* the other CSPSourceList to intersect "this" with |
| 565 |
* @returns |
| 566 |
* a new instance of a CSPSourceList representing the intersection |
| 567 |
*/ |
| 568 |
intersectWith: |
| 569 |
function cspsd_intersectWith(that) { |
| 570 |
if(this._permitAllSources) { |
| 571 |
return that.clone(); |
| 572 |
} |
| 573 |
if(that._permitAllSources) { |
| 574 |
return this.clone(); |
| 575 |
} |
| 576 |
|
| 577 |
// "'none'" case |
| 578 |
if(this.isNone() || that.isNone()) { |
| 579 |
return CSPSourceList.fromString("'none'"); |
| 580 |
} |
| 581 |
|
| 582 |
// -- find only common sources |
| 583 |
|
| 584 |
//XXX (sid): figure out a good algorithm for this. |
| 585 |
// This is horribly inefficient. O(n^2) |
| 586 |
var isrcs = []; |
| 587 |
for(var i in this._sources) { |
| 588 |
for(var j in that._sources) { |
| 589 |
var s = that._sources[j].intersectWith(this._sources[i]); |
| 590 |
if(s) { |
| 591 |
isrcs.push(s); |
| 592 |
} |
| 593 |
} |
| 594 |
} |
| 595 |
//Next, remove duplicates |
| 596 |
dup:for(var i = 0; i < isrcs.length; i++) { |
| 597 |
for(var j = 0; j < i; j++) { |
| 598 |
if(isrcs[i].equals(isrcs[j])) { |
| 599 |
isrcs.splice(i, 1); |
| 600 |
i--; |
| 601 |
continue dup; |
| 602 |
} |
| 603 |
} |
| 604 |
} |
| 605 |
var newCSPSrcList = new CSPSourceList(); |
| 606 |
newCSPSrcList._sources = isrcs; |
| 607 |
return newCSPSrcList; |
| 608 |
} |
| 609 |
} |
| 610 |
|
| 611 |
|
| 612 |
////////////////////////////////////////////////////////////////////// |
| 613 |
/** |
| 614 |
* Class to model a source (scheme, host, port) |
| 615 |
*/ |
| 616 |
function CSPSource() { |
| 617 |
this._scheme = "*"; |
| 618 |
this._port = "*"; |
| 619 |
this._host = CSPHost.fromString("*"); |
| 620 |
} |
| 621 |
|
| 622 |
/** |
| 623 |
* Factory to create a new CSPSource, from a nsIURI. |
| 624 |
* |
| 625 |
* @param aURI |
| 626 |
* nsIURI rep of a URI |
| 627 |
* @param self (optional) |
| 628 |
* string or CSPSource representing the "self" source |
| 629 |
* @returns |
| 630 |
* an instance of CSPSource |
| 631 |
*/ |
| 632 |
CSPSource.fromURI = function(aURI, self) { |
| 633 |
|
| 634 |
if( !(aURI instanceof Components.interfaces.nsIURI) ){ |
| 635 |
CSPError("Provided argument is not an nsIURI"); |
| 636 |
return null; |
| 637 |
} |
| 638 |
|
| 639 |
if(typeof self === "string") { |
| 640 |
self = CSPSource.fromString(self); |
| 641 |
} |
| 642 |
|
| 643 |
var sObj = new CSPSource(); |
| 644 |
|
| 645 |
try { |
| 646 |
//if there was a scheme... |
| 647 |
sObj._scheme = aURI.scheme; |
| 648 |
} catch (e) { |
| 649 |
// aaah! No scheme even though we tried two methods to force a scheme into |
| 650 |
// the source. |
| 651 |
CSPError("failed parsing source: '" + aURI.asciiSpec + "'"); |
| 652 |
return null; |
| 653 |
} |
| 654 |
|
| 655 |
//now, check if there was a host |
| 656 |
try { |
| 657 |
sObj._host = CSPHost.fromString(aURI.host); |
| 658 |
|
| 659 |
//and if there was a host, copy the port (if present) |
| 660 |
if(aURI.port > 0) { |
| 661 |
sObj._port = aURI.port; |
| 662 |
} else { |
| 663 |
// either port was absent or *. |
| 664 |
// If empty, the port is inherited from "self" |
| 665 |
if(aStr.substr(-2) === ":*") |
| 666 |
sObj._port = "*"; |
| 667 |
else |
| 668 |
sObj._port = self._port; |
| 669 |
} |
| 670 |
} catch(e) { |
| 671 |
// no host! (or an error getting the port) |
| 672 |
// It's ok if there's no host (scheme-only sources like javascript: |
| 673 |
// allowed). |
| 674 |
// Copy in any data that needs to be inherited, but if there's no |
| 675 |
// host, there's no need for a port either. "*" as host _WILL_ be |
| 676 |
// parsed correctly by newURI(), and won't throw an exception |
| 677 |
// putting us here. |
| 678 |
if(sObj._host && !sObj._port) |
| 679 |
sObj._port = self._port; |
| 680 |
} |
| 681 |
return sObj; |
| 682 |
}; |
| 683 |
|
| 684 |
/** |
| 685 |
* Factory to create a new CSPSource, parsed from a string. |
| 686 |
* |
| 687 |
* @param aStr |
| 688 |
* string rep of a CSP Source |
| 689 |
* @param self (optional) |
| 690 |
* string or CSPSource representing the "self" source |
| 691 |
* @returns |
| 692 |
* an instance of CSPSource |
| 693 |
*/ |
| 694 |
CSPSource.fromString = function(aStr, self) { |
| 695 |
//take care of 'self' keyword |
| 696 |
if(aStr == "'self'") { |
| 697 |
if(typeof self === 'undefined') { |
| 698 |
CSPError("self keyword used, but no self data specified"); |
| 699 |
return null; |
| 700 |
} |
| 701 |
if(self == "'self'") { |
| 702 |
CSPError("self reference cannot be 'self'"); |
| 703 |
return null; |
| 704 |
} |
| 705 |
if(self instanceof CSPSource) { |
| 706 |
return self.clone(); |
| 707 |
} |
| 708 |
return CSPSource.fromString(self); |
| 709 |
} |
| 710 |
|
| 711 |
if(typeof self === "string") { |
| 712 |
self = CSPSource.fromString(self); |
| 713 |
} |
| 714 |
|
| 715 |
|
| 716 |
let chunks = aStr.split(":"); |
| 717 |
// Workaround for problems: |
| 718 |
// - "*" not parsed right by the URI parser |
| 719 |
// ... solution: special case for the * source |
| 720 |
if(aStr == "*") |
| 721 |
return new CSPSource(); //default is *://*:* |
| 722 |
|
| 723 |
// - a.com is not easily parsed by gIoService.newURI(), need to add a scheme |
| 724 |
// - a.com:20 is ambiguous. Could mean scheme = a.com, or host = a.com to |
| 725 |
// the io-service URI parser |
| 726 |
// ... but since :20 is not a valid host, CSP should treat it as a |
| 727 |
// host:port combination. |
| 728 |
// ... not a problem for three-chunk values like x:a.com:20 |
| 729 |
// ... solution: prepend self's scheme |
| 730 |
if(chunks.length == 1 || chunks.length == 2 && chunks[1].match(/^\d+$/)) { |
| 731 |
if(!self) { |
| 732 |
CSPError("can't parse ambiguous source: '" + aStr + "' without 'self' defined"); |
| 733 |
return null; |
| 734 |
} |
| 735 |
aStr = self._scheme + "://" + aStr; |
| 736 |
} |
| 737 |
|
| 738 |
// - ports are not validated ('3f' is turned into '-1') |
| 739 |
// ... solution: validate ports manually |
| 740 |
if(chunks.length == 3 && !(chunks[2].match(/^\d+$/) || chunks[2] === "*")) { |
| 741 |
CSPError("invalid port string: '" + chunks[2] + "' not entirely numeric."); |
| 742 |
return null; |
| 743 |
} |
| 744 |
|
| 745 |
// Try to parse the URI |
| 746 |
var uriSource; |
| 747 |
try { |
| 748 |
uriSource = gIoService.newURI(aStr, null, null); |
| 749 |
} catch(e) { |
| 750 |
try { |
| 751 |
//this means there was no scheme specified, or the URI was malformed. |
| 752 |
// Need to use backup parsing mechanism here, with "self" defining the |
| 753 |
// scheme. |
| 754 |
let bStr = self._scheme + "://" + aStr; |
| 755 |
uriSource = gIoService.newURI(bStr, null, null); |
| 756 |
} catch(e) { |
| 757 |
//fatal error! Can't parse.. there must be a scheme or at least a |
| 758 |
//parseable host! |
| 759 |
CSPError("failed parsing source: '" + aStr + "'"); |
| 760 |
return null; |
| 761 |
} |
| 762 |
} |
| 763 |
return CSPSource.fromURI(uriSource); |
| 764 |
}; |
| 765 |
|
| 766 |
CSPSource.prototype = { |
| 767 |
|
| 768 |
/** |
| 769 |
* Generates string representation of the Source. |
| 770 |
* Should be fairly similar to the original. |
| 771 |
*/ |
| 772 |
toString: |
| 773 |
function() { |
| 774 |
var s = this._host; |
| 775 |
if(this._port && this._port != "*") { |
| 776 |
s = s + ":" + this._port; |
| 777 |
} |
| 778 |
if(this._scheme && this._scheme != "*") { |
| 779 |
s = this._scheme + "://" + s; |
| 780 |
} |
| 781 |
return s; |
| 782 |
}, |
| 783 |
|
| 784 |
/** |
| 785 |
* Makes a new instance that resembles this object. |
| 786 |
* @returns |
| 787 |
* a new CSPSource |
| 788 |
*/ |
| 789 |
clone: |
| 790 |
function() { |
| 791 |
var aClone = new CSPSource(); |
| 792 |
aClone._scheme = this._scheme; |
| 793 |
aClone._port = this._port; |
| 794 |
aClone._host = this._host ? this._host.clone() : null; |
| 795 |
return aClone; |
| 796 |
}, |
| 797 |
|
| 798 |
/** |
| 799 |
* Determines if this Source accepts a URI. |
| 800 |
* @param aURI |
| 801 |
* the URI in question |
| 802 |
* @returns |
| 803 |
* true if the URI matches a source in this source list. |
| 804 |
*/ |
| 805 |
permits: |
| 806 |
function(aSource) { |
| 807 |
if(aSource instanceof Components.interfaces.nsIURI) { |
| 808 |
//figure out based on URI |
| 809 |
return this.permits(CSPSource.fromURI(aSource)); |
| 810 |
} |
| 811 |
if(!(aSource instanceof CSPSource)) { |
| 812 |
//convert to CSPSource first |
| 813 |
return this.permits(CSPSource.fromString(aSource)); |
| 814 |
} |
| 815 |
|
| 816 |
//verify scheme, host, and port |
| 817 |
if( (this._scheme == "*" || this._scheme == aSource._scheme) |
| 818 |
&& (this._port == "*" || this._port == aSource._port) ){ |
| 819 |
return this._host === "*" || |
| 820 |
this._host.permits(aSource._host); |
| 821 |
} |
| 822 |
// something didn't match. |
| 823 |
return false; |
| 824 |
}, |
| 825 |
|
| 826 |
/** |
| 827 |
* Determines the intersection of two sources. |
| 828 |
* Returns a null object if intersection generates no |
| 829 |
* hosts that satisfy it. |
| 830 |
* @param that |
| 831 |
* the other CSPSource to intersect "this" with |
| 832 |
* @returns |
| 833 |
* a new instance of a CSPSource representing the intersection |
| 834 |
*/ |
| 835 |
intersectWith: |
| 836 |
function(that) { |
| 837 |
var newSource = new CSPSource(); |
| 838 |
|
| 839 |
//port |
| 840 |
if(this._port == "*") { |
| 841 |
newSource._port = that._port; |
| 842 |
} else if(that._port == "*") { |
| 843 |
newSource._port = this._port; |
| 844 |
} else if(that._port == this._port) { |
| 845 |
newSource._port = this._port; |
| 846 |
} else { |
| 847 |
//CSPError("Could not intersect " + this + " with " + that |
| 848 |
// + " due to port problems."); |
| 849 |
return null; |
| 850 |
} |
| 851 |
|
| 852 |
//scheme |
| 853 |
if(this._scheme == "*") { |
| 854 |
newSource._scheme = that._scheme; |
| 855 |
} else if(that._scheme == "*") { |
| 856 |
newSource._scheme = this._scheme; |
| 857 |
} else if(that._scheme == this._scheme) { |
| 858 |
newSource._scheme = this._scheme; |
| 859 |
} else { |
| 860 |
//CSPError("Could not intersect " + this + " with " + that |
| 861 |
// + " due to scheme problems."); |
| 862 |
return null; |
| 863 |
} |
| 864 |
|
| 865 |
//host |
| 866 |
if(this._host) |
| 867 |
newSource._host = this._host.intersectWith(that._host); |
| 868 |
|
| 869 |
return newSource._host ? newSource : null; |
| 870 |
}, |
| 871 |
|
| 872 |
/** |
| 873 |
* Compares one CSPSource to another. |
| 874 |
* |
| 875 |
* @param that |
| 876 |
* another CSPSource |
| 877 |
* @returns |
| 878 |
* true if they have the same data |
| 879 |
*/ |
| 880 |
equals: |
| 881 |
function(aSrc) { |
| 882 |
return this._scheme == aSrc._scheme |
| 883 |
&& this._port == aSrc._port |
| 884 |
&& this._host.equals(aSrc._host); |
| 885 |
} |
| 886 |
}; |
| 887 |
|
| 888 |
|
| 889 |
////////////////////////////////////////////////////////////////////// |
| 890 |
/** |
| 891 |
* Class to model a host *.x.y. |
| 892 |
*/ |
| 893 |
function CSPHost() { |
| 894 |
this._segments = []; |
| 895 |
} |
| 896 |
|
| 897 |
/** |
| 898 |
* Factory to create a new CSPHost, parsed from a string. |
| 899 |
* |
| 900 |
* @param aStr |
| 901 |
* string rep of a CSP Host |
| 902 |
* @returns |
| 903 |
* an instance of CSPHost |
| 904 |
*/ |
| 905 |
CSPHost.fromString = function(aStr) { |
| 906 |
var hObj = new CSPHost(); |
| 907 |
hObj._segments = aStr.split(/\./); |
| 908 |
if(hObj._segments.length < 1 || |
| 909 |
hObj._segments.length == 1 && hObj._segments[0] != "*" |
| 910 |
&& hObj._segments[0] != "localhost") { |
| 911 |
//only short hosts allowed are "*" and "localhost" |
| 912 |
return null; |
| 913 |
} |
| 914 |
//TODO: double-check that each segment is appropriate: |
| 915 |
// -- if there's a * it must be first |
| 916 |
// all segments are LDH (except *) |
| 917 |
return hObj; |
| 918 |
}; |
| 919 |
|
| 920 |
CSPHost.prototype = { |
| 921 |
/** |
| 922 |
* Generates string representation of the Source. |
| 923 |
* Should be fairly similar to the original. |
| 924 |
*/ |
| 925 |
toString: |
| 926 |
function() { |
| 927 |
return this._segments.join("."); |
| 928 |
}, |
| 929 |
|
| 930 |
/** |
| 931 |
* Makes a new instance that resembles this object. |
| 932 |
* @returns |
| 933 |
* a new CSPHost |
| 934 |
*/ |
| 935 |
clone: |
| 936 |
function() { |
| 937 |
var aHost = new CSPHost(); |
| 938 |
for(var i in this._segments) { |
| 939 |
aHost._segments[i] = this._segments[i]; |
| 940 |
} |
| 941 |
return aHost; |
| 942 |
}, |
| 943 |
|
| 944 |
/** |
| 945 |
* Returns true if this host accepts the provided host (or the other way |
| 946 |
* around). |
| 947 |
* @param aHost |
| 948 |
* the FQDN in question (CSPHost or String) |
| 949 |
* @returns |
| 950 |
*/ |
| 951 |
permits: |
| 952 |
function(aHost) { |
| 953 |
if(!(aHost instanceof CSPHost)) { |
| 954 |
// -- compare CSPHost to String |
| 955 |
return this.permits(CSPHost.fromString(aHost)); |
| 956 |
} |
| 957 |
var thislen = this._segments.length; |
| 958 |
var thatlen = aHost._segments.length; |
| 959 |
|
| 960 |
// don't accept a less specific host: |
| 961 |
// \--> *.b.a doesn't accept b.a. |
| 962 |
if(thatlen < thislen) { return false; } |
| 963 |
|
| 964 |
// check for more specific host (and wildcard): |
| 965 |
// \--> *.b.a accepts d.c.b.a. |
| 966 |
// \--> c.b.a doesn't accept d.c.b.a. |
| 967 |
if((thatlen > thislen) && this._segments[0] != "*") { |
| 968 |
return false; |
| 969 |
} |
| 970 |
|
| 971 |
// Given the wildcard condition (from above), |
| 972 |
// only necessary to compare elements that are present |
| 973 |
// in this host. Extra tokens in aHost are ok. |
| 974 |
// * Compare from right to left. |
| 975 |
for(var i=1; i <= thislen; i++) { |
| 976 |
if( this._segments[thislen-i] != "*" && |
| 977 |
(this._segments[thislen-i] != aHost._segments[thatlen-i]) ) { |
| 978 |
return false; |
| 979 |
} |
| 980 |
} |
| 981 |
|
| 982 |
//at this point, all conditions are met, so the host is allowed |
| 983 |
return true; |
| 984 |
}, |
| 985 |
|
| 986 |
/** |
| 987 |
* Determines the intersection of two Hosts. |
| 988 |
* Basically, they must be the same, or one must have a wildcard. |
| 989 |
* @param that |
| 990 |
* the other CSPHost to intersect "this" with |
| 991 |
* @returns |
| 992 |
* a new instance of a CSPHost representing the intersection |
| 993 |
* (or null, if they can't be intersected) |
| 994 |
*/ |
| 995 |
intersectWith: |
| 996 |
function(that) { |
| 997 |
if(!(this.permits(that) || that.permits(this))) { |
| 998 |
// host definitions cannot co-exist without a more general host |
| 999 |
// ... one must be a subset of the other, or intersection makes no sense. |
| 1000 |
return null; |
| 1001 |
} |
| 1002 |
|
| 1003 |
// pick the more specific one, if both are same length. |
| 1004 |
if(this._segments.length == that._segments.length) { |
| 1005 |
// *.a vs b.a : b.a |
| 1006 |
return (this._segments[0] == "*") ? that.clone() : this.clone(); |
| 1007 |
} |
| 1008 |
|
| 1009 |
//different lengths... |
| 1010 |
// *.b.a vs *.a : *.b.a |
| 1011 |
// *.b.a vs d.c.b.a : d.c.b.a |
| 1012 |
return (this._segments.length > that._segments.length) |
| 1013 |
? this.clone() : that.clone(); |
| 1014 |
}, |
| 1015 |
|
| 1016 |
/** |
| 1017 |
* Compares one CSPHost to another. |
| 1018 |
* |
| 1019 |
* @param that |
| 1020 |
* another CSPHost |
| 1021 |
* @returns |
| 1022 |
* true if they have the same data |
| 1023 |
*/ |
| 1024 |
equals: |
| 1025 |
function(that) { |
| 1026 |
if(this._segments.length != that._segments.length) { |
| 1027 |
return false; |
| 1028 |
} |
| 1029 |
for(var i=0; i<this._segments.length; i++) { |
| 1030 |
if(this._segments[i] != that._segments[i]) { |
| 1031 |
return false; |
| 1032 |
} |
| 1033 |
} |
| 1034 |
return true; |
| 1035 |
} |
| 1036 |
}; |