summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js545
1 files changed, 545 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js
new file mode 100644
index 0000000000..402f54ca5e
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_webRequest_mergecsp.js
@@ -0,0 +1,545 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "43"
+);
+
+const server = createHttpServer({
+ hosts: ["example.net", "example.com"],
+});
+server.registerDirectory("/data/", do_get_file("data"));
+
+const pageContent = `<!DOCTYPE html>
+ <script id="script1" src="/data/file_script_good.js"></script>
+ <script id="script3" src="//example.com/data/file_script_bad.js"></script>
+ <img id="img1" src='/data/file_image_good.png'>
+ <img id="img3" src='//example.com/data/file_image_good.png'>
+`;
+
+server.registerPathHandler("/", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html");
+ if (request.queryString) {
+ response.setHeader(
+ "Content-Security-Policy",
+ decodeURIComponent(request.queryString)
+ );
+ }
+ response.write(pageContent);
+});
+
+let extensionData = {
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "*://example.net/*"],
+ },
+ background() {
+ let csp_value = undefined;
+ browser.test.onMessage.addListener(function (msg) {
+ csp_value = msg;
+ browser.test.sendMessage("csp-set");
+ });
+ browser.webRequest.onHeadersReceived.addListener(
+ e => {
+ browser.test.log(`onHeadersReceived ${e.requestId} ${e.url}`);
+ if (csp_value === undefined) {
+ browser.test.assertTrue(false, "extension called before CSP was set");
+ }
+ if (csp_value !== null) {
+ e.responseHeaders = e.responseHeaders.filter(
+ i => i.name.toLowerCase() != "content-security-policy"
+ );
+ if (csp_value !== "") {
+ e.responseHeaders.push({
+ name: "Content-Security-Policy",
+ value: csp_value,
+ });
+ }
+ }
+ return { responseHeaders: e.responseHeaders };
+ },
+ { urls: ["*://example.net/*"] },
+ ["blocking", "responseHeaders"]
+ );
+ },
+};
+
+/**
+ * @typedef {object} ExpectedResourcesToLoad
+ * @property {object} img1_loaded image from a first party origin.
+ * @property {object} img3_loaded image from a third party origin.
+ * @property {object} script1_loaded script from a first party origin.
+ * @property {object} script3_loaded script from a third party origin.
+ * @property {object} [cspJSON] expected final document CSP (in JSON format, See dom/webidl/CSPDictionaries.webidl).
+ */
+
+/**
+ * Test a combination of Content Security Policies against first/third party images/scripts.
+ *
+ * @param {object} opts
+ * @param {string} opts.site_csp The CSP to be sent by the site, or null.
+ * @param {string} opts.ext1_csp The CSP to be sent by the first extension,
+ * "" to remove the header, or null to not modify it.
+ * @param {string} opts.ext2_csp The CSP to be sent by the first extension,
+ * "" to remove the header, or null to not modify it.
+ * @param {ExpectedResourcesToLoad} opts.expect
+ * Object containing information which resources are expected to be loaded.
+ * @param {object} [opts.ext1_data] first test extension definition data (defaults to extensionData).
+ * @param {object} [opts.ext2_data] second test extension definition data (defaults to extensionData).
+ */
+async function test_csp({
+ site_csp,
+ ext1_csp,
+ ext2_csp,
+ expect,
+ ext1_data = extensionData,
+ ext2_data = extensionData,
+}) {
+ let extension1 = await ExtensionTestUtils.loadExtension(ext1_data);
+ let extension2 = await ExtensionTestUtils.loadExtension(ext2_data);
+ await extension1.startup();
+ await extension2.startup();
+ extension1.sendMessage(ext1_csp);
+ extension2.sendMessage(ext2_csp);
+ await extension1.awaitMessage("csp-set");
+ await extension2.awaitMessage("csp-set");
+
+ let csp_value = encodeURIComponent(site_csp || "");
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ `http://example.net/?${csp_value}`
+ );
+ let results = await contentPage.spawn([], async () => {
+ let img1 = this.content.document.getElementById("img1");
+ let img3 = this.content.document.getElementById("img3");
+ let cspJSON = JSON.parse(this.content.document.cspJSON);
+ return {
+ img1_loaded: img1.complete && img1.naturalWidth > 0,
+ img3_loaded: img3.complete && img3.naturalWidth > 0,
+ // Note: "good" and "bad" are just placeholders; they don't mean anything.
+ script1_loaded: !!this.content.document.getElementById("good"),
+ script3_loaded: !!this.content.document.getElementById("bad"),
+ cspJSON,
+ };
+ });
+
+ await contentPage.close();
+ await extension1.unload();
+ await extension2.unload();
+
+ let action = {
+ true: "loaded",
+ false: "blocked",
+ };
+
+ info(
+ `test_csp: From "${site_csp}" to ${JSON.stringify(
+ ext1_csp
+ )} to ${JSON.stringify(ext2_csp)}`
+ );
+
+ equal(
+ expect.img1_loaded,
+ results.img1_loaded,
+ `expected first party image to be ${action[expect.img1_loaded]}`
+ );
+ equal(
+ expect.img3_loaded,
+ results.img3_loaded,
+ `expected third party image to be ${action[expect.img3_loaded]}`
+ );
+ equal(
+ expect.script1_loaded,
+ results.script1_loaded,
+ `expected first party script to be ${action[expect.script1_loaded]}`
+ );
+ equal(
+ expect.script3_loaded,
+ results.script3_loaded,
+ `expected third party script to be ${action[expect.script3_loaded]}`
+ );
+
+ if (expect.cspJSON) {
+ Assert.deepEqual(
+ expect.cspJSON,
+ results.cspJSON["csp-policies"],
+ `Got the expected final CSP set on the content document`
+ );
+ }
+}
+
+add_setup(async () => {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+// Test that merging csp header on both mv2 and mv3 extensions
+// (and combination of both).
+add_task(async function test_webRequest_mergecsp() {
+ const testCases = [
+ {
+ site_csp: "default-src *",
+ ext1_csp: "script-src 'none'",
+ ext2_csp: null,
+ expect: {
+ img1_loaded: true,
+ img3_loaded: true,
+ script1_loaded: false,
+ script3_loaded: false,
+ },
+ },
+ {
+ site_csp: null,
+ ext1_csp: "script-src 'none'",
+ ext2_csp: null,
+ expect: {
+ img1_loaded: true,
+ img3_loaded: true,
+ script1_loaded: false,
+ script3_loaded: false,
+ },
+ },
+ {
+ site_csp: "default-src *",
+ ext1_csp: "script-src 'none'",
+ ext2_csp: "img-src 'none'",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: false,
+ script3_loaded: false,
+ },
+ },
+ {
+ site_csp: null,
+ ext1_csp: "script-src 'none'",
+ ext2_csp: "img-src 'none'",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: false,
+ script3_loaded: false,
+ },
+ },
+ {
+ site_csp: "default-src *",
+ ext1_csp: "img-src example.com",
+ ext2_csp: "img-src example.org",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: true,
+ script3_loaded: true,
+ },
+ },
+ ];
+
+ const extMV2Data = { ...extensionData };
+ const extMV3Data = {
+ ...extensionData,
+ useAddonManager: "temporary",
+ manifest: {
+ ...extensionData.manifest,
+ manifest_version: 3,
+ permissions: ["webRequest", "webRequestBlocking"],
+ host_permissions: ["*://example.net/*"],
+ granted_host_permissions: true,
+ },
+ };
+
+ info("Run all test cases on ext1 MV2 and ext2 MV2");
+ for (const testCase of testCases) {
+ await test_csp({
+ ...testCase,
+ ext1_data: extMV2Data,
+ ext2_data: extMV2Data,
+ });
+ }
+
+ info("Run all test cases on ext1 MV3 and ext2 MV3");
+ for (const testCase of testCases) {
+ await test_csp({
+ ...testCase,
+ ext1_data: extMV3Data,
+ ext2_data: extMV3Data,
+ });
+ }
+
+ info("Run all test cases on ext1 MV3 and ext2 MV2");
+ for (const testCase of testCases) {
+ await test_csp({
+ ...testCase,
+ ext1_data: extMV3Data,
+ ext2_data: extMV2Data,
+ });
+ }
+
+ info("Run all test cases on ext1 MV2 and ext2 MV3");
+ for (const testCase of testCases) {
+ await test_csp({
+ ...testCase,
+ ext1_data: extMV2Data,
+ ext2_data: extMV3Data,
+ });
+ }
+});
+
+add_task(async function test_remove_and_replace_csp_mv2() {
+ // CSP removed, CSP added.
+ await test_csp({
+ site_csp: "img-src 'self'",
+ ext1_csp: "",
+ ext2_csp: "img-src example.com",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: true,
+ script1_loaded: true,
+ script3_loaded: true,
+ },
+ });
+
+ // CSP removed, CSP added.
+ await test_csp({
+ site_csp: "default-src 'none'",
+ ext1_csp: "",
+ ext2_csp: "img-src example.com",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: true,
+ script1_loaded: true,
+ script3_loaded: true,
+ },
+ });
+
+ // CSP replaced - regression test for bug 1635781.
+ await test_csp({
+ site_csp: "default-src 'none'",
+ ext1_csp: "img-src example.com",
+ ext2_csp: null,
+ expect: {
+ img1_loaded: false,
+ img3_loaded: true,
+ script1_loaded: true,
+ script3_loaded: true,
+ },
+ });
+
+ // CSP unchanged, CSP replaced - regression test for bug 1635781.
+ await test_csp({
+ site_csp: "default-src 'none'",
+ ext1_csp: null,
+ ext2_csp: "img-src example.com",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: true,
+ script1_loaded: true,
+ script3_loaded: true,
+ },
+ });
+
+ // CSP replaced, CSP removed.
+ await test_csp({
+ site_csp: "default-src 'none'",
+ ext1_csp: "img-src example.com",
+ ext2_csp: "",
+ expect: {
+ img1_loaded: true,
+ img3_loaded: true,
+ script1_loaded: true,
+ script3_loaded: true,
+ },
+ });
+});
+
+// Test that fully replace the website csp header from an mv3 extension
+// isn't allowed and it is considered a no-op.
+add_task(async function test_remove_and_replace_csp_mv3() {
+ const extMV2Data = { ...extensionData };
+
+ const extMV3Data = {
+ ...extensionData,
+ useAddonManager: "temporary",
+ manifest: {
+ ...extensionData.manifest,
+ manifest_version: 3,
+ permissions: ["webRequest", "webRequestBlocking"],
+ host_permissions: ["*://example.net/*"],
+ granted_host_permissions: true,
+ },
+ };
+
+ await test_csp({
+ // site: CSP strict on images, lax on default and script src.
+ site_csp: "img-src 'self'",
+ // ext1: MV3 extension which return an empty CSP header (which is a no-op).
+ ext1_csp: "",
+ // ext2: MV3 extension which return a CSP header (which is expected to be merged).
+ ext2_csp: "img-src example.com",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: true,
+ script3_loaded: true,
+ cspJSON: [
+ { "img-src": ["'self'"], "report-only": false },
+ { "img-src": ["http://example.com"], "report-only": false },
+ ],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV3Data,
+ });
+
+ await test_csp({
+ // site: CSP strict on default-src.
+ site_csp: "default-src 'none'",
+ // ext1: MV3 extension which return an empty CSP header (which is a no-op).
+ ext1_csp: "",
+ // ext2: MV3 extension which return a CSP header (which is expected to be merged).
+ ext2_csp: "img-src example.com",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: false,
+ script3_loaded: false,
+ cspJSON: [
+ { "default-src": ["'none'"], "report-only": false },
+ { "img-src": ["http://example.com"], "report-only": false },
+ ],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV3Data,
+ });
+
+ await test_csp({
+ // site: CSP strict on default-src.
+ site_csp: "default-src 'none'",
+ // ext1: MV3 extension which return a CSP header (which is expected to be merged and to
+ // not be able to make it less strict).
+ ext1_csp: "img-src example.com",
+ // ext2: MV3 extension which leaves the header unmodified.
+ ext2_csp: null,
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: false,
+ script3_loaded: false,
+ cspJSON: [
+ { "default-src": ["'none'"], "report-only": false },
+ { "img-src": ["http://example.com"], "report-only": false },
+ ],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV3Data,
+ });
+
+ await test_csp({
+ // site: CSP strict on default-src.
+ site_csp: "default-src 'none'",
+ // ext1: MV3 extension which merges additional directive into the site csp (and can't make
+ // it less strict).
+ ext1_csp: "img-src example.com",
+ // ext2: MV3 extension which merges an empty CSP header (which is a no-op, unlike with MV2).
+ ext2_csp: "",
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: false,
+ script3_loaded: false,
+ cspJSON: [
+ { "default-src": ["'none'"], "report-only": false },
+ { "img-src": ["http://example.com"], "report-only": false },
+ ],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV3Data,
+ });
+
+ await test_csp({
+ // site: lax CSP (which is expected to be made stricted by the ext1 extension).
+ site_csp: "default-src *",
+ // ext1: MV3 extension which wants to set a stricter CSP (expected to work fine with the MV3 extension)
+ ext1_csp: "default-src 'none'",
+ // ext2: MV3 extension which leaves it unchanged.
+ ext2_csp: null,
+ expect: {
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: false,
+ script3_loaded: false,
+ cspJSON: [
+ { "default-src": ["*"], "report-only": false },
+ { "default-src": ["'none'"], "report-only": false },
+ ],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV3Data,
+ });
+
+ await test_csp({
+ // site: CSP strict on default-src.
+ site_csp: "default-src 'none'",
+ // ext1: MV3 extension and tries to replace the strict site csp with this lax one
+ // (but as an MV3 extension that is going to be merged to the site csp and the
+ // resulting site CSP is expected to stay strict).
+ ext1_csp: "default-src *",
+ // ext2: MV3 extension which leaves it unchanged.
+ ext2_csp: null,
+ expect: {
+ // strict site csp merged with the lax one from ext1 stays strict.
+ img1_loaded: false,
+ img3_loaded: false,
+ script1_loaded: false,
+ script3_loaded: false,
+ cspJSON: [
+ { "default-src": ["'none'"], "report-only": false },
+ { "default-src": ["*"], "report-only": false },
+ ],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV3Data,
+ });
+
+ await test_csp({
+ // site: CSP strict on default-src.
+ site_csp: "default-src 'none'",
+ // ext1: MV3 extension which return an empty CSP (expected to be a no-op for an MV3 extension).
+ ext1_csp: "",
+ // ext2: MV2 exension which wants to replace the site csp with a lax one (and still be allowed to
+ // because the empty one from the MV3 extension is expected to be a no-op).
+ ext2_csp: "default-src *",
+ expect: {
+ img1_loaded: true,
+ img3_loaded: true,
+ script1_loaded: true,
+ script3_loaded: true,
+ cspJSON: [{ "default-src": ["*"], "report-only": false }],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV2Data,
+ });
+
+ await test_csp({
+ // site: CSP strict on default-src.
+ site_csp: "default-src 'none'",
+ // ext1: MV3 extension which return an empty CSP (which is expected to be a no-op).
+ ext1_csp: "",
+ // ext2: MV2 extension which also returns an empty CSP (which for an MV2 extension is expected
+ // to clear the CSP).
+ ext2_csp: "",
+ expect: {
+ img1_loaded: true,
+ img3_loaded: true,
+ script1_loaded: true,
+ script3_loaded: true,
+ // Expect the resulting final document CSP to be empty (due to the MV2 extension clearing it).
+ cspJSON: [],
+ },
+ ext1_data: extMV3Data,
+ ext2_data: extMV2Data,
+ });
+});