Bug 1734987 - Part 3: Add isPrivileged to ExtensionData + test r=rpl

See https://bugzilla.mozilla.org/show_bug.cgi?id=1734987#c0 for the
description of the why and how of this patch.

Since isPrivileged is now supported on ExtensionData, this patch also
removes the work-arounds from bug 1675858.

Differential Revision: https://phabricator.services.mozilla.com/D128232
This commit is contained in:
Rob Wu 2022-02-27 13:23:57 +00:00
Родитель fdcd5ac4ad
Коммит c02964f87e
7 изменённых файлов: 381 добавлений и 90 удалений

Просмотреть файл

@ -530,9 +530,10 @@ const manifestTypes = new Map([
* `loadManifest` has been called, and completed.
*/
class ExtensionData {
constructor(rootURI) {
constructor(rootURI, isPrivileged = false) {
this.rootURI = rootURI;
this.resourceURL = rootURI.spec;
this.isPrivileged = isPrivileged;
this.manifest = null;
this.type = null;
@ -553,6 +554,38 @@ class ExtensionData {
this.eventPagesEnabled = eventPagesEnabled;
}
/**
* A factory function that allows the construction of ExtensionData, with
* the isPrivileged flag computed asynchronously.
*
* @param {nsIURI} rootURI
* The URI pointing to the extension root.
* @param {function(type, id)} checkPrivileged
* An (async) function that takes the addon type and addon ID and returns
* whether the given add-on is privileged.
* @returns {ExtensionData}
*/
static async constructAsync({ rootURI, checkPrivileged }) {
let extension = new ExtensionData(rootURI);
// checkPrivileged depends on the extension type and id.
await extension.initializeAddonTypeAndID();
let { type, id } = extension;
// Map the extension type to the type name used by the add-on manager.
// TODO bug 1757084: Remove this.
type = type == "langpack" ? "locale" : type;
extension.isPrivileged = await checkPrivileged(type, id);
return extension;
}
static getIsPrivileged({ signedState, builtIn, temporarilyInstalled }) {
return (
signedState === AddonManager.SIGNEDSTATE_PRIVILEGED ||
signedState === AddonManager.SIGNEDSTATE_SYSTEM ||
builtIn ||
(AddonSettings.EXPERIMENTS_ENABLED && temporarilyInstalled)
);
}
get builtinMessages() {
return null;
}
@ -725,25 +758,8 @@ class ExtensionData {
});
}
canCheckSignature() {
// ExtensionData instances can't check the signature because it is not yet
// available when XPIProvider does use it to load the extension manifest.
//
// This method will return true for the ExtensionData subclasses (like
// the Extension class) to enable the additional validation that would require
// the signature to be available (e.g. to check if the extension is allowed to
// use a privileged permission).
return this.constructor != ExtensionData;
}
get restrictSchemes() {
// mozillaAddons permission is only allowed for privileged addons and
// filtered out if the extension isn't privileged.
// When the manifest is loaded by an explicit ExtensionData class
// instance, the signature data isn't available yet and this helper
// would always return false, but it will return true when appropriate
// (based on the isPrivileged boolean property) for the Extension class.
return !this.hasPermission("mozillaAddons");
return !(this.isPrivileged && this.hasPermission("mozillaAddons"));
}
/**
@ -1027,15 +1043,48 @@ class ExtensionData {
return Schemas.normalize(this.rawManifest, manifestType, context);
}
async initializeAddonTypeAndID() {
if (this.type) {
// Already initialized.
return;
}
this.rawManifest = await this.readJSON("manifest.json");
let manifest = this.rawManifest;
if (manifest.theme) {
this.type = "theme";
} else if (manifest.langpack_id) {
// TODO bug 1757084: This should be "locale".
this.type = "langpack";
} else if (manifest.dictionaries) {
this.type = "dictionary";
} else if (manifest.site_permissions) {
this.type = "sitepermission";
} else {
this.type = "extension";
}
if (!this.id) {
let bss =
manifest.browser_specific_settings?.gecko ||
manifest.applications?.gecko;
let id = bss?.id;
// This is a basic type check.
// When parseManifest is called, the ID is validated more thoroughly
// because the id is defined to be an ExtensionID type in
// toolkit/components/extensions/schemas/manifest.json
if (typeof id == "string") {
this.id = id;
}
}
}
// eslint-disable-next-line complexity
async parseManifest() {
let [manifest] = await Promise.all([
this.readJSON("manifest.json"),
Management.lazyInit(),
]);
await Promise.all([this.initializeAddonTypeAndID(), Management.lazyInit()]);
let manifest = this.rawManifest;
this.manifest = manifest;
this.rawManifest = manifest;
if (manifest.default_locale) {
await this.initLocale();
@ -1054,18 +1103,6 @@ class ExtensionData {
}
}
if (this.manifest.theme) {
this.type = "theme";
} else if (this.manifest.langpack_id) {
this.type = "langpack";
} else if (this.manifest.dictionaries) {
this.type = "dictionary";
} else if (this.manifest.site_permissions) {
this.type = "sitepermission";
} else {
this.type = "extension";
}
let normalized = await this._getNormalizedManifest();
if (normalized.error) {
this.manifestError(normalized.error);
@ -1096,8 +1133,6 @@ class ExtensionData {
this.logWarning("Event pages are not currently supported.");
}
this.id ??= manifest.applications?.gecko?.id;
let apiNames = new Set();
let dependencies = new Set();
let originPermissions = new Set();
@ -1106,6 +1141,9 @@ class ExtensionData {
let schemaPromises = new Map();
// Note: this.id and this.type were computed in initializeAddonTypeAndID.
// The format of `this.id` was confirmed to be a valid extensionID by the
// Schema validation as part of the _getNormalizedManifest() call.
let result = {
apiNames,
dependencies,
@ -1148,18 +1186,6 @@ class ExtensionData {
} else if (type.api) {
apiNames.add(type.api);
} else if (type.invalid) {
if (!this.canCheckSignature() && PRIVILEGED_PERMS.has(perm)) {
// Do not emit the warning if the invalid permission is a privileged one
// and the current instance can't yet check for a valid signature
// (see Bug 1675858 and the inline comment inside the canCheckSignature
// method for more details).
//
// This parseManifest method will be called again on the Extension class
// instance, which will have the signature available and the invalid
// extension permission warnings will be collected and logged if necessary.
continue;
}
this.manifestWarning(`Invalid extension permission: ${perm}`);
continue;
}
@ -2101,7 +2127,7 @@ let pendingExtensions = new Map();
*/
class Extension extends ExtensionData {
constructor(addonData, startupReason) {
super(addonData.resourceURI);
super(addonData.resourceURI, addonData.isPrivileged);
this.startupStates = new Set();
this.state = "Not started";
@ -2345,15 +2371,6 @@ class Extension extends ExtensionData {
return [this.id, this.version, Services.locale.appLocaleAsBCP47];
}
get isPrivileged() {
return (
this.addonData.signedState === AddonManager.SIGNEDSTATE_PRIVILEGED ||
this.addonData.signedState === AddonManager.SIGNEDSTATE_SYSTEM ||
this.addonData.builtIn ||
(AddonSettings.EXPERIMENTS_ENABLED && this.temporarilyInstalled)
);
}
get temporarilyInstalled() {
return !!this.addonData.temporarilyInstalled;
}

Просмотреть файл

@ -36,6 +36,11 @@ ChromeUtils.defineModuleGetter(
"Extension",
"resource://gre/modules/Extension.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ExtensionData",
"resource://gre/modules/Extension.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ExtensionParent",
@ -575,6 +580,12 @@ ExtensionTestCommon = class ExtensionTestCommon {
signedState = AddonManager.SIGNEDSTATE_SYSTEM;
}
let isPrivileged = ExtensionData.getIsPrivileged({
signedState,
builtIn: false,
temporarilyInstalled: !!data.temporarilyInstalled,
});
return new Extension(
{
id,
@ -583,6 +594,7 @@ ExtensionTestCommon = class ExtensionTestCommon {
signedState,
incognitoOverride: data.incognitoOverride,
temporarilyInstalled: !!data.temporarilyInstalled,
isPrivileged,
TEST_NO_ADDON_MANAGER: true,
// By default we set TEST_NO_DELAYED_STARTUP to true
TEST_NO_DELAYED_STARTUP: !data.delayedStartup,

Просмотреть файл

@ -668,26 +668,28 @@ add_task(async function update_unprivileged_with_mozillaAddons() {
});
// Tests that invalid permission warning for privileged permissions requested
// without the privilged signature are emitted by the Extension class instance
// but not for the ExtensionData instances (on which the signature is not
// available and the warning would be emitted even for the ones signed correctly).
// are not emitted for privileged extensions, only for unprivileged extensions.
add_task(
async function test_invalid_permission_warning_on_privileged_permission() {
await AddonTestUtils.promiseStartupManager();
const MANIFEST_WARNINGS = [
"Reading manifest: Invalid extension permission: mozillaAddons",
"Reading manifest: Invalid extension permission: resource://x/",
"Reading manifest: Invalid extension permission: about:reader*",
];
async function testInvalidPermissionWarning({ isPrivileged }) {
let id = isPrivileged
? "privileged-addon@mochi.test"
: "nonprivileged-addon@mochi.test";
let expectedWarnings = isPrivileged
? []
: ["Reading manifest: Invalid extension permission: mozillaAddons"];
let expectedWarnings = isPrivileged ? [] : MANIFEST_WARNINGS;
const ext = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
permissions: ["mozillaAddons"],
permissions: ["mozillaAddons", "resource://x/", "about:reader*"],
applications: { gecko: { id } },
},
background() {},
@ -711,28 +713,67 @@ add_task(
// ExtensionData instance created below).
let generatedExt = ExtensionTestCommon.generate({
manifest: {
permissions: ["mozillaAddons"],
permissions: ["mozillaAddons", "resource://x/", "about:reader*"],
applications: { gecko: { id: "extension-data@mochi.test" } },
},
});
// Verify that XPIInstall.jsm will not collect the warning for the
// privileged permission as expected.
const extData = new ExtensionData(generatedExt.rootURI);
await extData.loadManifest();
async function getWarningsFromExtensionData({ isPrivileged }) {
let extData;
if (typeof isPrivileged == "function") {
// isPrivileged expected to be computed asynchronously.
extData = await ExtensionData.constructAsync({
rootURI: generatedExt.rootURI,
checkPrivileged: isPrivileged,
});
} else {
extData = new ExtensionData(generatedExt.rootURI, isPrivileged);
}
await extData.loadManifest();
// This assertion is just meant to prevent the test to pass if there were
// no warnings because some errors prevented the warnings to be
// collected).
Assert.deepEqual(
extData.errors,
[],
"No errors collected by the ExtensionData instance"
);
return extData.warnings;
}
Assert.deepEqual(
extData.warnings,
[],
"No warnings for mozillaAddons permission collected for the ExtensionData instance"
await getWarningsFromExtensionData({ isPrivileged: undefined }),
MANIFEST_WARNINGS,
"Got warnings about privileged permissions by default"
);
// This assertion is just meant to prevent the test to pass if there were no warnings
// because some errors prevented the warnings to be collected).
Assert.deepEqual(
extData.errors,
[],
"No errors collected by the ExtensionData instance"
await getWarningsFromExtensionData({ isPrivileged: false }),
MANIFEST_WARNINGS,
"Got warnings about privileged permissions for non-privileged extensions"
);
Assert.deepEqual(
await getWarningsFromExtensionData({ isPrivileged: true }),
[],
"No warnings about privileged permissions on privileged extensions"
);
Assert.deepEqual(
await getWarningsFromExtensionData({ isPrivileged: async () => false }),
MANIFEST_WARNINGS,
"Got warnings about privileged permissions for non-privileged extensions (async)"
);
Assert.deepEqual(
await getWarningsFromExtensionData({ isPrivileged: async () => true }),
[],
"No warnings about privileged permissions on privileged extensions (async)"
);
// Cleanup the generated xpi file.
await generatedExt.cleanupGeneratedFile();

Просмотреть файл

@ -441,14 +441,30 @@ function waitForAllPromises(promises) {
*
* @param {Package} aPackage
* The install package for the add-on
* @returns {AddonInternal}
* @param {XPIStateLocation} aLocation
* The install location the add-on is installed in, or will be
* installed to.
* @returns {{ addon: AddonInternal, verifiedSignedState: object}}
* @throws if the install manifest in the stream is corrupt or could not
* be read
*/
async function loadManifestFromWebManifest(aPackage) {
let extension = new ExtensionData(
XPIInternal.maybeResolveURI(aPackage.rootURI)
);
async function loadManifestFromWebManifest(aPackage, aLocation) {
let verifiedSignedState;
let extension = await ExtensionData.constructAsync({
rootURI: XPIInternal.maybeResolveURI(aPackage.rootURI),
async checkPrivileged(type, id) {
verifiedSignedState = await aPackage.verifySignedState(
id,
type,
aLocation
);
return ExtensionData.getIsPrivileged({
signedState: verifiedSignedState.signedState,
builtIn: aLocation.isBuiltin,
temporarilyInstalled: aLocation.isTemporary,
});
},
});
let manifest = await extension.loadManifest();
@ -574,7 +590,7 @@ async function loadManifestFromWebManifest(aPackage) {
addon.softDisabled =
addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED;
return addon;
return { addon, verifiedSignedState };
}
async function readRecommendationStates(aPackage, aAddonID) {
@ -649,13 +665,23 @@ function generateTemporaryInstallID(aFile) {
var loadManifest = async function(aPackage, aLocation, aOldAddon) {
let addon;
let verifiedSignedState;
if (await aPackage.hasResource("manifest.json")) {
addon = await loadManifestFromWebManifest(aPackage);
({ addon, verifiedSignedState } = await loadManifestFromWebManifest(
aPackage,
aLocation
));
} else {
// TODO bug 1674799: Remove this unused branch.
for (let loader of AddonManagerPrivate.externalExtensionLoaders.values()) {
if (await aPackage.hasResource(loader.manifestFile)) {
addon = await loader.loadManifest(aPackage);
addon.loader = loader.name;
verifiedSignedState = await aPackage.verifySignedState(
addon.id,
addon.type,
aLocation
);
break;
}
}
@ -671,11 +697,7 @@ var loadManifest = async function(aPackage, aLocation, aOldAddon) {
addon.rootURI = aPackage.rootURI.spec;
addon.location = aLocation;
let { signedState, cert } = await aPackage.verifySignedState(
addon.id,
addon.type,
aLocation
);
let { signedState, cert } = verifiedSignedState;
addon.signedState = signedState;
addon.signedDate = cert?.validity?.notBefore / 1000 || null;
if (!addon.isPrivileged) {

Просмотреть файл

@ -33,6 +33,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
Dictionary: "resource://gre/modules/Extension.jsm",
Extension: "resource://gre/modules/Extension.jsm",
ExtensionData: "resource://gre/modules/Extension.jsm",
Langpack: "resource://gre/modules/Extension.jsm",
SitePermission: "resource://gre/modules/Extension.jsm",
FileUtils: "resource://gre/modules/FileUtils.jsm",
@ -1804,6 +1805,13 @@ class BootstrapScope {
}
}
}
// TODO D128233: Replace AddonInternal's isPrivileged getter with a call to
// Extensions.getIsPrivileged, and use addon.isPrivileged instead of this.
const isPrivileged = ExtensionData.getIsPrivileged({
signedState: addon.signedState,
builtIn: addon.location.isBuiltin,
temporarilyInstalled: addon.location.isTemporary,
});
let params = {
id: addon.id,
@ -1813,6 +1821,7 @@ class BootstrapScope {
temporarilyInstalled: addon.location.isTemporary,
builtIn: addon.location.isBuiltin,
isSystem: addon.location.isSystem,
isPrivileged,
recommendationState: addon.recommendationState,
};

Просмотреть файл

@ -0,0 +1,189 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { XPIInstall } = ChromeUtils.import(
"resource://gre/modules/addons/XPIInstall.jsm"
);
const {
XPIInternal: {
BuiltInLocation,
KEY_APP_PROFILE,
KEY_APP_SYSTEM_DEFAULTS,
KEY_APP_SYSTEM_PROFILE,
TemporaryInstallLocation,
XPIStates,
},
} = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm");
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
Services.prefs.setIntPref(
"extensions.enabledScopes",
// SCOPE_PROFILE is enabled by default,
// SCOPE_APPLICATION is to enable KEY_APP_SYSTEM_PROFILE, which we need to
// test the combination (isSystem && !isBuiltin) in test_system_location.
AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION
);
// test_builtin_system_location tests the (isSystem && isBuiltin) combination
// (i.e. KEY_APP_SYSTEM_DEFAULTS). That location only exists if this directory
// is found:
const distroDir = FileUtils.getDir("ProfD", ["sysfeatures"], true);
registerDirectory("XREAppFeat", distroDir);
function getInstallLocation({
isBuiltin = false,
isSystem = false,
isTemporary = false,
}) {
if (isTemporary) {
// Temporary installation. Signatures will not be verified.
return TemporaryInstallLocation; // KEY_APP_TEMPORARY
}
let location;
if (isSystem) {
if (isBuiltin) {
// System location. Signatures will not be verified.
location = XPIStates.getLocation(KEY_APP_SYSTEM_DEFAULTS);
} else {
// Normandy installations. Signatures will be verified.
location = XPIStates.getLocation(KEY_APP_SYSTEM_PROFILE);
}
} else if (isBuiltin) {
// Packaged with the application. Signatures will not be verified.
location = BuiltInLocation; // KEY_APP_BUILTINS
} else {
// By default - The profile directory. Signatures will be verified.
location = XPIStates.getLocation(KEY_APP_PROFILE);
}
// Sanity checks to make sure that the flags match the expected values.
if (location.isSystem !== isSystem) {
ok(false, `${location.name}, unexpected isSystem=${location.isSystem}`);
}
if (location.isBuiltin !== isBuiltin) {
ok(false, `${location.name}, unexpected isBuiltin=${location.isBuiltin}`);
}
return location;
}
async function testLoadManifest({ location, expectPrivileged }) {
location ??= getInstallLocation({});
let xpi = await AddonTestUtils.createTempWebExtensionFile({
manifest: {
applications: { gecko: { id: "@with-privileged-perm" } },
permissions: ["mozillaAddons", "cookies"],
},
});
let actualPermissions;
let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
let addon = await XPIInstall.loadManifestFromFile(xpi, location);
actualPermissions = addon.userPermissions;
});
if (expectPrivileged) {
AddonTestUtils.checkMessages(messages, {
expected: [],
forbidden: [
{
message: /Reading manifest: Invalid extension permission/,
},
],
});
Assert.deepEqual(
actualPermissions,
{ origins: [], permissions: ["mozillaAddons", "cookies"] },
"Privileged permission should exist"
);
} else {
AddonTestUtils.checkMessages(messages, {
expected: [
{
message: /Reading manifest: Invalid extension permission: mozillaAddons/,
},
],
forbidden: [],
});
Assert.deepEqual(
actualPermissions,
{ origins: [], permissions: ["cookies"] },
"Privileged permission should be ignored"
);
}
}
add_task(async function setup() {
await ExtensionTestUtils.startAddonManager();
});
add_task(async function test_regular_addon() {
AddonTestUtils.usePrivilegedSignatures = false;
await testLoadManifest({
expectPrivileged: false,
});
});
add_task(async function test_privileged_signature() {
AddonTestUtils.usePrivilegedSignatures = true;
await testLoadManifest({
expectPrivileged: true,
});
});
add_task(async function test_system_signature() {
AddonTestUtils.usePrivilegedSignatures = "system";
await testLoadManifest({
expectPrivileged: true,
});
});
add_task(async function test_builtin_location() {
AddonTestUtils.usePrivilegedSignatures = false;
await testLoadManifest({
expectPrivileged: true,
location: getInstallLocation({ isBuiltin: true }),
});
});
add_task(async function test_system_location() {
AddonTestUtils.usePrivilegedSignatures = false;
await testLoadManifest({
expectPrivileged: false,
location: getInstallLocation({ isSystem: true }),
});
});
add_task(async function test_builtin_system_location() {
AddonTestUtils.usePrivilegedSignatures = false;
await testLoadManifest({
expectPrivileged: true,
location: getInstallLocation({ isSystem: true, isBuiltin: true }),
});
});
add_task(async function test_temporary_regular() {
AddonTestUtils.usePrivilegedSignatures = false;
Services.prefs.setBoolPref("extensions.experiments.enabled", false);
await testLoadManifest({
expectPrivileged: false,
location: getInstallLocation({ isTemporary: true }),
});
});
add_task(async function test_temporary_privileged_signature() {
AddonTestUtils.usePrivilegedSignatures = true;
Services.prefs.setBoolPref("extensions.experiments.enabled", false);
await testLoadManifest({
expectPrivileged: true,
location: getInstallLocation({ isTemporary: true }),
});
});
add_task(async function test_temporary_experiments_enabled() {
AddonTestUtils.usePrivilegedSignatures = false;
Services.prefs.setBoolPref("extensions.experiments.enabled", true);
await testLoadManifest({
expectPrivileged: true,
location: getInstallLocation({ isTemporary: true }),
});
});

Просмотреть файл

@ -57,6 +57,7 @@ skip-if = appname != "firefox" || (os == "win" && processor == "aarch64") # bug
[test_installtrigger_schemes.js]
[test_isDebuggable.js]
[test_isReady.js]
[test_loadManifest_isPrivileged.js]
[test_locale.js]
[test_moved_extension_metadata.js]
skip-if = true