/* -*- 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 = ` `; 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, }); });