Skip to content
Snippets Groups Projects
SecurityLevel.sys.mjs 15.14 KiB
const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
});

const logger = console.createInstance({
  maxLogLevel: "Info",
  prefix: "SecurityLevel",
});

const BrowserTopics = Object.freeze({
  ProfileAfterChange: "profile-after-change",
});

// The Security Settings prefs in question.
const kSliderPref = "browser.security_level.security_slider";
const kCustomPref = "browser.security_level.security_custom";

// __getPrefValue(prefName)__
// Returns the current value of a preference, regardless of its type.
var getPrefValue = function (prefName) {
  switch (Services.prefs.getPrefType(prefName)) {
    case Services.prefs.PREF_BOOL:
      return Services.prefs.getBoolPref(prefName);
    case Services.prefs.PREF_INT:
      return Services.prefs.getIntPref(prefName);
    case Services.prefs.PREF_STRING:
      return Services.prefs.getCharPref(prefName);
    default:
      return null;
  }
};

// __bindPref(prefName, prefHandler, init)__
// Applies prefHandler whenever the value of the pref changes.
// If init is true, applies prefHandler to the current value.
// Returns a zero-arg function that unbinds the pref.
var bindPref = function (prefName, prefHandler, init = false) {
  let update = () => {
      prefHandler(getPrefValue(prefName));
    },
    observer = {
      observe(subject, topic, data) {
        if (data === prefName) {
          update();
        }
      },
    };
  Services.prefs.addObserver(prefName, observer);
  if (init) {
    update();
  }
  return () => {
    Services.prefs.removeObserver(prefName, observer);
  };
};

// __bindPrefAndInit(prefName, prefHandler)__
// Applies prefHandler to the current value of pref specified by prefName.
// Re-applies prefHandler whenever the value of the pref changes.
// Returns a zero-arg function that unbinds the pref.
var bindPrefAndInit = (prefName, prefHandler) =>
  bindPref(prefName, prefHandler, true);

async function waitForExtensionMessage(extensionId, checker = () => {}) {
  const { torWaitForExtensionMessage } = lazy.ExtensionParent;
  if (torWaitForExtensionMessage) {
    return torWaitForExtensionMessage(extensionId, checker);
  }
  return undefined;
}

async function sendExtensionMessage(extensionId, message) {
  const { torSendExtensionMessage } = lazy.ExtensionParent;
  if (torSendExtensionMessage) {
    return torSendExtensionMessage(extensionId, message);
  }
  return undefined;
}

// ## NoScript settings

// Minimum and maximum capability states as controlled by NoScript.
const max_caps = [
  "fetch",
  "font",
  "frame",
  "media",
  "object",
  "other",
  "script",
  "webgl",
  "noscript",
];
const min_caps = ["frame", "other", "noscript"];

// Untrusted capabilities for [Standard, Safer, Safest] safety levels.
const untrusted_caps = [
  max_caps, // standard safety: neither http nor https
  ["frame", "font", "object", "other", "noscript"], // safer: http
  min_caps, // safest: neither http nor https
];

// Default capabilities for [Standard, Safer, Safest] safety levels.
const default_caps = [
  max_caps, // standard: both http and https
  ["fetch", "font", "frame", "object", "other", "script", "noscript"], // safer: https only
  min_caps, // safest: both http and https
];

// __noscriptSettings(safetyLevel)__.
// Produces NoScript settings with policy according to
// the safetyLevel which can be:
// 0 = Standard, 1 = Safer, 2 = Safest
//
// At the "Standard" safety level, we leave all sites at
// default with maximal capabilities. Essentially no content
// is blocked.
//
// At "Safer", we set all http sites to untrusted,
// and all https sites to default. Scripts are only permitted
// on https sites. Neither type of site is supposed to allow
// media, but both allow fonts (as we used in legacy NoScript).
//
// At "Safest", all sites are at default with minimal
// capabilities. Most things are blocked.
let noscriptSettings = safetyLevel => ({
  __meta: {
    name: "updateSettings",
    recipientInfo: null,
  },
  policy: {
    DEFAULT: {
      capabilities: default_caps[safetyLevel],
      temp: false,
    },
    TRUSTED: {
      capabilities: max_caps,
      temp: false,
    },
    UNTRUSTED: {
      capabilities: untrusted_caps[safetyLevel],
      temp: false,
    },
    sites: {
      trusted: [],
      untrusted: [[], ["http:"], []][safetyLevel],
      custom: {},
      temp: [],
    },
    enforced: true,
    autoAllowTop: false,
  },
  isTorBrowser: true,
  tabId: -1,
});

// ## Communications

// The extension ID for NoScript (WebExtension)
const noscriptID = "{73a6fe31-595d-460b-a920-fcc0f8843232}";

// Ensure binding only occurs once.
let initialized = false;

