tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };