mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
Bug 1594234 manifest v3 content security validation improvements r=robwu,geckoview-reviewers,agi
This patch adds CSP validation for manifest v3 changes when parsing the addon manifest. Differential Revision: https://phabricator.services.mozilla.com/D100720
This commit is contained in:
parent
4a14410028
commit
98c9307c72
@ -68,11 +68,6 @@ pref("extensions.startupScanScopes", 0);
|
||||
pref("extensions.geckoProfiler.acceptedExtensionIds", "geckoprofiler@mozilla.com,quantum-foxfooding@mozilla.com,raptor@mozilla.org");
|
||||
|
||||
|
||||
// Add-on content security policies.
|
||||
pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
|
||||
pref("extensions.webextensions.base-content-security-policy.v3", "script-src 'self'; object-src 'self'; style-src 'self'; worker-src 'self';");
|
||||
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
|
||||
|
||||
pref("extensions.webextensions.remote", true);
|
||||
pref("extensions.webextensions.background-delayed-startup", true);
|
||||
|
||||
|
@ -75,10 +75,21 @@ interface nsIAddonPolicyService : nsISupports
|
||||
[scriptable, uuid(7a4fe60b-9131-45f5-83f3-dc63b5d71a5d)]
|
||||
interface nsIAddonContentPolicy : nsISupports
|
||||
{
|
||||
/* options to pass to validateAddonCSP
|
||||
*
|
||||
* Manifest V2 uses CSP_ALLOW_ANY.
|
||||
* In Manifest V3, extension_pages would use CSP_ALLOW_LOCALHOST and
|
||||
* sandbox would use CSP_ALLOW_EVAL.
|
||||
*/
|
||||
const unsigned long CSP_ALLOW_ANY = 0xFFFF;
|
||||
const unsigned long CSP_ALLOW_LOCALHOST = (1<<0);
|
||||
const unsigned long CSP_ALLOW_EVAL = (1<<1);
|
||||
const unsigned long CSP_ALLOW_REMOTE = (1<<2);
|
||||
|
||||
/**
|
||||
* Checks a custom content security policy string, to ensure that it meets
|
||||
* minimum security requirements. Returns null for valid policies, or a
|
||||
* string describing the error for invalid policies.
|
||||
*/
|
||||
AString validateAddonCSP(in AString aPolicyString);
|
||||
AString validateAddonCSP(in AString aPolicyString, in unsigned long aPermittedPolicy);
|
||||
};
|
||||
|
@ -196,11 +196,6 @@ pref("extensions.installDistroAddons", false);
|
||||
pref("extensions.webextPermissionPrompts", true);
|
||||
pref("extensions.webextOptionalPermissionPrompts", true);
|
||||
|
||||
// Add-on content security policies.
|
||||
pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
|
||||
pref("extensions.webextensions.base-content-security-policy.v3", "script-src 'self'; object-src 'self'; style-src 'self'; worker-src 'self';");
|
||||
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
|
||||
|
||||
pref("extensions.webextensions.background-delayed-startup", true);
|
||||
|
||||
pref("extensions.experiments.enabled", false);
|
||||
|
@ -3907,6 +3907,12 @@ pref("extensions.webcompat-reporter.newIssueEndpoint", "https://webcompat.com/is
|
||||
pref("extensions.webcompat-reporter.enabled", false);
|
||||
#endif
|
||||
|
||||
// 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' 'unsafe-inline'; object-src 'self' moz-extension: blob: filesystem:;");
|
||||
pref("extensions.webextensions.base-content-security-policy.v3", "script-src 'self' http://localhost:* http://127.0.0.1:*; object-src 'self';");
|
||||
pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
|
||||
|
||||
|
||||
pref("network.buffer.cache.count", 24);
|
||||
pref("network.buffer.cache.size", 32768);
|
||||
|
||||
|
@ -883,6 +883,7 @@ class ExtensionData {
|
||||
this.manifestWarning(error);
|
||||
},
|
||||
preprocessors: {},
|
||||
manifestVersion: this.manifest.manifest_version,
|
||||
};
|
||||
|
||||
if (this.fluentL10n || this.localeData) {
|
||||
|
@ -388,7 +388,7 @@ class Context {
|
||||
}
|
||||
}
|
||||
|
||||
let props = ["preprocessors", "isChromeCompat"];
|
||||
let props = ["preprocessors", "isChromeCompat", "manifestVersion"];
|
||||
for (let prop of props) {
|
||||
if (prop in params) {
|
||||
if (prop in this && typeof this[prop] == "object") {
|
||||
@ -1099,7 +1099,14 @@ const FORMATS = {
|
||||
},
|
||||
|
||||
contentSecurityPolicy(string, context) {
|
||||
let error = contentPolicyService.validateAddonCSP(string);
|
||||
// Manifest V3 extension_pages allows localhost. 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_LOCALHOST;
|
||||
let error = contentPolicyService.validateAddonCSP(string, flags);
|
||||
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
|
||||
|
@ -140,6 +140,26 @@ add_task(async function test_extension_csp() {
|
||||
},
|
||||
expectedPolicy: aps.defaultCSP,
|
||||
},
|
||||
{
|
||||
name: "manifest_v2 allows https protocol",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
content_security_policy: {
|
||||
extension_pages: `script-src 'self' https://*; object-src 'self'`,
|
||||
},
|
||||
},
|
||||
expectedPolicy: aps.defaultCSP,
|
||||
},
|
||||
{
|
||||
name: "manifest_v2 allows unsafe-eval",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
content_security_policy: {
|
||||
extension_pages: `script-src 'self' 'unsafe-eval'; object-src 'self'`,
|
||||
},
|
||||
},
|
||||
expectedPolicy: aps.defaultCSP,
|
||||
},
|
||||
{
|
||||
name: "manifest_v3 invalid csp results in default csp used",
|
||||
manifest: {
|
||||
@ -150,6 +170,46 @@ add_task(async function test_extension_csp() {
|
||||
},
|
||||
expectedPolicy: aps.defaultCSP,
|
||||
},
|
||||
{
|
||||
name: "manifest_v3 forbidden protocol results in default csp used",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
content_security_policy: {
|
||||
extension_pages: `script-src 'self' https://*; object-src 'self'`,
|
||||
},
|
||||
},
|
||||
expectedPolicy: aps.defaultCSP,
|
||||
},
|
||||
{
|
||||
name: "manifest_v3 forbidden eval results in default csp used",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
content_security_policy: {
|
||||
extension_pages: `script-src 'self' 'unsafe-eval'; object-src 'self'`,
|
||||
},
|
||||
},
|
||||
expectedPolicy: aps.defaultCSP,
|
||||
},
|
||||
{
|
||||
name: "manifest_v3 allows localhost",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
content_security_policy: {
|
||||
extension_pages: `script-src 'self' https://localhost; object-src 'self'`,
|
||||
},
|
||||
},
|
||||
expectedPolicy: `script-src 'self' https://localhost; object-src 'self'`,
|
||||
},
|
||||
{
|
||||
name: "manifest_v3 allows 127.0.0.1",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
content_security_policy: {
|
||||
extension_pages: `script-src 'self' https://127.0.0.1; object-src 'self'`,
|
||||
},
|
||||
},
|
||||
expectedPolicy: `script-src 'self' https://127.0.0.1; object-src 'self'`,
|
||||
},
|
||||
{
|
||||
name: "manifest_v2 csp",
|
||||
manifest: {
|
||||
|
@ -6,11 +6,64 @@ const cps = Cc["@mozilla.org/addons/content-policy;1"].getService(
|
||||
Ci.nsIAddonContentPolicy
|
||||
);
|
||||
|
||||
add_task(async function test_csp_validator_flags() {
|
||||
let checkPolicy = (policy, flags, expectedResult, message = null) => {
|
||||
info(`Checking policy: ${policy}`);
|
||||
|
||||
let result = cps.validateAddonCSP(policy, flags);
|
||||
equal(result, expectedResult);
|
||||
};
|
||||
|
||||
let flags = Ci.nsIAddonContentPolicy;
|
||||
|
||||
checkPolicy(
|
||||
"default-src 'self'; script-src 'self' http://localhost",
|
||||
0,
|
||||
"\u2018script-src\u2019 directive contains a forbidden http: protocol source",
|
||||
"localhost disallowed"
|
||||
);
|
||||
checkPolicy(
|
||||
"default-src 'self'; script-src 'self' http://localhost",
|
||||
flags.CSP_ALLOW_LOCALHOST,
|
||||
null,
|
||||
"localhost allowed"
|
||||
);
|
||||
|
||||
checkPolicy(
|
||||
"default-src 'self'; script-src 'self' 'unsafe-eval'",
|
||||
0,
|
||||
"\u2018script-src\u2019 directive contains a forbidden 'unsafe-eval' keyword",
|
||||
"eval disallowed"
|
||||
);
|
||||
checkPolicy(
|
||||
"default-src 'self'; script-src 'self' 'unsafe-eval'",
|
||||
flags.CSP_ALLOW_EVAL,
|
||||
null,
|
||||
"eval allowed"
|
||||
);
|
||||
|
||||
checkPolicy(
|
||||
"default-src 'self'; script-src 'self' https://example.com",
|
||||
0,
|
||||
"\u2018script-src\u2019 directive contains a forbidden https: protocol source",
|
||||
"remote disallowed"
|
||||
);
|
||||
checkPolicy(
|
||||
"default-src 'self'; script-src 'self' https://example.com",
|
||||
flags.CSP_ALLOW_REMOTE,
|
||||
null,
|
||||
"remote allowed"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_csp_validator() {
|
||||
let checkPolicy = (policy, expectedResult, message = null) => {
|
||||
info(`Checking policy: ${policy}`);
|
||||
|
||||
let result = cps.validateAddonCSP(policy);
|
||||
let result = cps.validateAddonCSP(
|
||||
policy,
|
||||
Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
|
||||
);
|
||||
equal(result, expectedResult);
|
||||
};
|
||||
|
||||
@ -82,6 +135,16 @@ add_task(async function test_csp_validator() {
|
||||
"\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword"
|
||||
);
|
||||
|
||||
// Localhost is always valid
|
||||
for (let src of [
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
"http://127.0.0.1",
|
||||
"https://127.0.0.1",
|
||||
]) {
|
||||
checkPolicy(`script-src 'self' ${src}; object-src 'none';`, null);
|
||||
}
|
||||
|
||||
let directives = ["script-src", "object-src"];
|
||||
|
||||
for (let [directive, other] of [directives, directives.slice().reverse()]) {
|
||||
@ -92,17 +155,135 @@ add_task(async function test_csp_validator() {
|
||||
);
|
||||
}
|
||||
|
||||
checkPolicy(
|
||||
`${directive} 'self' https:; ${other} 'self';`,
|
||||
`https: protocol requires a host in \u2018${directive}\u2019 directives`
|
||||
);
|
||||
for (let protocol of ["http", "https"]) {
|
||||
checkPolicy(
|
||||
`${directive} 'self' ${protocol}:; ${other} 'self';`,
|
||||
`${protocol}: protocol requires a host in \u2018${directive}\u2019 directives`
|
||||
);
|
||||
}
|
||||
|
||||
checkPolicy(
|
||||
`${directive} 'self' http://example.com; ${other} 'self';`,
|
||||
`\u2018${directive}\u2019 directive contains a forbidden http: protocol source`
|
||||
);
|
||||
|
||||
for (let protocol of ["http", "ftp", "meh"]) {
|
||||
for (let protocol of ["ftp", "meh"]) {
|
||||
checkPolicy(
|
||||
`${directive} 'self' ${protocol}:; ${other} 'self';`,
|
||||
`\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source`
|
||||
);
|
||||
}
|
||||
|
||||
checkPolicy(
|
||||
`${directive} 'self' 'nonce-01234'; ${other} 'self';`,
|
||||
`\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_csp_validator_extension_pages() {
|
||||
let checkPolicy = (policy, expectedResult, message = null) => {
|
||||
info(`Checking policy: ${policy}`);
|
||||
|
||||
let result = cps.validateAddonCSP(
|
||||
policy,
|
||||
Ci.nsIAddonContentPolicy.CSP_ALLOW_LOCALHOST
|
||||
);
|
||||
equal(result, expectedResult);
|
||||
};
|
||||
|
||||
checkPolicy("script-src 'self'; object-src 'self';", null);
|
||||
checkPolicy("script-src 'self'; object-src 'self'; worker-src 'none'", null);
|
||||
checkPolicy("script-src 'self'; object-src 'none'; worker-src 'self'", null);
|
||||
|
||||
let hash =
|
||||
"'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='";
|
||||
|
||||
checkPolicy(
|
||||
`script-src 'self' moz-extension://09abcdef blob: filesystem: ${hash}; ` +
|
||||
`object-src 'self' moz-extension://09abcdef blob: filesystem: ${hash}`,
|
||||
null
|
||||
);
|
||||
|
||||
for (let policy of ["", "object-src 'none';", "worker-src 'none';"]) {
|
||||
checkPolicy(
|
||||
policy,
|
||||
"Policy is missing a required \u2018script-src\u2019 directive"
|
||||
);
|
||||
}
|
||||
|
||||
checkPolicy(
|
||||
"default-src 'self'",
|
||||
null,
|
||||
"A valid default-src should count as a valid script-src or object-src"
|
||||
);
|
||||
|
||||
for (let directive of ["script-src", "object-src", "worker-src"]) {
|
||||
checkPolicy(
|
||||
`default-src 'self'; ${directive} 'self'`,
|
||||
null,
|
||||
`A valid default-src should count as a valid ${directive}`
|
||||
);
|
||||
checkPolicy(
|
||||
`default-src 'self'; ${directive} http://example.com`,
|
||||
`\u2018${directive}\u2019 directive contains a forbidden http: protocol source`,
|
||||
`A valid default-src should not allow an invalid ${directive} directive`
|
||||
);
|
||||
}
|
||||
|
||||
checkPolicy(
|
||||
"script-src 'self';",
|
||||
"Policy is missing a required \u2018object-src\u2019 directive"
|
||||
);
|
||||
|
||||
checkPolicy(
|
||||
"script-src 'none'; object-src 'none'",
|
||||
"\u2018script-src\u2019 must include the source 'self'"
|
||||
);
|
||||
|
||||
checkPolicy("script-src 'self'; object-src 'none';", null);
|
||||
|
||||
checkPolicy(
|
||||
"script-src 'self' 'unsafe-inline'; object-src 'self';",
|
||||
"\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword"
|
||||
);
|
||||
|
||||
checkPolicy(
|
||||
"script-src 'self' 'unsafe-eval'; object-src 'self';",
|
||||
"\u2018script-src\u2019 directive contains a forbidden 'unsafe-eval' keyword"
|
||||
);
|
||||
|
||||
// Localhost is always valid
|
||||
for (let src of [
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
"http://127.0.0.1",
|
||||
"https://127.0.0.1",
|
||||
]) {
|
||||
checkPolicy(`script-src 'self' ${src}; object-src 'none';`, null);
|
||||
}
|
||||
|
||||
let directives = ["script-src", "object-src"];
|
||||
|
||||
for (let [directive, other] of [directives, directives.slice().reverse()]) {
|
||||
for (let protocol of ["http", "https"]) {
|
||||
checkPolicy(
|
||||
`${directive} 'self' ${protocol}:; ${other} 'self';`,
|
||||
`${protocol}: protocol requires a host in \u2018${directive}\u2019 directives`
|
||||
);
|
||||
}
|
||||
|
||||
checkPolicy(
|
||||
`${directive} 'self' https://example.com; ${other} 'self';`,
|
||||
`\u2018${directive}\u2019 directive contains a forbidden https: protocol source`
|
||||
);
|
||||
|
||||
checkPolicy(
|
||||
`${directive} 'self' http://example.com; ${other} 'self';`,
|
||||
`\u2018${directive}\u2019 directive contains a forbidden http: protocol source`
|
||||
);
|
||||
|
||||
for (let protocol of ["ftp", "meh"]) {
|
||||
checkPolicy(
|
||||
`${directive} 'self' ${protocol}:; ${other} 'self';`,
|
||||
`\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source`
|
||||
|
@ -1,5 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
|
||||
|
||||
const server = createHttpServer({ hosts: ["example.com"] });
|
||||
|
||||
server.registerPathHandler("/dummy", (request, response) => {
|
||||
@ -8,36 +10,45 @@ server.registerPathHandler("/dummy", (request, response) => {
|
||||
response.write("<!DOCTYPE html><html></html>");
|
||||
});
|
||||
|
||||
server.registerPathHandler("/worker.js", (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "application/javascript", false);
|
||||
response.write("let x = true;");
|
||||
});
|
||||
|
||||
const baseCSP = [];
|
||||
baseCSP[2] = {
|
||||
"object-src": ["blob:", "filesystem:", "moz-extension:", "'self'"],
|
||||
"script-src": [
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
"blob:",
|
||||
"filesystem:",
|
||||
"http://localhost:*",
|
||||
"http://127.0.0.1:*",
|
||||
"https://*",
|
||||
"moz-extension:",
|
||||
"'self'",
|
||||
],
|
||||
};
|
||||
baseCSP[3] = {
|
||||
"object-src": ["'self'"],
|
||||
"script-src": ["http://localhost:*", "http://127.0.0.1:*", "'self'"],
|
||||
"worker-src": ["http://localhost:*", "http://127.0.0.1:*", "'self'"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that content security policies for an add-on are actually applied to *
|
||||
* documents that belong to it. This tests both the base policies and add-on
|
||||
* specific policies, and ensures that the parsed policies applied to the
|
||||
* document's principal match what was specified in the policy string.
|
||||
*
|
||||
* @param {number} [manifest_version]
|
||||
* @param {object} [customCSP]
|
||||
*/
|
||||
async function testPolicy(customCSP = null) {
|
||||
async function testPolicy(manifest_version = 2, customCSP = null) {
|
||||
let baseURL;
|
||||
|
||||
let baseCSP = {
|
||||
"object-src": [
|
||||
"blob:",
|
||||
"filesystem:",
|
||||
"https://*",
|
||||
"moz-extension:",
|
||||
"'self'",
|
||||
],
|
||||
"script-src": [
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
"blob:",
|
||||
"filesystem:",
|
||||
"https://*",
|
||||
"moz-extension:",
|
||||
"'self'",
|
||||
],
|
||||
};
|
||||
|
||||
let addonCSP = {
|
||||
"object-src": ["'self'"],
|
||||
"script-src": ["'self'"],
|
||||
@ -56,8 +67,13 @@ async function testPolicy(customCSP = null) {
|
||||
}
|
||||
|
||||
function checkSource(name, policy, expected) {
|
||||
// fallback to script-src when comparing worker-src if policy does not include worker-src
|
||||
let policySrc =
|
||||
name != "worker-src" || policy[name]
|
||||
? policy[name]
|
||||
: policy["script-src"];
|
||||
equal(
|
||||
JSON.stringify(policy[name].sort()),
|
||||
JSON.stringify(policySrc.sort()),
|
||||
JSON.stringify(expected[name].sort()),
|
||||
`Expected value for ${name}`
|
||||
);
|
||||
@ -67,16 +83,19 @@ async function testPolicy(customCSP = null) {
|
||||
let policies = csp["csp-policies"];
|
||||
|
||||
info(`Base policy for ${location}`);
|
||||
let base = baseCSP[manifest_version];
|
||||
|
||||
equal(policies[0]["report-only"], false, "Policy is not report-only");
|
||||
checkSource("object-src", policies[0], baseCSP);
|
||||
checkSource("script-src", policies[0], baseCSP);
|
||||
for (let key in base) {
|
||||
checkSource(key, policies[0], base);
|
||||
}
|
||||
|
||||
info(`Add-on policy for ${location}`);
|
||||
|
||||
equal(policies[1]["report-only"], false, "Policy is not report-only");
|
||||
checkSource("object-src", policies[1], addonCSP);
|
||||
checkSource("script-src", policies[1], addonCSP);
|
||||
for (let key in addonCSP) {
|
||||
checkSource(key, policies[1], addonCSP);
|
||||
}
|
||||
}
|
||||
|
||||
function background() {
|
||||
@ -90,6 +109,25 @@ async function testPolicy(customCSP = null) {
|
||||
|
||||
function tabScript() {
|
||||
browser.test.sendMessage("tab-csp", window.getCSP());
|
||||
|
||||
const worker = new Worker("worker.js");
|
||||
worker.onmessage = event => {
|
||||
browser.test.sendMessage("worker-csp", event.data);
|
||||
};
|
||||
|
||||
worker.postMessage({});
|
||||
}
|
||||
|
||||
function testWorker(port) {
|
||||
this.onmessage = () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
importScripts(`http://127.0.0.1:${port}/worker.js`);
|
||||
postMessage({ loaded: true });
|
||||
} catch (e) {
|
||||
postMessage({ loaded: false });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
@ -102,9 +140,11 @@ async function testPolicy(customCSP = null) {
|
||||
"tab.js": tabScript,
|
||||
|
||||
"content.html": `<html><head><meta charset="utf-8"></head></html>`,
|
||||
"worker.js": `(${testWorker})(${server.identity.primaryPort})`,
|
||||
},
|
||||
|
||||
manifest: {
|
||||
manifest_version,
|
||||
content_security_policy,
|
||||
|
||||
web_accessible_resources: ["content.html", "tab.html"],
|
||||
@ -169,6 +209,10 @@ async function testPolicy(customCSP = null) {
|
||||
|
||||
checkCSP(contentCSP, "content frame");
|
||||
|
||||
let workerCSP = await extension.awaitMessage("worker-csp");
|
||||
// TODO BUG 1685627: This test should fail if localhost is not in the csp.
|
||||
ok(workerCSP.loaded, "worker loaded");
|
||||
|
||||
await contentPage.close();
|
||||
await tabPage.close();
|
||||
|
||||
@ -178,18 +222,30 @@ async function testPolicy(customCSP = null) {
|
||||
}
|
||||
|
||||
add_task(async function testCSP() {
|
||||
await testPolicy(null);
|
||||
await testPolicy(2, null);
|
||||
|
||||
let hash =
|
||||
"'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='";
|
||||
|
||||
await testPolicy({
|
||||
await testPolicy(2, {
|
||||
"object-src": "'self' https://*.example.com",
|
||||
"script-src": `'self' https://*.example.com 'unsafe-eval' ${hash}`,
|
||||
});
|
||||
|
||||
await testPolicy({
|
||||
await testPolicy(2, {
|
||||
"object-src": "'none'",
|
||||
"script-src": `'self'`,
|
||||
});
|
||||
|
||||
await testPolicy(3, {
|
||||
"object-src": "'self' http://localhost",
|
||||
"script-src": `'self' http://localhost:123 ${hash}`,
|
||||
"worker-src": `'self' http://127.0.0.1:*`,
|
||||
});
|
||||
|
||||
await testPolicy(3, {
|
||||
"object-src": "'none'",
|
||||
"script-src": `'self'`,
|
||||
"worker-src": `'self'`,
|
||||
});
|
||||
});
|
||||
|
@ -48,11 +48,16 @@ add_task(async function test_manifest_csp_v3() {
|
||||
},
|
||||
});
|
||||
|
||||
equal(normalized.error, undefined, "Should not have an error");
|
||||
equal(normalized.errors.length, 0, "Should not have warnings");
|
||||
Assert.deepEqual(
|
||||
normalized.errors,
|
||||
[
|
||||
"Error processing content_security_policy.extension_pages: ‘script-src’ directive contains a forbidden 'unsafe-eval' keyword",
|
||||
],
|
||||
"Should have the expected warning"
|
||||
);
|
||||
equal(
|
||||
normalized.value.content_security_policy.extension_pages,
|
||||
"script-src 'self' 'unsafe-eval'; object-src 'none'",
|
||||
null,
|
||||
"Should have the expected policy string"
|
||||
);
|
||||
|
||||
|
@ -149,7 +149,8 @@ AddonContentPolicy::ShouldProcess(nsIURI* aContentLocation,
|
||||
|
||||
static const char* allowedSchemes[] = {"blob", "filesystem", nullptr};
|
||||
|
||||
static const char* allowedHostSchemes[] = {"https", "moz-extension", nullptr};
|
||||
static const char* allowedHostSchemes[] = {"http", "https", "moz-extension",
|
||||
nullptr};
|
||||
|
||||
/**
|
||||
* Validates a CSP directive to ensure that it is sufficiently stringent.
|
||||
@ -167,13 +168,21 @@ static const char* allowedHostSchemes[] = {"https", "moz-extension", nullptr};
|
||||
*
|
||||
* - No keyword sources are allowed other than 'none', 'self', 'unsafe-eval',
|
||||
* and hash sources.
|
||||
*
|
||||
* Manifest V3 limits CSP for extension_pages, the script-src, object-src, and
|
||||
* worker-src directives may only be the following:
|
||||
* - self
|
||||
* - none
|
||||
* - Any localhost source, (http://localhost, http://127.0.0.1, or any port
|
||||
* on those domains)
|
||||
*/
|
||||
class CSPValidator final : public nsCSPSrcVisitor {
|
||||
public:
|
||||
CSPValidator(nsAString& aURL, CSPDirective aDirective,
|
||||
bool aDirectiveRequired = true)
|
||||
bool aDirectiveRequired = true, uint32_t aPermittedPolicy = 0)
|
||||
: mURL(aURL),
|
||||
mDirective(CSP_CSPDirectiveToString(aDirective)),
|
||||
mPermittedPolicy(aPermittedPolicy),
|
||||
mFoundSelf(false) {
|
||||
// Start with the default error message for a missing directive, since no
|
||||
// visitors will be called if the directive isn't present.
|
||||
@ -206,7 +215,24 @@ class CSPValidator final : public nsCSPSrcVisitor {
|
||||
src.getScheme(scheme);
|
||||
src.getHost(host);
|
||||
|
||||
if (scheme.LowerCaseEqualsLiteral("http")) {
|
||||
// Allow localhost on http
|
||||
if (mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_LOCALHOST &&
|
||||
HostIsLocal(host)) {
|
||||
return true;
|
||||
}
|
||||
FormatError("csp.error.illegal-protocol", scheme);
|
||||
return false;
|
||||
}
|
||||
if (scheme.LowerCaseEqualsLiteral("https")) {
|
||||
if (mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_LOCALHOST &&
|
||||
HostIsLocal(host)) {
|
||||
return true;
|
||||
}
|
||||
if (!(mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_REMOTE)) {
|
||||
FormatError("csp.error.illegal-protocol", scheme);
|
||||
return false;
|
||||
}
|
||||
if (!HostIsAllowed(host)) {
|
||||
FormatError("csp.error.illegal-host-wildcard", scheme);
|
||||
return false;
|
||||
@ -237,9 +263,13 @@ class CSPValidator final : public nsCSPSrcVisitor {
|
||||
switch (src.getKeyword()) {
|
||||
case CSP_NONE:
|
||||
case CSP_SELF:
|
||||
case CSP_UNSAFE_EVAL:
|
||||
return true;
|
||||
|
||||
case CSP_UNSAFE_EVAL:
|
||||
if (mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_EVAL) {
|
||||
return true;
|
||||
}
|
||||
// fall through and produce an illegal-keyword error.
|
||||
[[fallthrough]];
|
||||
default:
|
||||
FormatError(
|
||||
"csp.error.illegal-keyword",
|
||||
@ -272,6 +302,9 @@ class CSPValidator final : public nsCSPSrcVisitor {
|
||||
|
||||
private:
|
||||
// Validators
|
||||
bool HostIsLocal(nsAString& host) {
|
||||
return host.EqualsLiteral("localhost") || host.EqualsLiteral("127.0.0.1");
|
||||
}
|
||||
|
||||
bool HostIsAllowed(nsAString& host) {
|
||||
if (host.First() == '*') {
|
||||
@ -342,6 +375,7 @@ class CSPValidator final : public nsCSPSrcVisitor {
|
||||
NS_ConvertASCIItoUTF16 mDirective;
|
||||
nsString mError;
|
||||
|
||||
uint32_t mPermittedPolicy;
|
||||
bool mFoundSelf;
|
||||
};
|
||||
|
||||
@ -356,6 +390,7 @@ class CSPValidator final : public nsCSPSrcVisitor {
|
||||
*/
|
||||
NS_IMETHODIMP
|
||||
AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString,
|
||||
uint32_t aPermittedPolicy,
|
||||
nsAString& aResult) {
|
||||
nsresult rv;
|
||||
|
||||
@ -393,12 +428,14 @@ AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString,
|
||||
|
||||
const nsCSPPolicy* policy = csp->GetPolicy(0);
|
||||
if (!policy) {
|
||||
CSPValidator validator(url, nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE);
|
||||
CSPValidator validator(url, nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE,
|
||||
true, aPermittedPolicy);
|
||||
aResult.Assign(validator.GetError());
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool haveValidDefaultSrc = false;
|
||||
bool hasValidScriptSrc = false;
|
||||
{
|
||||
CSPDirective directive = nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE;
|
||||
CSPValidator validator(url, directive);
|
||||
@ -409,7 +446,8 @@ AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString,
|
||||
aResult.SetIsVoid(true);
|
||||
{
|
||||
CSPDirective directive = nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE;
|
||||
CSPValidator validator(url, directive, !haveValidDefaultSrc);
|
||||
CSPValidator validator(url, directive, !haveValidDefaultSrc,
|
||||
aPermittedPolicy);
|
||||
|
||||
if (!policy->visitDirectiveSrcs(directive, &validator)) {
|
||||
aResult.Assign(validator.GetError());
|
||||
@ -417,11 +455,24 @@ AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString,
|
||||
validator.FormatError("csp.error.missing-source", u"'self'"_ns);
|
||||
aResult.Assign(validator.GetError());
|
||||
}
|
||||
hasValidScriptSrc = true;
|
||||
}
|
||||
|
||||
if (aResult.IsVoid()) {
|
||||
CSPDirective directive = nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE;
|
||||
CSPValidator validator(url, directive, !haveValidDefaultSrc);
|
||||
CSPValidator validator(url, directive, !haveValidDefaultSrc,
|
||||
aPermittedPolicy);
|
||||
|
||||
if (!policy->visitDirectiveSrcs(directive, &validator)) {
|
||||
aResult.Assign(validator.GetError());
|
||||
}
|
||||
}
|
||||
|
||||
if (aResult.IsVoid()) {
|
||||
CSPDirective directive = nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE;
|
||||
CSPValidator validator(url, directive,
|
||||
!haveValidDefaultSrc && !hasValidScriptSrc,
|
||||
aPermittedPolicy);
|
||||
|
||||
if (!policy->visitDirectiveSrcs(directive, &validator)) {
|
||||
aResult.Assign(validator.GetError());
|
||||
|
Loading…
Reference in New Issue
Block a user