// __initialize()__.
// The main function that binds the NoScript settings to the security
// slider pref state.
var initializeNoScriptControl = () => {
  if (initialized) {
    return;
  }
  initialized = true;

  try {
    // LegacyExtensionContext is not there anymore. Using raw
    // Services.cpmm.sendAsyncMessage mechanism to communicate with
    // NoScript.

    // The component that handles WebExtensions' sendMessage.

    // __setNoScriptSettings(settings)__.
    // NoScript listens for internal settings with onMessage. We can send
    // a new settings JSON object according to NoScript's
    // protocol and these are accepted! See the use of
    // `browser.runtime.onMessage.addListener(...)` in NoScript's bg/main.js.

    // TODO: Is there a better way?
    let sendNoScriptSettings = settings =>
      sendExtensionMessage(noscriptID, settings);

    // __setNoScriptSafetyLevel(safetyLevel)__.
    // Set NoScript settings according to a particular safety level
    // (security slider level): 0 = Standard, 1 = Safer, 2 = Safest
    let setNoScriptSafetyLevel = safetyLevel =>
      sendNoScriptSettings(noscriptSettings(safetyLevel));

    // __securitySliderToSafetyLevel(sliderState)__.
    // Converts the "browser.security_level.security_slider" pref value
    // to a "safety level" value: 0 = Standard, 1 = Safer, 2 = Safest
    let securitySliderToSafetyLevel = sliderState =>
      [undefined, 2, 1, 1, 0][sliderState];

    // Wait for the first message from NoScript to arrive, and then
    // bind the security_slider pref to the NoScript settings.
    let messageListener = a => {
      try {
        logger.debug("Message received from NoScript:", a);
        let noscriptPersist = Services.prefs.getBoolPref(
          "browser.security_level.noscript_persist",
          false
        );
        let noscriptInited = Services.prefs.getBoolPref(
          "browser.security_level.noscript_inited",
          false
        );
        // Set the noscript safety level once if we have never run noscript
        // before, or if we are not allowing noscript per-site settings to be
        // persisted between browser sessions. Otherwise make sure that the
        // security slider position, if changed, will rewrite the noscript
        // settings.
        bindPref(
          kSliderPref,
          sliderState =>
            setNoScriptSafetyLevel(securitySliderToSafetyLevel(sliderState)),
          !noscriptPersist || !noscriptInited
        );
        if (!noscriptInited) {
          Services.prefs.setBoolPref(
            "browser.security_level.noscript_inited",
            true
          );
        }
      } catch (e) {
        logger.exception(e);
      }
    };
    waitForExtensionMessage(noscriptID, a => a.__meta.name === "started").then(
      messageListener
    );
    logger.info("Listening for messages from NoScript.");
  } catch (e) {
    logger.exception(e);
  }
};

// ### Constants

// __kSecuritySettings__.
// A table of all prefs bound to the security slider, and the value
// for each security setting. Note that 2-m and 3-m are identical,
// corresponding to the old 2-medium-high setting. We also separately
// bind NoScript settings to the browser.security_level.security_slider
// (see noscript-control.js).
/* eslint-disable */
// prettier-ignore
const kSecuritySettings = {
  // Preference name :                                          [0, 1-high 2-m    3-m    4-low]
  "javascript.options.ion" :                                    [,  false, false, false, true ],
  "javascript.options.baselinejit" :                            [,  false, false, false, true ],
  "javascript.options.native_regexp" :                          [,  false, false, false, true ],
  "mathml.disabled" :                                           [,  true,  true,  true,  false],
  "gfx.font_rendering.graphite.enabled" :                       [,  false, false, false, true ],
  "gfx.font_rendering.opentype_svg.enabled" :                   [,  false, false, false, true ],
  "svg.disabled" :                                              [,  true,  false, false, false],
  "javascript.options.asmjs" :                                  [,  false, false, false, true ],
  "javascript.options.wasm" :                                   [,  false, false, false, true ],
  "dom.security.https_only_mode_send_http_background_request" : [,  false, false, false, true ],
};
/* eslint-enable */

// ### Prefs

// __write_setting_to_prefs(settingIndex)__.
// Take a given setting index and write the appropriate pref values
// to the pref database.
var write_setting_to_prefs = function (settingIndex) {
  Object.keys(kSecuritySettings).forEach(prefName =>
    Services.prefs.setBoolPref(
      prefName,
      kSecuritySettings[prefName][settingIndex]
    )
  );
};

// __read_setting_from_prefs()__.
// Read the current pref values, and decide if any of our
// security settings matches. Otherwise return null.
var read_setting_from_prefs = function (prefNames) {
  prefNames = prefNames || Object.keys(kSecuritySettings);
  for (let settingIndex of [1, 2, 3, 4]) {
    let possibleSetting = true;
    // For the given settingIndex, check if all current pref values
    // match the setting.
    for (let prefName of prefNames) {
      if (
        kSecuritySettings[prefName][settingIndex] !==
        Services.prefs.getBoolPref(prefName)
      ) {
        possibleSetting = false;
      }
    }
    if (possibleSetting) {
      // We have a match!
      return settingIndex;
    }
  }
  // No matching setting; return null.
  return null;
};

