summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.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_contentscript_csp.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_contentscript_csp.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js433
1 files changed, 433 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js
new file mode 100644
index 0000000000..6b03f5b0b0
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_csp.js
@@ -0,0 +1,433 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+
+const server = createHttpServer({
+ hosts: ["example.com", "csplog.example.net"],
+});
+server.registerDirectory("/data/", do_get_file("data"));
+
+var gDefaultCSP = `default-src 'self' 'report-sample'; script-src 'self' 'report-sample';`;
+var gCSP = gDefaultCSP;
+const pageContent = `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title></title>
+ </head>
+ <body>
+ <img id="testimg">
+ </body>
+ </html>`;
+
+server.registerPathHandler("/plain.html", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html");
+ if (gCSP) {
+ info(`Content-Security-Policy: ${gCSP}`);
+ response.setHeader("Content-Security-Policy", gCSP);
+ }
+ response.write(pageContent);
+});
+
+const BASE_URL = `http://example.com`;
+const pageURL = `${BASE_URL}/plain.html`;
+
+const CSP_REPORT_PATH = "/csp-report.sjs";
+
+function readUTF8InputStream(stream) {
+ let buffer = NetUtil.readInputStream(stream, stream.available());
+ return new TextDecoder().decode(buffer);
+}
+
+server.registerPathHandler(CSP_REPORT_PATH, (request, response) => {
+ response.setStatusLine(request.httpVersion, 204, "No Content");
+ let data = readUTF8InputStream(request.bodyInputStream);
+ Services.obs.notifyObservers(null, "extension-test-csp-report", data);
+});
+
+async function promiseCSPReport(test) {
+ let res = await TestUtils.topicObserved("extension-test-csp-report", test);
+ return JSON.parse(res[1]);
+}
+
+// Test functions loaded into extension content script.
+function testImage(data = {}) {
+ return new Promise(resolve => {
+ let img = window.document.getElementById("testimg");
+ img.onload = () => resolve(true);
+ img.onerror = () => {
+ browser.test.log(`img error: ${img.src}`);
+ resolve(false);
+ };
+ img.src = data.image_url;
+ });
+}
+
+function testFetch(data = {}) {
+ let f = data.content ? content.fetch : fetch;
+ return f(data.url)
+ .then(() => true)
+ .catch(e => {
+ browser.test.assertEq(
+ e.message,
+ "NetworkError when attempting to fetch resource.",
+ "expected fetch failure"
+ );
+ return false;
+ });
+}
+
+async function testEval(data = {}) {
+ try {
+ // eslint-disable-next-line no-eval
+ let ev = data.content ? window.eval : eval;
+ return ev("true");
+ } catch (e) {
+ return false;
+ }
+}
+
+async function testFunction(data = {}) {
+ try {
+ // eslint-disable-next-line no-eval
+ let fn = data.content ? window.Function : Function;
+ let sum = new fn("a", "b", "return a + b");
+ return sum(1, 1);
+ } catch (e) {
+ return 0;
+ }
+}
+
+function testScriptTag(data) {
+ return new Promise(resolve => {
+ let script = document.createElement("script");
+ script.src = data.url;
+ script.onload = () => {
+ resolve(true);
+ };
+ script.onerror = () => {
+ resolve(false);
+ };
+ document.body.appendChild(script);
+ });
+}
+
+async function testHttpRequestUpgraded(data = {}) {
+ let f = data.content ? content.fetch : fetch;
+ return f(data.url)
+ .then(() => "http:")
+ .catch(() => "https:");
+}
+
+async function testWebSocketUpgraded(data = {}) {
+ let ws = data.content ? content.WebSocket : WebSocket;
+ new ws(data.url);
+}
+
+function webSocketUpgradeListenerBackground() {
+ // Catch websocket requests and send the protocol back to be asserted.
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ // Send the protocol back as test result.
+ // This will either be "wss:", "ws:"
+ browser.test.sendMessage("result", new URL(details.url).protocol);
+ return { cancel: true };
+ },
+ { urls: ["wss://example.com/*", "ws://example.com/*"] },
+ ["blocking"]
+ );
+}
+
+// If the violation source is the extension the securitypolicyviolation event is not fired.
+// If the page is the source, the event is fired and both the content script or page scripts
+// will receive the event. If we're expecting a moz-extension report we'll fail in the
+// event listener if we receive a report. Otherwise we want to resolve in the listener to
+// ensure we've received the event for the test.
+function contentScript(report) {
+ return new Promise(resolve => {
+ if (!report || report["document-uri"] === "moz-extension") {
+ resolve();
+ }
+ // eslint-disable-next-line mozilla/balanced-listeners
+ document.addEventListener("securitypolicyviolation", e => {
+ browser.test.assertTrue(
+ e.documentURI !== "moz-extension",
+ `securitypolicyviolation: ${e.violatedDirective} ${e.documentURI}`
+ );
+ resolve();
+ });
+ });
+}
+
+let TESTS = [
+ // Image Tests
+ {
+ description:
+ "Image from content script using default extension csp. Image is allowed.",
+ pageCSP: `${gDefaultCSP} img-src 'none';`,
+ script: testImage,
+ data: { image_url: `${BASE_URL}/data/file_image_good.png` },
+ expect: true,
+ },
+ // Fetch Tests
+ {
+ description: "Fetch url in content script uses default extension csp.",
+ pageCSP: `${gDefaultCSP} connect-src 'none';`,
+ script: testFetch,
+ data: { url: `${BASE_URL}/data/file_image_good.png` },
+ expect: true,
+ },
+ {
+ description: "Fetch full url from content script uses page csp.",
+ pageCSP: `${gDefaultCSP} connect-src 'none';`,
+ script: testFetch,
+ data: {
+ content: true,
+ url: `${BASE_URL}/data/file_image_good.png`,
+ },
+ expect: false,
+ report: {
+ "blocked-uri": `${BASE_URL}/data/file_image_good.png`,
+ "document-uri": `${BASE_URL}/plain.html`,
+ "violated-directive": "connect-src",
+ },
+ },
+
+ // Eval tests.
+ {
+ description: "Eval from content script uses page csp with unsafe-eval.",
+ pageCSP: `default-src 'none'; script-src 'unsafe-eval';`,
+ script: testEval,
+ data: { content: true },
+ expect: true,
+ },
+ {
+ description: "Eval from content script uses page csp.",
+ pageCSP: `default-src 'self' 'report-sample'; script-src 'self';`,
+ version: 3,
+ script: testEval,
+ data: { content: true },
+ expect: false,
+ report: {
+ "blocked-uri": "eval",
+ "document-uri": "http://example.com/plain.html",
+ "violated-directive": "script-src",
+ },
+ },
+ {
+ description: "Eval in content script allowed by v2 csp.",
+ pageCSP: `script-src 'self' 'unsafe-eval';`,
+ script: testEval,
+ expect: true,
+ },
+ {
+ description: "Eval in content script disallowed by v3 csp.",
+ pageCSP: `script-src 'self' 'unsafe-eval';`,
+ version: 3,
+ script: testEval,
+ expect: false,
+ },
+ {
+ description: "Wrapped Eval in content script uses page csp.",
+ pageCSP: `script-src 'self' 'unsafe-eval';`,
+ version: 3,
+ script: async () => {
+ return window.wrappedJSObject.eval("true");
+ },
+ expect: true,
+ },
+ {
+ description: "Wrapped Eval in content script denied by page csp.",
+ pageCSP: `script-src 'self';`,
+ version: 3,
+ script: async () => {
+ try {
+ return window.wrappedJSObject.eval("true");
+ } catch (e) {
+ return false;
+ }
+ },
+ expect: false,
+ },
+
+ {
+ description: "Function from content script uses page csp.",
+ pageCSP: `default-src 'self'; script-src 'self' 'unsafe-eval';`,
+ script: testFunction,
+ data: { content: true },
+ expect: 2,
+ },
+ {
+ description: "Function from content script uses page csp.",
+ pageCSP: `default-src 'self' 'report-sample'; script-src 'self';`,
+ version: 3,
+ script: testFunction,
+ data: { content: true },
+ expect: 0,
+ report: {
+ "blocked-uri": "eval",
+ "document-uri": "http://example.com/plain.html",
+ "violated-directive": "script-src",
+ },
+ },
+ {
+ description: "Function in content script uses extension csp.",
+ pageCSP: `default-src 'self'; script-src 'self' 'unsafe-eval';`,
+ version: 3,
+ script: testFunction,
+ expect: 0,
+ },
+
+ // The javascript url tests are not included as we do not execute those,
+ // aparently even with the urlbar filtering pref flipped.
+ // (browser.urlbar.filter.javascript)
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=866522
+
+ // script tag injection tests
+ {
+ description: "remote script in content script passes in v2",
+ version: 2,
+ pageCSP: "script-src http://example.com:*;",
+ script: testScriptTag,
+ data: { url: `${BASE_URL}/data/file_script_good.js` },
+ expect: true,
+ },
+ {
+ description: "remote script in content script fails in v3",
+ version: 3,
+ pageCSP: "script-src http://example.com:*;",
+ script: testScriptTag,
+ data: { url: `${BASE_URL}/data/file_script_good.js` },
+ expect: false,
+ },
+ {
+ description: "content.WebSocket in content script is affected by page csp.",
+ version: 2,
+ pageCSP: `upgrade-insecure-requests;`,
+ data: { content: true, url: "ws://example.com/ws_dummy" },
+ script: testWebSocketUpgraded,
+ expect: "wss:", // we expect the websocket to be upgraded.
+ backgroundScript: webSocketUpgradeListenerBackground,
+ },
+ {
+ description: "WebSocket in content script is not affected by page csp.",
+ version: 2,
+ pageCSP: `upgrade-insecure-requests;`,
+ data: { url: "ws://example.com/ws_dummy" },
+ script: testWebSocketUpgraded,
+ expect: "ws:", // we expect the websocket to not be upgraded.
+ backgroundScript: webSocketUpgradeListenerBackground,
+ },
+ {
+ description: "WebSocket in content script is not affected by page csp. v3",
+ version: 3,
+ pageCSP: `upgrade-insecure-requests;`,
+ data: { url: "ws://example.com/ws_dummy" },
+ script: testWebSocketUpgraded,
+ // TODO bug 1766813: MV3+WebSocket should use content script CSP.
+ expect: "wss:", // TODO: we expect the websocket to not be upgraded (ws:).
+ backgroundScript: webSocketUpgradeListenerBackground,
+ },
+ {
+ description: "Http request in content script is not affected by page csp.",
+ version: 2,
+ pageCSP: `upgrade-insecure-requests;`,
+ data: { url: "http://example.com/plain.html" },
+ script: testHttpRequestUpgraded,
+ expect: "http:", // we expect the request to not be upgraded.
+ },
+ {
+ description:
+ "Http request in content script is not affected by page csp. v3",
+ version: 3,
+ pageCSP: `upgrade-insecure-requests;`,
+ data: { url: "http://example.com/plain.html" },
+ script: testHttpRequestUpgraded,
+ // TODO bug 1766813: MV3+fetch should use content script CSP.
+ expect: "https:", // TODO: we expect the request to not be upgraded (http:).
+ },
+ {
+ description: "content.fetch in content script is affected by page csp.",
+ version: 2,
+ pageCSP: `upgrade-insecure-requests;`,
+ data: { content: true, url: "http://example.com/plain.html" },
+ script: testHttpRequestUpgraded,
+ expect: "https:", // we expect the request to be upgraded.
+ },
+];
+
+async function runCSPTest(test) {
+ // Set the CSP for the page loaded into the tab.
+ gCSP = `${test.pageCSP || gDefaultCSP} report-uri ${CSP_REPORT_PATH}`;
+ let data = {
+ manifest: {
+ manifest_version: test.version || 2,
+ content_scripts: [
+ {
+ matches: ["http://*/plain.html"],
+ run_at: "document_idle",
+ js: ["content_script.js"],
+ },
+ ],
+ permissions: ["webRequest", "webRequestBlocking"],
+ host_permissions: ["<all_urls>"],
+ granted_host_permissions: true,
+ background: { scripts: ["background.js"] },
+ },
+ temporarilyInstalled: true,
+ files: {
+ "content_script.js": `
+ (${contentScript})(${JSON.stringify(test.report)}).then(() => {
+ browser.test.sendMessage("violationEvent");
+ });
+ (${test.script})(${JSON.stringify(test.data)}).then(result => {
+ if(result !== undefined) {
+ browser.test.sendMessage("result", result);
+ }
+ });
+ `,
+ "background.js": `(${test.backgroundScript || (() => {})})()`,
+ ...test.files,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(data);
+ await extension.startup();
+
+ let reportPromise = test.report && promiseCSPReport();
+ let contentPage = await ExtensionTestUtils.loadContentPage(pageURL);
+
+ info(`running: ${test.description}`);
+ await extension.awaitMessage("violationEvent");
+
+ let result = await extension.awaitMessage("result");
+ equal(result, test.expect, test.description);
+
+ if (test.report) {
+ let report = await reportPromise;
+ for (let key of Object.keys(test.report)) {
+ equal(
+ report["csp-report"][key],
+ test.report[key],
+ `csp-report ${key} matches`
+ );
+ }
+ }
+
+ await extension.unload();
+ await contentPage.close();
+ clearCache();
+}
+
+add_task(async function test_contentscript_csp() {
+ for (let test of TESTS) {
+ await runCSPTest(test);
+ }
+});