tor-browser

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

commit a6dee5b53d67439568fc0b968ad76b39094699ae
parent fc3765bf0b6c6dec2645b88f14c74933e42c7e35
Author: James Hay <yellow.desk1472@fastmail.com>
Date:   Mon, 24 Nov 2025 02:19:28 +0000

Bug 1864284 - Allow localhost origins in temporary MV3 add-ons. r=robwu

Differential Revision: https://phabricator.services.mozilla.com/D269149

Diffstat:
Mmodules/libpref/init/all.js | 1+
Mtoolkit/components/extensions/Extension.sys.mjs | 1+
Mtoolkit/components/extensions/Schemas.sys.mjs | 30++++++++++++++++++++++++++++++
Mtoolkit/components/extensions/WebExtensionPolicy.cpp | 13+++++++++++++
Mtoolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mtoolkit/components/extensions/test/xpcshell/test_csp_validator.js | 13++++++++++++-
Mtoolkit/components/extensions/test/xpcshell/test_ext_content_security_policy.js | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mtoolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 301 insertions(+), 31 deletions(-)

diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js @@ -3164,6 +3164,7 @@ pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/is // Add-on content security policies. pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* http://localhost:* http://127.0.0.1:* moz-extension: blob: filesystem: 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-inline';"); pref("extensions.webextensions.base-content-security-policy.v3", "script-src 'self' 'wasm-unsafe-eval';"); +pref("extensions.webextensions.base-content-security-policy.v3-with-localhost", "script-src 'self' 'wasm-unsafe-eval' http://localhost:* http://127.0.0.1:*;"); pref("extensions.webextensions.default-content-security-policy", "script-src 'self' 'wasm-unsafe-eval';"); pref("extensions.webextensions.default-content-security-policy.v3", "script-src 'self'; upgrade-insecure-requests;"); diff --git a/toolkit/components/extensions/Extension.sys.mjs b/toolkit/components/extensions/Extension.sys.mjs @@ -1717,6 +1717,7 @@ export class ExtensionData { manifestVersion: this.manifestVersion, // We introduced this context param in Bug 1831417. ignoreUnrecognizedProperties: false, + temporarilyInstalled: this.temporarilyInstalled, }; if (this.fluentL10n || this.localeData) { diff --git a/toolkit/components/extensions/Schemas.sys.mjs b/toolkit/components/extensions/Schemas.sys.mjs @@ -483,6 +483,10 @@ class Context { return !!this.params.ignoreUnrecognizedProperties; } + get temporarilyInstalled() { + return !!this.params.temporarilyInstalled; + } + get principal() { return ( this.params.principal || @@ -1270,11 +1274,37 @@ const FORMATS = { // Manifest V3 extension_pages allows WASM. When sandbox is // implemented, or any other V3 or later directive, the flags // logic will need to be updated. + let flags = context.manifestVersion < 3 ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY : Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM; + let error = lazy.contentPolicyService.validateAddonCSP(string, flags); + + if ( + error && + context.manifestVersion === 3 && + !lazy.contentPolicyService.validateAddonCSP( + string, + flags | Ci.nsIAddonContentPolicy.CSP_ALLOW_LOCALHOST + ) + ) { + error = + `Using localhost in the Content Security Policy is invalid, ` + + `and is only permitted during development with temporarily ` + + `loaded add-ons`; + + // The error occurred due to the presence of localhost CSP settings, which should be allowed + // when an MV3 extension is loaded as a temporary add-on for debugging purposes. + if (context.temporarilyInstalled) { + context.logWarning( + `Warning processing ${context.currentTarget}: ${error}` + ); + return string; + } + } + if (error != null) { // The CSP validation error is not reported as part of the "choices" error message, // we log the CSP validation error explicitly here to make it easier for the addon developers diff --git a/toolkit/components/extensions/WebExtensionPolicy.cpp b/toolkit/components/extensions/WebExtensionPolicy.cpp @@ -60,6 +60,12 @@ static const char kBackgroundPageHTMLEnd[] = "extensions.webextensions.base-content-security-policy.v3" #define DEFAULT_BASE_CSP_V3 "script-src 'self' 'wasm-unsafe-eval';" +#define BASE_CSP_PREF_V3_WITH_LOCALHOST \ + "extensions.webextensions.base-content-security-policy.v3-with-localhost" +#define DEFAULT_BASE_CSP_V3_WITH_LOCALHOST \ + "script-src 'self' 'wasm-unsafe-eval' http://localhost:* " \ + "http://127.0.0.1:*;" + static inline ExtensionPolicyService& EPS() { return ExtensionPolicyService::GetSingleton(); } @@ -202,6 +208,13 @@ WebExtensionPolicyCore::WebExtensionPolicyCore(GlobalObject& aGlobal, if (NS_FAILED(rv)) { mBaseCSP = NS_LITERAL_STRING_FROM_CSTRING(DEFAULT_BASE_CSP_V2); } + } else if (mTemporarilyInstalled) { + nsresult rv = + Preferences::GetString(BASE_CSP_PREF_V3_WITH_LOCALHOST, mBaseCSP); + if (NS_FAILED(rv)) { + mBaseCSP = + NS_LITERAL_STRING_FROM_CSTRING(DEFAULT_BASE_CSP_V3_WITH_LOCALHOST); + } } else { nsresult rv = Preferences::GetString(BASE_CSP_PREF_V3, mBaseCSP); if (NS_FAILED(rv)) { diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js @@ -15,10 +15,27 @@ const aps = Cc["@mozilla.org/addons/policy-service;1"].getService( const v2_csp = Preferences.get( "extensions.webextensions.base-content-security-policy" ); + const v3_csp = Preferences.get( "extensions.webextensions.base-content-security-policy.v3" ); +const v3_with_localhost_csp = Preferences.get( + "extensions.webextensions.base-content-security-policy.v3-with-localhost" +); + +function getExpectedBaseCSP(manifestVersion, temporarilyInstalled) { + if (manifestVersion === 2) { + return v2_csp; + } + + if (temporarilyInstalled) { + return v3_with_localhost_csp; + } + + return v3_csp; +} + add_task(async function test_invalid_addon_csp() { await Assert.throws( () => aps.getBaseCSP("invalid@missing"), @@ -84,11 +101,32 @@ add_task(async function test_policy_csp() { }, expectedPolicy: CUSTOM_POLICY, }, + { + name: "manifest 3 version set (temporary install), no custom policy", + policyData: { + manifestVersion: 3, + temporarilyInstalled: true, + }, + expectedPolicy: aps.defaultCSPV3, + }, + { + name: "manifest 3 version set (temporary install), custom extensionPage policy", + policyData: { + manifestVersion: 3, + temporarilyInstalled: true, + extensionPageCSP: `script-src 'self' https://127.0.0.1 https://localhost`, + }, + expectedPolicy: `script-src 'self' https://127.0.0.1 https://localhost`, + }, ]; let policy = null; - function setExtensionCSP({ manifestVersion, extensionPageCSP }) { + function setExtensionCSP({ + manifestVersion, + extensionPageCSP, + temporarilyInstalled, + }) { if (policy) { policy.active = false; } @@ -101,6 +139,7 @@ add_task(async function test_policy_csp() { allowedOrigins: new MatchPatternSet([]), localizeCallback() {}, + temporarilyInstalled, manifestVersion, extensionPageCSP, }); @@ -111,11 +150,13 @@ add_task(async function test_policy_csp() { for (let test of tests) { info(test.name); setExtensionCSP(test.policyData); - equal( - aps.getBaseCSP(ADDON_ID), - test.policyData.manifestVersion == 3 ? v3_csp : v2_csp, - "baseCSP is correct" + + let expectedBaseCSP = getExpectedBaseCSP( + test.policyData.manifestVersion ?? 2, + !!test.policyData.temporarilyInstalled ); + + equal(aps.getBaseCSP(ADDON_ID), expectedBaseCSP, "baseCSP is correct"); equal( aps.getExtensionPageCSP(ADDON_ID), test.expectedPolicy, @@ -223,6 +264,28 @@ add_task(async function test_extension_csp() { expectedPolicy: aps.defaultCSPV3, }, { + name: "manifest_v3 allows localhost when temporarily installed", + temporarilyInstalled: true, + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self' https://127.0.0.1 https://localhost`, + }, + }, + expectedPolicy: `script-src 'self' https://127.0.0.1 https://localhost`, + }, + { + name: "manifest_v3 allows 127.0.0.1 when temporarily installed", + temporarilyInstalled: true, + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: `script-src 'self' https://127.0.0.1`, + }, + }, + expectedPolicy: `script-src 'self' https://127.0.0.1`, + }, + { name: "manifest_v3 allows wasm-unsafe-eval", manifest: { manifest_version: 3, @@ -281,19 +344,24 @@ add_task(async function test_extension_csp() { info(test.name); let extension = ExtensionTestUtils.loadExtension({ manifest: test.manifest, + temporarilyInstalled: !!test.temporarilyInstalled, }); await extension.startup(); let policy = WebExtensionPolicy.getByID(extension.id); - equal( - policy.baseCSP, - test.manifest.manifest_version == 3 ? v3_csp : v2_csp, - "baseCSP is correct" + + const expectedBaseCSP = getExpectedBaseCSP( + policy.manifestVersion, + policy.temporarilyInstalled ); + + equal(policy.baseCSP, expectedBaseCSP, "baseCSP is correct"); + equal( policy.extensionPageCSP, test.expectedPolicy, "extensionPageCSP is correct." ); + await extension.unload(); } diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_validator.js b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js @@ -273,7 +273,9 @@ add_task(async function test_csp_validator_extension_pages() { "\u2018script-src\u2019 directive contains a forbidden 'unsafe-eval' keyword" ); - // Localhost is invalid + // Localhost is invalid under normal circumstance, but passes through + // if validated under CSP_ALLOW_LOCALHOST, which is the validation + // setting used for temporarily loaded extensions. for (let src of [ "http://localhost", "https://localhost", @@ -285,6 +287,15 @@ add_task(async function test_csp_validator_extension_pages() { `script-src 'self' ${src};`, `\u2018script-src\u2019 directive contains a forbidden ${protocol}: protocol source` ); + + equal( + cps.validateAddonCSP( + `script-src 'self' ${src}`, + Ci.nsIAddonContentPolicy.CSP_ALLOW_LOCALHOST + ), + null, + `Localhost source should be allowed by CSP_ALLOW_LOCALHOST: ${src}` + ); } let directives = ["script-src", "worker-src"]; diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_content_security_policy.js b/toolkit/components/extensions/test/xpcshell/test_ext_content_security_policy.js @@ -16,25 +16,41 @@ server.registerPathHandler("/worker.js", (request, response) => { response.write("let x = true;"); }); -const baseCSP = []; -// Keep in sync with extensions.webextensions.base-content-security-policy -baseCSP[2] = { - "script-src": [ - "'unsafe-eval'", - "'wasm-unsafe-eval'", - "'unsafe-inline'", - "blob:", - "filesystem:", - "http://localhost:*", - "http://127.0.0.1:*", - "https://*", - "moz-extension:", - "'self'", - ], -}; -// Keep in sync with extensions.webextensions.base-content-security-policy.v3 -baseCSP[3] = { - "script-src": ["'self'", "'wasm-unsafe-eval'"], +server.registerPathHandler("/local.js", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/javascript", false); + response.write("let y = true;"); +}); + +const baseCSP = { + // Keep in sync with extensions.webextensions.base-content-security-policy + v2: { + "script-src": [ + "'unsafe-eval'", + "'wasm-unsafe-eval'", + "'unsafe-inline'", + "blob:", + "filesystem:", + "http://localhost:*", + "http://127.0.0.1:*", + "https://*", + "moz-extension:", + "'self'", + ], + }, + // Keep in sync with extensions.webextensions.base-content-security-policy.v3 + v3: { + "script-src": ["'self'", "'wasm-unsafe-eval'"], + }, + // Keep in sync with extensions.webextensions.base-content-security-policy.v3-with-localhost + v3_with_localhost: { + "script-src": [ + "'self'", + "'wasm-unsafe-eval'", + "http://localhost:*", + "http://127.0.0.1:*", + ], + }, }; /** @@ -43,6 +59,7 @@ baseCSP[3] = { * @param {boolean} workerEvalAllowed * @param {boolean} workerImportScriptsAllowed * @param {boolean} workerWasmAllowed + * @param {boolean} localhostAllowed */ /** @@ -55,16 +72,19 @@ baseCSP[3] = { * @param {number} [options.manifest_version] * @param {object} [options.customCSP] * @param {TestPolicyExpects} options.expects + * @param {boolean} [options.temporarilyInstalled] */ async function testPolicy({ manifest_version = 2, customCSP = null, expects = {}, + temporarilyInstalled = false, }) { info( `Enter tests for extension CSP with ${JSON.stringify({ manifest_version, customCSP, + temporarilyInstalled, })}` ); @@ -103,11 +123,23 @@ async function testPolicy({ ); } + function getBaseCsp() { + if (manifest_version === 2) { + return baseCSP.v2; + } + + if (temporarilyInstalled) { + return baseCSP.v3_with_localhost; + } + + return baseCSP.v3; + } + function checkCSP(csp, location) { let policies = csp["csp-policies"]; info(`Base policy for ${location}`); - let base = baseCSP[manifest_version]; + let base = getBaseCsp(); equal(policies[0]["report-only"], false, "Policy is not report-only"); for (let key in base) { @@ -139,6 +171,8 @@ async function testPolicy({ browser.test.sendMessage("worker-csp", event.data); }; + browser.test.sendMessage("localhost-csp", typeof y !== "undefined"); + worker.postMessage({}); } @@ -193,7 +227,9 @@ async function testPolicy({ files: { "tab.html": `<html><head><meta charset="utf-8"> - <script src="tab.js"></${"script"}></head></html>`, + <script src="http://127.0.0.1:${server.identity.primaryPort}/local.js"></${"script"}> + <script src="tab.js"></${"script"}> + </head><body></body></html>`, "tab.js": tabScript, @@ -206,6 +242,7 @@ async function testPolicy({ content_security_policy, web_accessible_resources, }, + temporarilyInstalled, }); function frameScript() { @@ -228,8 +265,17 @@ async function testPolicy({ info(`Testing CSP for policy: ${JSON.stringify(content_security_policy)}`); + // As temporarily installed manifest V3 extensions will intentionally + // present warnings when localhost URLs are specified in the CSP, + // we want to disable failure-by-warning for these tests. + ExtensionTestUtils.failOnSchemaWarnings( + !temporarilyInstalled || manifest_version !== 3 || !expects.localhostAllowed + ); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + baseURL = await extension.awaitMessage("base-url"); let tabPage = await ExtensionTestUtils.loadContentPage( @@ -266,6 +312,9 @@ async function testPolicy({ checkCSP(contentCSP, "content frame"); + let localhostAllowed = await extension.awaitMessage("localhost-csp"); + equal(localhostAllowed, expects.localhostAllowed, "localhost allowed"); + let workerCSP = await extension.awaitMessage("worker-csp"); equal( workerCSP.importScriptsAllowed, @@ -291,6 +340,7 @@ add_task(async function testCSP() { workerEvalAllowed: false, workerImportAllowed: false, workerWasmAllowed: true, + localhostAllowed: false, }, }); @@ -306,6 +356,7 @@ add_task(async function testCSP() { workerEvalAllowed: true, workerImportAllowed: false, workerWasmAllowed: true, + localhostAllowed: false, }, }); @@ -318,6 +369,21 @@ add_task(async function testCSP() { workerEvalAllowed: false, workerImportAllowed: false, workerWasmAllowed: true, + localhostAllowed: false, + }, + }); + + await testPolicy({ + manifest_version: 2, + customCSP: { + "script-src": `'self' http://127.0.0.1:${server.identity.primaryPort}`, + }, + expects: { + workerEvalAllowed: false, + // importScripts() of localhost URL is allowed by the CSP + workerImportAllowed: true, + workerWasmAllowed: true, + localhostAllowed: true, }, }); @@ -331,6 +397,7 @@ add_task(async function testCSP() { workerEvalAllowed: false, workerImportAllowed: false, workerWasmAllowed: false, + localhostAllowed: false, }, }); @@ -344,6 +411,7 @@ add_task(async function testCSP() { workerEvalAllowed: false, workerImportAllowed: false, workerWasmAllowed: false, + localhostAllowed: false, }, }); @@ -357,6 +425,33 @@ add_task(async function testCSP() { workerEvalAllowed: false, workerImportAllowed: false, workerWasmAllowed: true, + localhostAllowed: false, + }, + }); + + await testPolicy({ + manifest_version: 3, + customCSP: null, + expects: { + workerEvalAllowed: false, + workerImportAllowed: false, + workerWasmAllowed: false, + localhostAllowed: false, + }, + temporarilyInstalled: true, + }); + + await testPolicy({ + manifest_version: 3, + customCSP: { + "script-src": `'self' http://127.0.0.1:${server.identity.primaryPort}`, + }, + expects: { + workerEvalAllowed: false, + workerImportAllowed: true, + workerWasmAllowed: false, + localhostAllowed: true, }, + temporarilyInstalled: true, }); }); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js @@ -3,6 +3,7 @@ "use strict"; Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); +AddonTestUtils.init(this); add_task(async function test_manifest_csp() { let normalized = await ExtensionTestUtils.normalizeManifest({ @@ -111,4 +112,54 @@ add_task(async function test_manifest_csp_v3() { ], "Should have the expected warning for extension_pages CSP" ); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: "script-src 'self' http://localhost:8080;", + }, + }, + temporarilyInstalled: true, + }); + + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + + Assert.deepEqual( + extension.extension.warnings, + "Reading manifest: Warning processing content_security_policy.extension_pages: Warning processing content_security_policy.extension_pages: " + + "Using localhost in the Content Security Policy is invalid, and is only permitted during development with temporarily loaded add-ons", + "Expected manifest warnings" + ); + + await extension.unload(); + + extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + content_security_policy: { + extension_pages: "script-src 'self' http://localhost:8080;", + }, + }, + temporarilyInstalled: false, + }); + + let { messages } = await promiseConsoleOutput(async () => { + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + }); + + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: + /Reading manifest: Error processing content_security_policy.extension_pages: Using localhost in the Content Security Policy is invalid, and is only permitted during development with temporarily loaded add-ons/, + }, + ], + }); + + await extension.unload(); });