tor-browser

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

api.js (10787B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* global ExtensionCommon, ExtensionAPI, Glean, Services, XPCOMUtils, ExtensionUtils */
      8 
      9 /* eslint-disable no-console */
     10 
     11 var { ExtensionParent } = ChromeUtils.importESModule(
     12  "resource://gre/modules/ExtensionParent.sys.mjs"
     13 );
     14 
     15 const { ChannelWrapper } = Cu.getGlobalForObject(ExtensionParent);
     16 
     17 const lazy = {};
     18 
     19 ChromeUtils.defineESModuleGetters(lazy, {
     20  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
     21  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
     22 });
     23 
     24 const DATA_LEAK_BLOCKER_RS_COLLECTION = "addons-data-leak-blocker-domains";
     25 
     26 XPCOMUtils.defineLazyPreferenceGetter(
     27  lazy,
     28  "TEST_DOMAINS",
     29  "extensions.data-leak-blocker@mozilla.com.testDomains",
     30  "",
     31  null,
     32  value => {
     33    try {
     34      return new Set(
     35        value
     36          .split(",")
     37          // Trim whitespaces.
     38          .map(v => v.trim())
     39          // Omit any empty entries.
     40          .filter(el => el)
     41      );
     42    } catch {
     43      // Return an empty set if parsing the value fails.
     44      return new Set();
     45    }
     46  }
     47 );
     48 
     49 XPCOMUtils.defineLazyPreferenceGetter(
     50  lazy,
     51  "TESTING",
     52  "extensions.data-leak-blocker@mozilla.com.testing",
     53  false
     54 );
     55 
     56 XPCOMUtils.defineLazyPreferenceGetter(
     57  lazy,
     58  "GLEAN_SUBMIT_TASK_TIMEOUT",
     59  "extensions.data-leak-blocker@mozilla.com.gleanSubmitTaskTimeout",
     60  5000
     61 );
     62 
     63 this.dataAbuseDetection = class extends ExtensionAPI {
     64  deferredPingSubmitTask = null;
     65  domainsSet = null;
     66  fogMetricsInitialized = false;
     67  requestObserverRegistered = false;
     68  rsClient = null;
     69  rsSyncListener = null;
     70 
     71  get IsShuttingDown() {
     72    return (
     73      !this.extension ||
     74      this.extension.hasShutdown ||
     75      Services.startup.shuttingDown
     76    );
     77  }
     78 
     79  onStartup() {
     80    this.deferredPingSubmitTask = new lazy.DeferredTask(
     81      () => this.submitPing(),
     82      lazy.GLEAN_SUBMIT_TASK_TIMEOUT
     83    );
     84 
     85    ExtensionParent.browserStartupPromise
     86      .then(() => {
     87        if (this.IsShuttingDown) {
     88          console.log(
     89            "Data Leak Blocker initialization cancelled on detected extension or app shutdown"
     90          );
     91          return Promise.resolve();
     92        }
     93        this.registerFOGMetricsAndPings();
     94        this.rsClient = lazy.RemoteSettings(DATA_LEAK_BLOCKER_RS_COLLECTION);
     95        // Process existing RS entries.
     96        return this.onRemoteSettingsSync();
     97      })
     98      .then(() => {
     99        if (this.IsShuttingDown) {
    100          console.log(
    101            "Data Leak Blocker initialization cancelled on detected extension or app shutdown"
    102          );
    103          return;
    104        }
    105        Services.obs.addObserver(this, "http-on-modify-request");
    106        this.requestObserverRegistered = true;
    107        this.rsSyncListener = this.onRemoteSettingsSync.bind(this);
    108        this.rsClient.on("sync", this.rsSyncListener);
    109        // Submit any events that may be collected for this custom ping
    110        // in a previous session if it wasn't already sent.
    111        this.deferredPingSubmitTask.arm();
    112      });
    113  }
    114 
    115  onShutdown(isAppShutdown) {
    116    if (isAppShutdown) {
    117      return;
    118    }
    119 
    120    if (this.requestObserverRegistered) {
    121      Services.obs.removeObserver(this, "http-on-modify-request");
    122    }
    123 
    124    if (this.rsSyncListener) {
    125      this.rsClient?.off("sync", this.rsSyncListener);
    126      this.rsSyncListener = null;
    127    }
    128    this.rsClient = null;
    129 
    130    if (this.deferredPingSubmitTask) {
    131      this.deferredPingSubmitTask.finalize();
    132      this.deferredPingSubmitTask = null;
    133    }
    134  }
    135 
    136  async onRemoteSettingsSync() {
    137    const entries = await this.rsClient
    138      .get({ syncIfEmpty: false })
    139      .catch(err => {
    140        console.error(
    141          `Failure to process ${DATA_LEAK_BLOCKER_RS_COLLECTION} RemoteSettings`,
    142          err
    143        );
    144        return [];
    145      });
    146    const domains = new Set();
    147    for (const entry of entries) {
    148      if (!Array.isArray(entry.domains)) {
    149        if (lazy.TESTING) {
    150          console.debug(
    151            "Ignoring invalid RemoteSettings entry ('domains' property invalid or missing)",
    152            entry
    153          );
    154        }
    155        continue;
    156      }
    157      for (const domain of entry.domains) {
    158        if (!domain) {
    159          if (lazy.TESTING) {
    160            console.debug(
    161              `Ignoring unxpected empty domain in ${DATA_LEAK_BLOCKER_RS_COLLECTION} record`,
    162              entry
    163            );
    164          }
    165          continue;
    166        }
    167        domains.add(domain);
    168      }
    169    }
    170    this.domainsSet = domains;
    171    if (lazy.TESTING) {
    172      this.domainsSet = domains.union(lazy.TEST_DOMAINS);
    173      console.debug(
    174        "Data Leak Blocker DomainsSet updated",
    175        Array.from(this.domainsSet)
    176      );
    177    }
    178  }
    179 
    180  registerFOGMetricsAndPings() {
    181    // Register the custom ping to Glean (if not already registered).
    182    //
    183    // NOTE: this should be kept in sync with the ping as defined in the
    184    // pings.yaml.
    185    if (!("dataLeakBlocker" in GleanPings)) {
    186      const ping = {
    187        name: "data-leak-blocker",
    188        includeClientId: true,
    189        sendIfEmpty: false,
    190        preciseTimestamp: false,
    191        includeInfoSections: true,
    192        enabled: true,
    193        schedulesPings: [],
    194        reasonCodes: [],
    195        followsCollectionEnabled: true,
    196        uploaderCapabilities: [],
    197      };
    198      Services.fog.registerRuntimePing(
    199        ping.name,
    200        ping.includeClientId,
    201        ping.sendIfEmpty,
    202        ping.preciseTimestamp,
    203        ping.includeInfoSections,
    204        ping.enabled,
    205        ping.schedulesPings,
    206        ping.reasonCodes,
    207        ping.followsCollectionEnabled,
    208        ping.uploaderCapabilities
    209      );
    210    }
    211 
    212    // Register the custom metric to Glean (if not already registered).
    213    //
    214    // NOTE: this should be kept in sync with the metric as defined in the
    215    // metrics.yaml.
    216    if (!Glean.dataLeakBlocker?.reportV1) {
    217      const metric = {
    218        category: "data_leak_blocker",
    219        name: "report_v1",
    220        type: "event",
    221        lifetime: "ping",
    222        pings: ["data-leak-blocker"],
    223        disabled: false,
    224        extraArgs: {
    225          allowed_extra_keys: [
    226            "addon_id",
    227            "blocked",
    228            "content_policy_type",
    229            "is_addon_triggering",
    230            "is_addon_loading",
    231            "is_content_script",
    232            "method",
    233          ],
    234        },
    235      };
    236      Services.fog.registerRuntimeMetric(
    237        metric.type,
    238        metric.category,
    239        metric.name,
    240        metric.pings,
    241        `"${metric.lifetime}"`,
    242        metric.disabled,
    243        JSON.stringify(metric.extraArgs)
    244      );
    245    }
    246    this.fogMetricsInitialized = true;
    247  }
    248 
    249  submitPing() {
    250    // NOTE: optional chaining is used here because on artifacts builds the runtime-registered
    251    // glean ping defined by the registerFOGMetricsAndPings method would be unregistered
    252    // as a side-effect of the jogfile for the artifacts build metrics being loaded
    253    // (See Bug 1983674).
    254    GleanPings.dataLeakBlocker?.submit();
    255  }
    256 
    257  observe(subject, _topic, _data) {
    258    try {
    259      if (!this.domainsSet || !Glean.dataLeakBlocker?.reportV1) {
    260        return;
    261      }
    262      this.processHttpOnModifyRequest(
    263        subject.QueryInterface(Ci.nsIHttpChannel)
    264      );
    265    } catch (err) {
    266      if (lazy.TESTING) {
    267        console.error(
    268          "Unexpected error on processing http-on-modify-request notification",
    269          err
    270        );
    271      }
    272    }
    273  }
    274 
    275  processHttpOnModifyRequest(channel) {
    276    // Ignore internal favicon request triggered by FaviconLoader.sys.mjs.
    277    if (
    278      channel.loadInfo.internalContentPolicyType ===
    279      Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
    280    ) {
    281      return;
    282    }
    283 
    284    const { URI } = channel;
    285 
    286    if (!URI.schemeIs("http") && !URI.schemeIs("https")) {
    287      return;
    288    }
    289 
    290    const { triggeringPrincipal, loadingPrincipal, externalContentPolicyType } =
    291      channel.loadInfo;
    292 
    293    // Ignore requests that are not attributed to add-ons.
    294    if (
    295      !triggeringPrincipal.isAddonOrExpandedAddonPrincipal &&
    296      !loadingPrincipal?.isAddonOrExpandedAddonPrincipal
    297    ) {
    298      return;
    299    }
    300 
    301    if (!this.domainsSet.has(URI.host)) {
    302      return;
    303    }
    304 
    305    // numeric nsContentPolicyType enum value.
    306    let content_policy_type = externalContentPolicyType;
    307    let is_addon_loading = false;
    308    let is_addon_triggering = false;
    309    let is_content_script = false;
    310    let method = channel.requestMethod;
    311    let addonPolicy;
    312    if (triggeringPrincipal.isAddonOrExpandedAddonPrincipal) {
    313      is_addon_triggering = true;
    314      is_content_script = !!triggeringPrincipal.contentScriptAddonPolicy;
    315      addonPolicy =
    316        triggeringPrincipal.addonPolicy ??
    317        triggeringPrincipal.contentScriptAddonPolicy;
    318    } else if (loadingPrincipal?.isAddonOrExpandedAddonPrincipal) {
    319      // Look for an addon id on the loadingPrincipal as a fallback
    320      // (if it is defined, e.g. it is not defined for request triggered
    321      // when a user loads an url in a new tab).
    322      is_addon_loading = true;
    323      is_content_script = !!loadingPrincipal.contentScriptAddonPolicy;
    324      addonPolicy =
    325        loadingPrincipal.addonPolicy ??
    326        loadingPrincipal.contentScriptAddonPolicy;
    327    } else {
    328      // Bail out if we can't determine an addon id for the request.
    329      return;
    330    }
    331 
    332    if (lazy.TESTING) {
    333      console.debug("Detected request to suspicious domain", channel.name);
    334    }
    335 
    336    const channelWrapper = ChannelWrapper.get(channel);
    337    channelWrapper.cancel(
    338      Cr.NS_ERROR_ABORT,
    339      Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST
    340    );
    341    let properties = channel.QueryInterface(Ci.nsIWritablePropertyBag);
    342    properties.setProperty("cancelledByExtension", this.extension.id);
    343 
    344    const addon_id = addonPolicy?.id ?? "no-addon-id";
    345 
    346    // NOTE: optional chaining is used here because on artifacts builds the runtime-registered
    347    // glean ping defined by the registerFOGMetricsAndPings method would be unregistered
    348    // as a side-effect of the jogfile for the artifacts build metrics being loaded
    349    // (See Bug 1983674).
    350    Glean.dataLeakBlocker?.reportV1.record({
    351      addon_id,
    352      is_addon_triggering,
    353      is_addon_loading,
    354      is_content_script,
    355      method,
    356      content_policy_type,
    357      blocked: true,
    358    });
    359 
    360    this.deferredPingSubmitTask.arm();
    361 
    362    if (lazy.TESTING) {
    363      console.debug("Suspicious request details", {
    364        name: channel.name,
    365        addon_id,
    366        is_addon_triggering,
    367        is_addon_loading,
    368        is_content_script,
    369        method,
    370        content_policy_type,
    371        blocked: true,
    372      });
    373    }
    374  }
    375 };