IdpSandbox.sys.mjs (8272B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs"; 6 7 /** This little class ensures that redirects maintain an https:// origin */ 8 function RedirectHttpsOnly() {} 9 10 RedirectHttpsOnly.prototype = { 11 asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { 12 if (newChannel.URI.scheme !== "https") { 13 callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT); 14 } else { 15 callback.onRedirectVerifyCallback(Cr.NS_OK); 16 } 17 }, 18 19 getInterface(iid) { 20 return this.QueryInterface(iid); 21 }, 22 QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]), 23 }; 24 25 /** 26 * This class loads a resource into a single string. ResourceLoader.load() is 27 * the entry point. 28 */ 29 function ResourceLoader(res, rej) { 30 this.resolve = res; 31 this.reject = rej; 32 this.data = ""; 33 } 34 35 /** Loads the identified https:// URL. */ 36 ResourceLoader.load = function (uri, doc) { 37 return new Promise((resolve, reject) => { 38 let listener = new ResourceLoader(resolve, reject); 39 let ioChannel = NetUtil.newChannel({ 40 uri, 41 loadingNode: doc, 42 securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, 43 contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT, 44 }); 45 46 ioChannel.loadGroup = doc.documentLoadGroup.QueryInterface(Ci.nsILoadGroup); 47 ioChannel.notificationCallbacks = new RedirectHttpsOnly(); 48 ioChannel.asyncOpen(listener); 49 }); 50 }; 51 52 ResourceLoader.prototype = { 53 onDataAvailable(request, input, offset, count) { 54 let stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( 55 Ci.nsIScriptableInputStream 56 ); 57 stream.init(input); 58 this.data += stream.read(count); 59 }, 60 61 onStartRequest() {}, 62 63 onStopRequest(request, status) { 64 if (Components.isSuccessCode(status)) { 65 var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus; 66 if (statusCode === 200) { 67 this.resolve({ request, data: this.data }); 68 } else { 69 this.reject(new Error("Non-200 response from server: " + statusCode)); 70 } 71 } else { 72 this.reject(new Error("Load failed: " + status)); 73 } 74 }, 75 76 getInterface(iid) { 77 return this.QueryInterface(iid); 78 }, 79 QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), 80 }; 81 82 /** 83 * A simple implementation of the WorkerLocation interface. 84 */ 85 function createLocationFromURI(uri) { 86 return { 87 href: uri.spec, 88 protocol: uri.scheme + ":", 89 host: uri.host + (uri.port >= 0 ? ":" + uri.port : ""), 90 port: uri.port, 91 hostname: uri.host, 92 pathname: uri.pathQueryRef.replace(/[#\?].*/, ""), 93 search: uri.pathQueryRef.replace(/^[^\?]*/, "").replace(/#.*/, ""), 94 hash: uri.hasRef ? "#" + uri.ref : "", 95 origin: uri.prePath, 96 toString() { 97 return uri.spec; 98 }, 99 }; 100 } 101 102 /** 103 * A javascript sandbox for running an IdP. 104 * 105 * @param domain (string) the domain of the IdP 106 * @param protocol (string?) the protocol of the IdP [default: 'default'] 107 * @param win (obj) the current window 108 * @throws if the domain or protocol aren't valid 109 */ 110 export function IdpSandbox(domain, protocol, win) { 111 this.source = IdpSandbox.createIdpUri(domain, protocol || "default"); 112 this.active = null; 113 this.sandbox = null; 114 this.window = win; 115 } 116 117 IdpSandbox.checkDomain = function (domain) { 118 if (!domain || typeof domain !== "string") { 119 throw new Error( 120 "Invalid domain for identity provider: " + 121 "must be a non-zero length string" 122 ); 123 } 124 }; 125 126 /** 127 * Checks that the IdP protocol is superficially sane. In particular, we don't 128 * want someone adding relative paths (e.g., '../../myuri'), which could be used 129 * to move outside of /.well-known/ and into space that they control. 130 */ 131 IdpSandbox.checkProtocol = function (protocol) { 132 let message = "Invalid protocol for identity provider: "; 133 if (!protocol || typeof protocol !== "string") { 134 throw new Error(message + "must be a non-zero length string"); 135 } 136 if (decodeURIComponent(protocol).match(/[\/\\]/)) { 137 throw new Error(message + "must not include '/' or '\\'"); 138 } 139 }; 140 141 /** 142 * Turns a domain and protocol into a URI. This does some aggressive checking 143 * to make sure that we aren't being fooled somehow. Throws on fooling. 144 */ 145 IdpSandbox.createIdpUri = function (domain, protocol) { 146 IdpSandbox.checkDomain(domain); 147 IdpSandbox.checkProtocol(protocol); 148 149 let message = "Invalid IdP parameters: "; 150 try { 151 let wkIdp = "https://" + domain + "/.well-known/idp-proxy/" + protocol; 152 let uri = Services.io.newURI(wkIdp); 153 154 if (uri.hostPort !== domain) { 155 throw new Error(message + "domain is invalid"); 156 } 157 if (uri.pathQueryRef.indexOf("/.well-known/idp-proxy/") !== 0) { 158 throw new Error(message + "must produce a /.well-known/idp-proxy/ URI"); 159 } 160 161 return uri; 162 } catch (e) { 163 if ( 164 typeof e.result !== "undefined" && 165 e.result === Cr.NS_ERROR_MALFORMED_URI 166 ) { 167 throw new Error(message + "must produce a valid URI"); 168 } 169 throw e; 170 } 171 }; 172 173 IdpSandbox.prototype = { 174 isSame(domain, protocol) { 175 return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec; 176 }, 177 178 start() { 179 if (!this.active) { 180 this.active = ResourceLoader.load(this.source, this.window.document).then( 181 result => this._createSandbox(result) 182 ); 183 } 184 return this.active; 185 }, 186 187 // Provides the sandbox with some useful facilities. Initially, this is only 188 // a minimal set; it is far easier to add more as the need arises, than to 189 // take them back if we discover a mistake. 190 _populateSandbox(uri) { 191 this.sandbox.location = Cu.cloneInto( 192 createLocationFromURI(uri), 193 this.sandbox, 194 { cloneFunctions: true } 195 ); 196 }, 197 198 _createSandbox(result) { 199 let principal = Services.scriptSecurityManager.getChannelResultPrincipal( 200 result.request 201 ); 202 203 this.sandbox = Cu.Sandbox(principal, { 204 sandboxName: "IdP-" + this.source.host, 205 wantComponents: false, 206 wantExportHelpers: false, 207 wantGlobalProperties: [ 208 "indexedDB", 209 "XMLHttpRequest", 210 "TextEncoder", 211 "TextDecoder", 212 "URL", 213 "URLSearchParams", 214 "atob", 215 "btoa", 216 "Blob", 217 "crypto", 218 "rtcIdentityProvider", 219 "fetch", 220 ], 221 }); 222 let registrar = this.sandbox.rtcIdentityProvider; 223 if (!Cu.isXrayWrapper(registrar)) { 224 throw new Error("IdP setup failed"); 225 } 226 227 // have to use the ultimate URI, not the starting one to avoid 228 // that origin stealing from the one that redirected to it 229 this._populateSandbox(result.request.URI); 230 try { 231 Cu.evalInSandbox( 232 result.data, 233 this.sandbox, 234 "latest", 235 result.request.URI.spec, 236 1 237 ); 238 } catch (e) { 239 // These can be passed straight on, because they are explicitly labelled 240 // as being IdP errors by the IdP and we drop line numbers as a result. 241 if (e.name === "IdpError" || e.name === "IdpLoginError") { 242 throw e; 243 } 244 this._logError(e); 245 throw new Error("Error in IdP, check console for details"); 246 } 247 248 if (!registrar.hasIdp) { 249 throw new Error("IdP failed to call rtcIdentityProvider.register()"); 250 } 251 return registrar; 252 }, 253 254 // Capture all the details from the error and log them to the console. This 255 // can't rethrow anything else because that could leak information about the 256 // internal workings of the IdP across origins. 257 _logError(e) { 258 let winID = this.window.windowGlobalChild.innerWindowId; 259 let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( 260 Ci.nsIScriptError 261 ); 262 scriptError.initWithWindowID( 263 e.message, 264 e.fileName, 265 e.lineNumber, 266 e.columnNumber, 267 Ci.nsIScriptError.errorFlag, 268 "content javascript", 269 winID 270 ); 271 Services.console.logMessage(scriptError); 272 }, 273 274 stop() { 275 if (this.sandbox) { 276 Cu.nukeSandbox(this.sandbox); 277 } 278 this.sandbox = null; 279 this.active = null; 280 }, 281 282 toString() { 283 return this.source.spec; 284 }, 285 };