// __watch_security_prefs(onSettingChanged)__.
// Whenever a pref bound to the security slider changes, onSettingChanged
// is called with the new security setting value (1,2,3,4 or null).
// Returns a zero-arg function that ends this binding.
var watch_security_prefs = function (onSettingChanged) {
  let prefNames = Object.keys(kSecuritySettings);
  let unbindFuncs = [];
  for (let prefName of prefNames) {
    unbindFuncs.push(
      bindPrefAndInit(prefName, () =>
        onSettingChanged(read_setting_from_prefs())
      )
    );
  }
  // Call all the unbind functions.
  return () => unbindFuncs.forEach(unbind => unbind());
};

// __initialized__.
// Have we called initialize() yet?
var initializedSecPrefs = false;

// __initialize()__.
// Defines the behavior of "browser.security_level.security_custom",
// "browser.security_level.security_slider", and the security-sensitive
// prefs declared in kSecuritySettings.
var initializeSecurityPrefs = function () {
  // Only run once.
  if (initializedSecPrefs) {
    return;
  }
  logger.info("Initializing security-prefs.js");
  initializedSecPrefs = true;
  // When security_custom is set to false, apply security_slider setting
  // to the security-sensitive prefs.
  bindPrefAndInit(kCustomPref, function (custom) {
    if (custom === false) {
      write_setting_to_prefs(Services.prefs.getIntPref(kSliderPref));
    }
  });
  // If security_slider is given a new value, then security_custom should
  // be set to false.
  bindPref(kSliderPref, function (prefIndex) {
    Services.prefs.setBoolPref(kCustomPref, false);
    write_setting_to_prefs(prefIndex);
  });
  // If a security-sensitive pref changes, then decide if the set of pref values
  // constitutes a security_slider setting or a custom value.
  watch_security_prefs(settingIndex => {
    if (settingIndex === null) {
      Services.prefs.setBoolPref(kCustomPref, true);
    } else {
      Services.prefs.setIntPref(kSliderPref, settingIndex);
      Services.prefs.setBoolPref(kCustomPref, false);
    }
  });
  // Migrate from old medium-low (3) to new medium (2).
  if (
    Services.prefs.getBoolPref(kCustomPref) === false &&
    Services.prefs.getIntPref(kSliderPref) === 3
  ) {
    Services.prefs.setIntPref(kSliderPref, 2);
    write_setting_to_prefs(2);
  }

  logger.info("security-prefs.js initialization complete");
};

// tor-browser#41460: we changed preference names in 12.0.
// 11.5.8 is an obligated step for desktop users, so this code is helpful only
// to alpha users, and we could remove it quite soon.
function migratePreferences() {
  const kPrefCheck = "extensions.torbutton.noscript_inited";
  // For 12.0, check for extensions.torbutton.noscript_inited, which was set
  // as a user preference for sure, if someone used security level in previous
  // versions.
  if (!Services.prefs.prefHasUserValue(kPrefCheck)) {
    return;
  }
  const migrate = (oldName, newName, getter, setter) => {
    oldName = `extensions.torbutton.${oldName}`;
    newName = `browser.${newName}`;
    if (Services.prefs.prefHasUserValue(oldName)) {
      setter(newName, getter(oldName));
      Services.prefs.clearUserPref(oldName);
    }
  };
  const prefs = {
    security_custom: "security_level.security_custom",
    noscript_persist: "security_level.noscript_persist",
    noscript_inited: "security_level.noscript_inited",
  };
  for (const [oldName, newName] of Object.entries(prefs)) {
    migrate(
      oldName,
      newName,
      Services.prefs.getBoolPref.bind(Services.prefs),
      Services.prefs.setBoolPref.bind(Services.prefs)
    );
  }
  migrate(
    "security_slider",
    "security_level.security_slider",
    Services.prefs.getIntPref.bind(Services.prefs),
    Services.prefs.setIntPref.bind(Services.prefs)
  );
}
/**
 * This class is used to initialize the security level stuff at the startup
 */
export class SecurityLevel {
  QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);

  init() {
    migratePreferences();
    initializeNoScriptControl();
    initializeSecurityPrefs();
  }

  observe(aSubject, aTopic) {
    if (aTopic === BrowserTopics.ProfileAfterChange) {
      this.init();
    }
  }
}

/*
  Security Level Prefs

  Getters and Setters for relevant torbutton prefs
*/
export const SecurityLevelPrefs = {
  SecurityLevels: Object.freeze({
    safest: 1,
    safer: 2,
    standard: 4,
  }),
  security_slider_pref: "browser.security_level.security_slider",
  security_custom_pref: "browser.security_level.security_custom",

  get securityLevel() {
    // Set the default return value to 0, which won't match anything in
    // SecurityLevels.
    const val = Services.prefs.getIntPref(this.security_slider_pref, 0);
    return Object.entries(this.SecurityLevels).find(
      entry => entry[1] === val
    )?.[0];
  },

  set securityLevel(level) {
    const val = this.SecurityLevels[level];
    if (val !== undefined) {
      Services.prefs.setIntPref(this.security_slider_pref, val);
    }
  },

  get securityCustom() {
    return Services.prefs.getBoolPref(this.security_custom_pref);
  },

  set securityCustom(val) {
    Services.prefs.setBoolPref(this.security_custom_pref, val);
  },
}; /* Security Level Prefs */