diff options
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.js | 545 |
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, + }); +}); |