From 7e7534ced47c8d8d809045916c142cfbc9e8b778 Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Tue, 9 Aug 2022 12:16:34 +0000 Subject: [PATCH] Bug 1780747 - Register DNR schema and permissions r=rpl,geckoview-reviewers,owlish,flod This patch adds the minimum necessary to register the declarativeNetRequest API and its permissions, behind prefs. Tests have been added/updated to verify that the permissions and API access are enforced correctly (effectiveness of preferences, API visibility, permission warnings). Before landing this, we need to register the permission warning in Android-Components too, as mentioned in the bug (i.e. bug 1671453). Differential Revision: https://phabricator.services.mozilla.com/D152503 --- .../en-US/chrome/browser/browser.properties | 1 + .../locales/en-US/chrome/browser.properties | 1 + toolkit/components/extensions/Extension.jsm | 25 +++ .../components/extensions/ext-toolkit.json | 8 + toolkit/components/extensions/jar.mn | 1 + .../parent/ext-declarativeNetRequest.js | 22 ++ .../schemas/declarative_net_request.json | 136 +++++++++++ toolkit/components/extensions/schemas/jar.mn | 1 + .../test/xpcshell/test_ext_dnr_api.js | 212 ++++++++++++++++++ .../xpcshell/test_ext_permission_warnings.js | 59 +++++ .../test/xpcshell/test_ext_permissions.js | 2 + .../test/xpcshell/xpcshell-common.ini | 1 + .../extensions/test/xpcshell/xpcshell.ini | 1 + 13 files changed, 470 insertions(+) create mode 100644 toolkit/components/extensions/parent/ext-declarativeNetRequest.js create mode 100644 toolkit/components/extensions/schemas/declarative_net_request.json create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_dnr_api.js diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index 1da45c0ce4d0..9a1709c89b45 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -127,6 +127,7 @@ webextPerms.description.browserSettings=Read and modify browser settings webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data webextPerms.description.clipboardRead=Get data from the clipboard webextPerms.description.clipboardWrite=Input data to the clipboard +webextPerms.description.declarativeNetRequest=Block content on any page webextPerms.description.devtools=Extend developer tools to access your data in open tabs webextPerms.description.downloads=Download files and read and modify the browser’s download history webextPerms.description.downloads.open=Open files downloaded to your computer diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties index 05070d3f469e..00a90f0979dc 100644 --- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -49,6 +49,7 @@ webextPerms.description.browserSettings=Read and modify browser settings webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data webextPerms.description.clipboardRead=Get data from the clipboard webextPerms.description.clipboardWrite=Input data to the clipboard +webextPerms.description.declarativeNetRequest=Block content on any page webextPerms.description.devtools=Extend developer tools to access your data in open tabs webextPerms.description.downloads=Download files and read and modify the browser’s download history webextPerms.description.downloads.open=Open files downloaded to your computer diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 1248c0f23d03..985c2b042433 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -210,6 +210,9 @@ if ( } } +const PREF_DNR_ENABLED = "extensions.dnr.enabled"; +const PREF_DNR_FEEDBACK = "extensions.dnr.feedback"; + // Message included in warnings and errors related to privileged permissions and // privileged manifest properties. Provides a link to the firefox-source-docs.mozilla.org // section related to developing and sign Privileged Add-ons. @@ -263,6 +266,23 @@ function isMozillaExtension(extension) { return isSigned && isMozillaLineExtension; } +function isDNRPermissionAllowed(perm) { + // DNR is under development and therefore disabled by default for now. + if (!Services.prefs.getBoolPref(PREF_DNR_ENABLED, false)) { + return false; + } + + // APIs tied to declarativeNetRequestFeedback are for debugging purposes and + // are only supposed to be available when the (add-on dev) user opts in. + if ( + perm === "declarativeNetRequestFeedback" && + !Services.prefs.getBoolPref(PREF_DNR_FEEDBACK, false) + ) { + return false; + } + return true; +} + /** * Classify an individual permission from a webextension manifest * as a host/origin permission, an api permission, or a regular permission. @@ -295,6 +315,11 @@ function classifyPermission(perm, restrictSchemes, isPrivileged) { return { api: match[2] }; } else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) { return { invalid: perm, privileged: true }; + } else if ( + perm.startsWith("declarativeNetRequest") && + !isDNRPermissionAllowed(perm) + ) { + return { invalid: perm }; } return { permission: perm }; } diff --git a/toolkit/components/extensions/ext-toolkit.json b/toolkit/components/extensions/ext-toolkit.json index 31f6afb755fd..7179c0072791 100644 --- a/toolkit/components/extensions/ext-toolkit.json +++ b/toolkit/components/extensions/ext-toolkit.json @@ -60,6 +60,14 @@ ["cookies"] ] }, + "declarativeNetRequest": { + "url": "chrome://extensions/content/parent/ext-declarativeNetRequest.js", + "schema": "chrome://extensions/content/schemas/declarative_net_request.json", + "scopes": ["addon_parent"], + "paths": [ + ["declarativeNetRequest"] + ] + }, "dns": { "url": "chrome://extensions/content/parent/ext-dns.js", "schema": "chrome://extensions/content/schemas/dns.json", diff --git a/toolkit/components/extensions/jar.mn b/toolkit/components/extensions/jar.mn index d04b65e1d719..c4cc77d77606 100644 --- a/toolkit/components/extensions/jar.mn +++ b/toolkit/components/extensions/jar.mn @@ -19,6 +19,7 @@ toolkit.jar: content/extensions/parent/ext-contextualIdentities.js (parent/ext-contextualIdentities.js) content/extensions/parent/ext-clipboard.js (parent/ext-clipboard.js) content/extensions/parent/ext-cookies.js (parent/ext-cookies.js) + content/extensions/parent/ext-declarativeNetRequest.js (parent/ext-declarativeNetRequest.js) content/extensions/parent/ext-dns.js (parent/ext-dns.js) content/extensions/parent/ext-downloads.js (parent/ext-downloads.js) content/extensions/parent/ext-extension.js (parent/ext-extension.js) diff --git a/toolkit/components/extensions/parent/ext-declarativeNetRequest.js b/toolkit/components/extensions/parent/ext-declarativeNetRequest.js new file mode 100644 index 000000000000..1d92718be673 --- /dev/null +++ b/toolkit/components/extensions/parent/ext-declarativeNetRequest.js @@ -0,0 +1,22 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.declarativeNetRequest = class extends ExtensionAPI { + getAPI(context) { + return { + declarativeNetRequest: { + async testMatchOutcome(request) { + // TODO bug 1745758: Implement rule evaluation engine. + // Since rule registration has not been implemented yet, the result + // is always an empty list. + return []; + }, + }, + }; + } +}; diff --git a/toolkit/components/extensions/schemas/declarative_net_request.json b/toolkit/components/extensions/schemas/declarative_net_request.json new file mode 100644 index 000000000000..8ea3d80be20a --- /dev/null +++ b/toolkit/components/extensions/schemas/declarative_net_request.json @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +[ + { + "namespace": "manifest", + "types": [ + { + "$extend": "Permission", + "choices": [{ + "type": "string", + "enum": ["declarativeNetRequest"] + }] + }, + { + "$extend": "PermissionNoPrompt", + "choices": [{ + "type": "string", + "enum": ["declarativeNetRequestFeedback", "declarativeNetRequestWithHostAccess"] + }] + } + ] + }, + { + "namespace": "declarativeNetRequest", + "description": "Use the declarativeNetRequest API to block or modify network requests by specifying declarative rules.", + "permissions": ["declarativeNetRequest", "declarativeNetRequestWithHostAccess"], + "types": [ + { + "id": "ResourceType", + "type": "string", + "description": "How the requested resource will be used. Comparable to the webRequest.ResourceType type.", + "enum": [ + "main_frame", + "sub_frame", + "stylesheet", + "script", + "image", + "object", + "object_subrequest", + "xmlhttprequest", + "xslt", + "ping", + "beacon", + "xml_dtd", + "font", + "media", + "websocket", + "csp_report", + "imageset", + "web_manifest", + "speculative", + "other" + ] + }, + { + "id": "MatchedRule", + "type": "object", + "properties": { + "ruleId": { + "type": "integer", + "description": "A matching rule's ID." + }, + "rulesetId": { + "type": "string", + "description": "ID of the Ruleset this rule belongs to." + } + } + } + ], + "functions": [ + { + "name": "testMatchOutcome", + "type": "function", + "description": "Checks if any of the extension's declarativeNetRequest rules would match a hypothetical request.", + "permissions": ["declarativeNetRequestFeedback"], + "async": "callback", + "parameters": [ + { + "name": "request", + "type": "object", + "description": "The details of the request to test.", + "properties": { + "url": { + "type": "string", + "description": "The URL of the hypothetical request." + }, + "initiator": { + "type": "string", + "description": "The initiator URL (if any) for the hypothetical request.", + "optional": true + }, + "method": { + "type": "string", + "description": "Standard HTTP method of the hypothetical request.", + "optional": true, + "default": "get" + }, + "type": { + "$ref": "ResourceType", + "description": "The resource type of the hypothetical request." + }, + "tabId": { + "type": "integer", + "description": "The ID of the tab in which the hypothetical request takes place. Does not need to correspond to a real tab ID. Default is -1, meaning that the request isn't related to a tab.", + "optional": true, + "default": -1 + } + } + }, + { + "name": "callback", + "type": "function", + "description": "Called with the details of matched rules.", + "parameters": [ + { + "name": "result", + "type": "object", + "properties": { + "matchedRules": { + "type": "array", + "description": "The rules (if any) that match the hypothetical request.", + "items": { + "$ref": "MatchedRule" + } + } + } + } + ] + } + ] + } + ] + } +] diff --git a/toolkit/components/extensions/schemas/jar.mn b/toolkit/components/extensions/schemas/jar.mn index d9b86d3613f8..59e2332ebe48 100644 --- a/toolkit/components/extensions/schemas/jar.mn +++ b/toolkit/components/extensions/schemas/jar.mn @@ -16,6 +16,7 @@ toolkit.jar: content/extensions/schemas/content_scripts.json content/extensions/schemas/contextual_identities.json content/extensions/schemas/cookies.json + content/extensions/schemas/declarative_net_request.json content/extensions/schemas/dns.json content/extensions/schemas/downloads.json content/extensions/schemas/events.json diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_api.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_api.js new file mode 100644 index 000000000000..cb86fdb227a6 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_api.js @@ -0,0 +1,212 @@ +"use strict"; + +AddonTestUtils.init(this); + +const PREF_DNR_FEEDBACK_DEFAULT_VALUE = Services.prefs.getBoolPref( + "extensions.dnr.feedback", + false +); + +async function testAvailability({ + allowDNRFeedback = false, + testExpectations, + ...extensionData +}) { + function background(testExpectations) { + let { + declarativeNetRequest_available = false, + testMatchOutcome_available = false, + } = testExpectations; + browser.test.assertEq( + declarativeNetRequest_available, + !!browser.declarativeNetRequest, + "declarativeNetRequest API namespace availability" + ); + browser.test.assertEq( + testMatchOutcome_available, + !!browser.declarativeNetRequest?.testMatchOutcome, + "declarativeNetRequest.testMatchOutcome availability" + ); + browser.test.sendMessage("done"); + } + let extension = ExtensionTestUtils.loadExtension({ + ...extensionData, + background: `(${background})(${JSON.stringify(testExpectations)});`, + }); + Services.prefs.setBoolPref("extensions.dnr.feedback", allowDNRFeedback); + try { + await extension.startup(); + await extension.awaitMessage("done"); + await extension.unload(); + } finally { + Services.prefs.clearUserPref("extensions.dnr.feedback"); + } +} + +add_setup(async () => { + // TODO bug 1782685: Remove this check. + Assert.equal( + Services.prefs.getBoolPref("extensions.dnr.enabled", false), + false, + "DNR is disabled by default" + ); + Services.prefs.setBoolPref("extensions.dnr.enabled", true); +}); + +// Verifies that DNR is disabled by default (until true in bug 1782685). +add_task( + { + pref_set: [["extensions.dnr.enabled", false]], + }, + async function dnr_disabled_by_default() { + let { messages } = await promiseConsoleOutput(async () => { + await testAvailability({ + allowDNRFeedback: PREF_DNR_FEEDBACK_DEFAULT_VALUE, + testExpectations: { + declarativeNetRequest_available: false, + }, + manifest: { + permissions: [ + "declarativeNetRequest", + "declarativeNetRequestFeedback", + "declarativeNetRequestWithHostAccess", + ], + }, + }); + }); + + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: /Reading manifest: Invalid extension permission: declarativeNetRequest$/, + }, + { + message: /Reading manifest: Invalid extension permission: declarativeNetRequestFeedback/, + }, + { + message: /Reading manifest: Invalid extension permission: declarativeNetRequestWithHostAccess/, + }, + ], + }); + } +); + +add_task(async function dnr_feedback_apis_disabled_by_default() { + let { messages } = await promiseConsoleOutput(async () => { + await testAvailability({ + allowDNRFeedback: PREF_DNR_FEEDBACK_DEFAULT_VALUE, + testExpectations: { + declarativeNetRequest_available: true, + }, + manifest: { + permissions: [ + "declarativeNetRequest", + "declarativeNetRequestFeedback", + "declarativeNetRequestWithHostAccess", + ], + }, + }); + }); + + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: /Reading manifest: Invalid extension permission: declarativeNetRequestFeedback/, + }, + ], + forbidden: [ + { + message: /Reading manifest: Invalid extension permission: declarativeNetRequest$/, + }, + { + message: /Reading manifest: Invalid extension permission: declarativeNetRequestWithHostAccess/, + }, + ], + }); +}); + +add_task(async function with_declarativeNetRequest_permission() { + await testAvailability({ + allowDNRFeedback: true, + testExpectations: { + declarativeNetRequest_available: true, + // feature allowed, but missing declarativeNetRequestFeedback: + testMatchOutcome_available: false, + }, + manifest: { + permissions: ["declarativeNetRequest"], + }, + }); +}); + +add_task(async function with_declarativeNetRequestWithHostAccess_permission() { + await testAvailability({ + allowDNRFeedback: true, + testExpectations: { + declarativeNetRequest_available: true, + // feature allowed, but missing declarativeNetRequestFeedback: + testMatchOutcome_available: false, + }, + manifest: { + permissions: ["declarativeNetRequestWithHostAccess"], + }, + }); +}); + +add_task(async function with_all_declarativeNetRequest_permissions() { + await testAvailability({ + allowDNRFeedback: true, + testExpectations: { + declarativeNetRequest_available: true, + // feature allowed, but missing declarativeNetRequestFeedback: + testMatchOutcome_available: false, + }, + manifest: { + permissions: [ + "declarativeNetRequest", + "declarativeNetRequestWithHostAccess", + ], + }, + }); +}); + +add_task(async function no_declarativeNetRequest_permission() { + await testAvailability({ + allowDNRFeedback: true, + testExpectations: { + // Just declarativeNetRequestFeedback should not unlock the API. + declarativeNetRequest_available: false, + }, + manifest: { + permissions: ["declarativeNetRequestFeedback"], + }, + }); +}); + +add_task(async function with_declarativeNetRequestFeedback_permission() { + await testAvailability({ + allowDNRFeedback: true, + testExpectations: { + declarativeNetRequest_available: true, + // feature allowed, and all permissions specified: + testMatchOutcome_available: true, + }, + manifest: { + permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"], + }, + }); +}); + +add_task(async function declarativeNetRequestFeedback_without_feature() { + await testAvailability({ + allowDNRFeedback: false, + testExpectations: { + declarativeNetRequest_available: true, + // all permissions set, but DNR feedback feature not allowed. + testMatchOutcome_available: false, + }, + manifest: { + permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"], + }, + }); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js index fb2611453cb8..264267664f6f 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js @@ -471,6 +471,65 @@ add_task(async function nativeMessaging_permission() { } }); +add_task(async function declarativeNetRequest_unavailable_by_default() { + let manifestPermissions = await getManifestPermissions({ + manifest: { + permissions: ["declarativeNetRequest"], + }, + }); + deepEqual( + manifestPermissions, + { origins: [], permissions: [] }, + "Expected declarativeNetRequest permission to be ignored/stripped" + ); +}); + +add_task( + { pref_set: [["extensions.dnr.enabled", true]] }, + async function declarativeNetRequest_permission_with_warning() { + let manifestPermissions = await getManifestPermissions({ + manifest: { + permissions: ["declarativeNetRequest"], + }, + }); + + deepEqual( + manifestPermissions, + { origins: [], permissions: ["declarativeNetRequest"] }, + "Expected origins and permissions" + ); + + deepEqual( + getPermissionWarnings(manifestPermissions), + [ + bundle.GetStringFromName( + "webextPerms.description.declarativeNetRequest" + ), + ], + "Expected warnings" + ); + } +); + +add_task( + { pref_set: [["extensions.dnr.enabled", true]] }, + async function declarativeNetRequest_permission_without_warning() { + let manifestPermissions = await getManifestPermissions({ + manifest: { + permissions: ["declarativeNetRequestWithHostAccess"], + }, + }); + + deepEqual( + manifestPermissions, + { origins: [], permissions: ["declarativeNetRequestWithHostAccess"] }, + "Expected origins and permissions" + ); + + deepEqual(getPermissionWarnings(manifestPermissions), [], "No warnings"); + } +); + // Tests that the expected permission warnings are generated for a mix of host // permissions and API permissions, for a privileged extension that uses the // mozillaAddons permission. diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js index 948b72c393ee..cd608c0aa41e 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js @@ -626,6 +626,8 @@ const GRANTED_WITHOUT_USER_PROMPT = [ "contextMenus", "contextualIdentities", "cookies", + "declarativeNetRequestFeedback", + "declarativeNetRequestWithHostAccess", "dns", "geckoProfiler", "identity", diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini index 29bf178c3c6c..39fad27830ee 100644 --- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini +++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini @@ -101,6 +101,7 @@ skip-if = appname == "thunderbird" || os == "android" # Containers are not expos [test_ext_cors_mozextension.js] [test_ext_csp_frame_ancestors.js] [test_ext_debugging_utils.js] +[test_ext_dnr_api.js] [test_ext_dns.js] skip-if = os == "android" # Android needs alternative for proxy.settings - bug 1723523 [test_ext_downloads.js] diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell.ini b/toolkit/components/extensions/test/xpcshell/xpcshell.ini index 3368e3c08a2a..95b60f92009a 100644 --- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini +++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini @@ -58,6 +58,7 @@ skip-if = toolkit == 'android' # browser_action icon testing not supported on an [test_ext_manifest_minimum_opera_version.js] [test_ext_manifest_themes.js] [test_ext_permission_warnings.js] +skip-if = condprof # Bug 1783828 - condprof fails to pick up schema changes [test_ext_schemas.js] head = head.js head_schemas.js [test_ext_schemas_roots.js]