Bug 1402850 Don't include runtime permissions in prompts for webextension updates
MozReview-Commit-ID: 1cnNsWLVGmg
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -418,36 +418,70 @@ class ExtensionData {
resolve(JSON.parse(text));
} catch (e) {
reject(e);
}
});
});
}
- // This method should return a structured representation of any
- // capabilities this extension has access to, as derived from the
- // manifest. The current implementation just returns the contents
- // of the permissions attribute, if we add things like url_overrides,
- // they should also be added here.
- get userPermissions() {
+ /**
+ * Returns an object representing any capabilities that the extension
+ * has access to based on fixed properties in the manifest. The result
+ * includes the contents of the "permissions" property as well as other
+ * capabilities that are derived from manifest fields that users should
+ * be informed of (e.g., origins where content scripts are injected).
+ */
+ get manifestPermissions() {
+ if (this.type !== "extension") {
+ return null;
+ }
+
+ let permissions = new Set();
+ let origins = new Set();
+ for (let perm of this.manifest.permissions || []) {
+ let type = classifyPermission(perm);
+ if (type.origin) {
+ origins.add(perm);
+ } else if (!type.api) {
+ permissions.add(perm);
+ }
+ }
+
+ if (this.manifest.devtools_page) {
+ permissions.add("devtools");
+ }
+
+ for (let entry of this.manifest.content_scripts || []) {
+ for (let origin of entry.matches) {
+ origins.add(origin);
+ }
+ }
+
+ return {
+ permissions: Array.from(permissions),
+ origins: Array.from(origins),
+ };
+ }
+
+ /**
+ * Returns an object representing all capabilities this extension has
+ * access to, including fixed ones from the manifest as well as dynamically
+ * granted permissions.
+ */
+ get activePermissions() {
if (this.type !== "extension") {
return null;
}
let result = {
origins: this.whiteListedHosts.patterns.map(matcher => matcher.pattern),
apis: [...this.apiNames],
};
- if (Array.isArray(this.manifest.content_scripts)) {
- for (let entry of this.manifest.content_scripts) {
- result.origins.push(...entry.matches);
- }
- }
const EXP_PATTERN = /^experiments\.\w+/;
result.permissions = [...this.permissions]
.filter(p => !result.origins.includes(p) && !EXP_PATTERN.test(p));
return result;
}
// Compute the difference between two sets of permissions, suitable
// for presenting to the user.
@@ -537,20 +571,16 @@ class ExtensionData {
originPermissions,
permissions,
schemaURLs: null,
type: this.type,
webAccessibleResources,
};
if (this.type === "extension") {
- if (this.manifest.devtools_page) {
- permissions.add("devtools");
- }
-
for (let perm of manifest.permissions) {
if (perm === "geckoProfiler" && !this.isPrivileged) {
const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
if (!acceptedExtensions.split(",").includes(id)) {
this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
continue;
}
}
--- a/toolkit/components/extensions/ext-permissions.js
+++ b/toolkit/components/extensions/ext-permissions.js
@@ -59,17 +59,17 @@ this.permissions = class extends Extensi
}
}
await ExtensionPermissions.add(context.extension, perms);
return true;
},
async getAll() {
- let perms = context.extension.userPermissions;
+ let perms = context.extension.activePermissions;
delete perms.apis;
return perms;
},
async contains(permissions) {
for (let perm of permissions.permissions) {
if (!context.extension.hasPermission(perm)) {
return false;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -1,12 +1,14 @@
"use strict";
+ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionPermissions.jsm");
ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
+ChromeUtils.import("resource://gre/modules/osfile.jsm");
const BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties";
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
let extensionHandlers = new WeakSet();
@@ -430,8 +432,93 @@ add_task(function test_permissions_have_
ok(str.length, `Found localization string for '${perm}' permission`);
} catch (e) {
ok(GRANTED_WITHOUT_USER_PROMPT.includes(perm),
`Permission '${perm}' intentionally granted without prompting the user`);
}
}
}
});
+
+// Check that optional permissions are not included in update prompts
+add_task(async function test_permissions_prompt() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, arg) => {
+ if (msg == "request") {
+ let result = await browser.permissions.request(arg);
+ browser.test.sendMessage("result", result);
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ name: "permissions test",
+ description: "permissions test",
+ manifest_version: 2,
+ version: "1.0",
+
+ permissions: ["tabs", "https://test1.example.com/*"],
+ optional_permissions: ["clipboardWrite", "<all_urls>"],
+
+ content_scripts: [
+ {
+ matches: ["https://test2.example.com/*"],
+ js: [],
+ },
+ ],
+ },
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+
+ await withHandlingUserInput(extension, async () => {
+ extension.sendMessage("request", {
+ permissions: ["clipboardWrite"],
+ origins: ["https://test2.example.com/*"],
+ });
+ let result = await extension.awaitMessage("result");
+ equal(result, true, "request() for optional permissions succeeded");
+ });
+
+ const PERMS = ["history", "tabs"];
+ const ORIGINS = ["https://test1.example.com/*", "https://test3.example.com/"];
+ let xpi = Extension.generateXPI({
+ background,
+ manifest: {
+ name: "permissions test",
+ description: "permissions test",
+ manifest_version: 2,
+ version: "2.0",
+
+ applications: {gecko: {id: extension.id}},
+
+ permissions: [...PERMS, ...ORIGINS],
+ optional_permissions: ["clipboardWrite", "<all_urls>"],
+ },
+ });
+
+ let install = await AddonManager.getInstallForFile(xpi);
+
+ Services.prefs.setBoolPref("extensions.webextPermissionPrompts", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webextPermissionPrompts");
+ });
+
+ let perminfo;
+ install.promptHandler = info => {
+ perminfo = info;
+ return Promise.resolve();
+ };
+
+ await AddonTestUtils.promiseCompleteInstall(install);
+ await extension.awaitStartup();
+
+ notEqual(perminfo, undefined, "Permission handler was invoked");
+ let perms = perminfo.addon.userPermissions;
+ deepEqual(perms.permissions, PERMS, "Update details includes only manifest api permissions");
+ deepEqual(perms.origins, ORIGINS, "Update details includes only manifest origin permissions");
+
+ await extension.unload();
+ await OS.File.remove(xpi.path);
+});
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -370,17 +370,17 @@ async function loadManifestFromWebManife
else
addon.optionsBrowserStyle = manifest.options_ui.browser_style;
}
// WebExtensions don't use iconURLs
addon.iconURL = null;
addon.icon64URL = null;
addon.icons = manifest.icons || {};
- addon.userPermissions = extension.userPermissions;
+ addon.userPermissions = extension.manifestPermissions;
addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
function getLocale(aLocale) {
// Use the raw manifest, here, since we need values with their
// localization placeholders still in place.
let rawManifest = extension.rawManifest;