summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_dnr_without_webrequest.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_dnr_without_webrequest.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_dnr_without_webrequest.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_without_webrequest.js877
1 files changed, 877 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_without_webrequest.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_without_webrequest.js
new file mode 100644
index 0000000000..5a69b255d2
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_without_webrequest.js
@@ -0,0 +1,877 @@
+"use strict";
+
+// This test file verifies that the declarativeNetRequest API can modify
+// network requests as expected without the presence of the webRequest API. See
+// test_ext_dnr_webRequest.js for the interaction between webRequest and DNR.
+
+add_setup(() => {
+ Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+ Services.prefs.setBoolPref("extensions.dnr.enabled", true);
+});
+
+const server = createHttpServer({
+ hosts: ["example.com", "example.net", "example.org", "redir", "dummy"],
+});
+server.registerPathHandler("/cors_202", (req, res) => {
+ res.setStatusLine(req.httpVersion, 202, "Accepted");
+ // The extensions in this test have minimal permissions, so grant CORS to
+ // allow them to read the response without host permissions.
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Max-Age", "0");
+ res.write("cors_response");
+});
+server.registerPathHandler("/never_reached", (req, res) => {
+ Assert.ok(false, "Server should never have been reached");
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Max-Age", "0");
+});
+let gPreflightCount = 0;
+server.registerPathHandler("/preflight_count", (req, res) => {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Max-Age", "0");
+ res.setHeader("Access-Control-Allow-Methods", "NONSIMPLE");
+ if (req.method === "OPTIONS") {
+ ++gPreflightCount;
+ } else {
+ // CORS Preflight considers 2xx to be successful. To rule out inadvertent
+ // server opt-in to CORS, respond with a non-2xx response.
+ res.setStatusLine(req.httpVersion, 418, "I'm a teapot");
+ res.write(`count=${gPreflightCount}`);
+ }
+});
+server.registerPathHandler("/", (req, res) => {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Max-Age", "0");
+ res.write("Dummy page");
+});
+
+async function contentFetch(initiatorURL, url, options) {
+ let contentPage = await ExtensionTestUtils.loadContentPage(initiatorURL);
+ // Sanity check: that the initiator is as specified, and not redirected.
+ Assert.equal(
+ await contentPage.spawn([], () => content.document.URL),
+ initiatorURL,
+ `Expected document load at: ${initiatorURL}`
+ );
+ let result = await contentPage.spawn([{ url, options }], async args => {
+ try {
+ let req = await content.fetch(args.url, args.options);
+ return {
+ status: req.status,
+ url: req.url,
+ body: await req.text(),
+ };
+ } catch (e) {
+ return { error: e.message };
+ }
+ });
+ await contentPage.close();
+ return result;
+}
+
+async function checkCanFetchFromOtherExtension() {
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let req = await fetch("http://example.com/cors_202", { method: "get" });
+ browser.test.assertEq(202, req.status, "not blocked by other extension");
+ browser.test.assertEq("cors_response", await req.text());
+ browser.test.sendMessage("other_extension_done");
+ },
+ });
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("other_extension_done");
+ await otherExtension.unload();
+}
+
+add_task(async function block_request_with_dnr() {
+ async function background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ condition: { requestMethods: ["get"] },
+ action: { type: "block" },
+ },
+ {
+ id: 2,
+ condition: { requestMethods: ["head"] },
+ action: { type: "allow" },
+ },
+ ],
+ });
+ {
+ // Request not matching DNR.
+ let req = await fetch("http://example.com/cors_202", { method: "post" });
+ browser.test.assertEq(202, req.status, "allowed without DNR rule");
+ browser.test.assertEq("cors_response", await req.text());
+ }
+ {
+ // Request with "allow" DNR action.
+ let req = await fetch("http://example.com/cors_202", { method: "head" });
+ browser.test.assertEq(202, req.status, "allowed by DNR rule");
+ browser.test.assertEq("", await req.text(), "no response for HEAD");
+ }
+
+ // Request with "block" DNR action.
+ await browser.test.assertRejects(
+ fetch("http://example.com/never_reached", { method: "get" }),
+ "NetworkError when attempting to fetch resource.",
+ "blocked by DNR rule"
+ );
+
+ browser.test.sendMessage("tested_dnr_block");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ allowInsecureRequests: true,
+ background,
+ manifest: {
+ manifest_version: 3,
+ permissions: ["declarativeNetRequest"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("tested_dnr_block");
+
+ // DNR should not only work with requests within the extension, but also from
+ // web pages.
+ Assert.deepEqual(
+ await contentFetch("http://dummy/", "http://example.com/never_reached"),
+ { error: "NetworkError when attempting to fetch resource." },
+ "Blocked by DNR with declarativeNetRequestWithHostAccess"
+ );
+
+ // declarativeNetRequest does not allow extensions to block requests from
+ // other extensions.
+ await checkCanFetchFromOtherExtension();
+
+ // Except when the user opts in via a preference. When the pref is on, then:
+ // The declarativeNetRequest permission grants the ability to block requests
+ // from other extensions. (The declarativeNetRequestWithHostAccess permission
+ // does not; see test task block_with_declarativeNetRequestWithHostAccess.)
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ fetch("http://example.com/never_reached", { method: "get" }),
+ "NetworkError when attempting to fetch resource.",
+ "blocked by different extension with declarativeNetRequest permission"
+ );
+ browser.test.sendMessage("other_extension_done");
+ },
+ });
+ await runWithPrefs(
+ [["extensions.dnr.match_requests_from_other_extensions", true]],
+ async () => {
+ info("Verifying that fetch() from extension is intercepted with pref");
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("other_extension_done");
+ await otherExtension.unload();
+ }
+ );
+
+ await extension.unload();
+});
+
+// Verifies that the "declarativeNetRequestWithHostAccess" permission can only
+// block if it has permission for the initiator.
+add_task(async function block_with_declarativeNetRequestWithHostAccess() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [{ id: 1, condition: {}, action: { type: "block" } }],
+ });
+ browser.test.sendMessage("dnr_registered");
+ },
+ temporarilyInstalled: true, // Needed for granted_host_permissions
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ granted_host_permissions: true,
+ host_permissions: ["<all_urls>"],
+ permissions: ["declarativeNetRequestWithHostAccess"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("dnr_registered");
+
+ // Initiator "http://dummy" does match "<all_urls>", so DNR rule should apply.
+ Assert.deepEqual(
+ await contentFetch("http://dummy/", "http://example.com/never_reached"),
+ { error: "NetworkError when attempting to fetch resource." },
+ "Blocked by DNR with declarativeNetRequestWithHostAccess"
+ );
+
+ // Extensions cannot have permissions for another extension and therefore the
+ // DNR rule never applies.
+ await checkCanFetchFromOtherExtension();
+
+ // Sanity check: even with the pref, the declarativeNetRequestWithHostAccess
+ // permission should not grant access.
+ info("Verifying that access is not allowed, despite the pref being true");
+ await runWithPrefs(
+ [["extensions.dnr.match_requests_from_other_extensions", true]],
+ checkCanFetchFromOtherExtension
+ );
+
+ await extension.unload();
+});
+
+add_task(async function block_in_sandboxed_extension_page() {
+ const filesWithSandbox = {
+ "page_with_sandbox.html": `<!DOCTYPE html><meta charset="utf-8">
+ <script src="page_with_sandbox.js"></script>
+ <iframe src="sandbox.html" sandbox="allow-scripts"></iframe>
+ `,
+ "page_with_sandbox.js": () => {
+ // Sent by sandbox.js:
+ window.onmessage = e => {
+ browser.test.assertEq("null", e.origin, "Sender has opaque origin");
+ browser.test.sendMessage("fetch_result", e.data);
+ };
+ },
+ "sandbox.html": `<script src="sandbox.js"></script>`,
+ "sandbox.js": async () => {
+ try {
+ // Note that the test server responds with CORS headers, so we should
+ // be able to fetch this URL:
+ await fetch("http://example.com/?fetch_by_sandbox");
+ parent.postMessage("FETCH_ALLOWED", "*");
+ } catch (e) {
+ // The only way for this to fail in this test is when DNR blocks it.
+ parent.postMessage("FETCH_BLOCKED", "*");
+ }
+ },
+ };
+ async function checkFetchInSandboxedExtensionPage(ext) {
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${ext.uuid}/page_with_sandbox.html`
+ );
+ let result = await ext.awaitMessage("fetch_result");
+ await contentPage.close();
+ return result;
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [{ id: 1, condition: {}, action: { type: "block" } }],
+ });
+ browser.test.sendMessage("dnr_registered");
+ },
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ permissions: ["declarativeNetRequest"],
+ },
+ files: filesWithSandbox,
+ });
+ await extension.startup();
+ await extension.awaitMessage("dnr_registered");
+
+ Assert.equal(
+ await checkFetchInSandboxedExtensionPage(extension),
+ "FETCH_BLOCKED",
+ "DNR blocks request from sandboxed page in own extension"
+ );
+
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ files: filesWithSandbox,
+ });
+ await otherExtension.startup();
+
+ // Note: In Firefox, webRequest can intercept requests from opaque origins
+ // opened by other extensions. In contrast, that is not the case in Chrome.
+ Assert.equal(
+ await checkFetchInSandboxedExtensionPage(otherExtension),
+ "FETCH_BLOCKED",
+ "DNR can block request from sandboxed page in other extension"
+ );
+
+ await runWithPrefs(
+ [["extensions.dnr.match_requests_from_other_extensions", true]],
+ async () => {
+ info("Verifying that fetch() from extension sandbox is matched via pref");
+ Assert.equal(
+ await checkFetchInSandboxedExtensionPage(otherExtension),
+ "FETCH_BLOCKED",
+ "DNR can block request from sandboxed page in other extension via pref"
+ );
+ }
+ );
+ await extension.unload();
+
+ // As a sanity check, to verify that the tests above do not always return
+ // FETCH_BLOCKED, run a test case that returns FETCH_ALLOWED:
+ Assert.equal(
+ await checkFetchInSandboxedExtensionPage(otherExtension),
+ "FETCH_ALLOWED",
+ "DNR does not affect sandboxed extensions after unloading the DNR extension"
+ );
+
+ await otherExtension.unload();
+});
+
+// Verifies that upgradeScheme works.
+// The HttpServer helper does not support https (bug 1742061), so in this
+// test we just verify whether the upgrade has been attempted. Coverage that
+// verifies that the upgraded request completes is in:
+// toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html
+add_task(async function upgradeScheme_declarativeNetRequestWithHostAccess() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ condition: { excludedRequestDomains: ["dummy"] },
+ action: { type: "upgradeScheme" },
+ },
+ {
+ id: 2,
+ // HttpServer does not support https (bug 1742061).
+ // As a work-around, we just redirect the https:-request to http.
+ condition: { urlFilter: "|https:*" },
+ action: {
+ type: "redirect",
+ redirect: { url: "http://dummy/cors_202?from_https" },
+ },
+ // The upgradeScheme and redirect actions have equal precedence. To
+ // make sure that the redirect action is executed when both rules
+ // match, we assign a higher priority to the redirect action.
+ priority: 2,
+ },
+ ],
+ });
+
+ let req = await fetch("http://redir/never_reached");
+ browser.test.assertEq(
+ "http://dummy/cors_202?from_https",
+ req.url,
+ "upgradeScheme upgraded to https"
+ );
+ browser.test.assertEq("cors_response", await req.text());
+
+ browser.test.sendMessage("tested_dnr_upgradeScheme");
+ },
+ temporarilyInstalled: true, // Needed for granted_host_permissions.
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ granted_host_permissions: true,
+ host_permissions: ["*://dummy/*", "*://redir/*"],
+ permissions: ["declarativeNetRequestWithHostAccess"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("tested_dnr_upgradeScheme");
+
+ // Request to same-origin subresource, which should be upgraded.
+ Assert.equal(
+ (await contentFetch("http://redir/", "http://redir/never_reached")).url,
+ "http://dummy/cors_202?from_https",
+ "upgradeScheme + host access should upgrade (same-origin request)"
+ );
+
+ // Request to cross-origin subresource, which should be upgraded.
+ // Note: after the upgrade, a cross-origin redirect happens. Internally, we
+ // reflect the Origin request header in the Access-Control-Allow-Origin (ACAO)
+ // response header, to ensure that the request is accepted by CORS. See
+ // https://github.com/w3c/webappsec-upgrade-insecure-requests/issues/32
+ Assert.equal(
+ (await contentFetch("http://dummy/", "http://redir/never_reached")).url,
+ "http://dummy/cors_202?from_https",
+ "upgradeScheme + host access should upgrade (cross-origin request)"
+ );
+
+ // The DNR extension does not have example.net in host_permissions.
+ const urlNoHostPerms = "http://example.net/cors_202?missing_host_permission";
+ Assert.equal(
+ (await contentFetch("http://dummy/", urlNoHostPerms)).url,
+ urlNoHostPerms,
+ "upgradeScheme not matched when extension lacks host access"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function redirect_request_with_dnr() {
+ async function background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ condition: {
+ requestDomains: ["example.com"],
+ requestMethods: ["get"],
+ },
+ action: {
+ type: "redirect",
+ redirect: {
+ url: "http://example.net/cors_202?1",
+ },
+ },
+ },
+ {
+ id: 2,
+ // Note: extension does not have example.org host permission.
+ condition: { requestDomains: ["example.org"] },
+ action: {
+ type: "redirect",
+ redirect: {
+ url: "http://example.net/cors_202?2",
+ },
+ },
+ },
+ ],
+ });
+ // The extension only has example.com permission, but the redirects to
+ // example.net are still due to the CORS headers from the server.
+ {
+ // Simple GET request.
+ let req = await fetch("http://example.com/never_reached");
+ browser.test.assertEq(202, req.status, "redirected by DNR (simple)");
+ browser.test.assertEq("http://example.net/cors_202?1", req.url);
+ browser.test.assertEq("cors_response", await req.text());
+ }
+ {
+ // GeT request should be matched despite having a different case.
+ let req = await fetch("http://example.com/never_reached", {
+ method: "GeT",
+ });
+ browser.test.assertEq(202, req.status, "redirected by DNR (GeT)");
+ browser.test.assertEq("http://example.net/cors_202?1", req.url);
+ browser.test.assertEq("cors_response", await req.text());
+ }
+ {
+ // Host permission missing for request, request not redirected by DNR.
+ // Response is readable due to the CORS response headers from the server.
+ let req = await fetch("http://example.org/cors_202?noredir");
+ browser.test.assertEq(202, req.status, "not redirected by DNR");
+ browser.test.assertEq("http://example.org/cors_202?noredir", req.url);
+ browser.test.assertEq("cors_response", await req.text());
+ }
+
+ browser.test.notifyPass();
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ temporarilyInstalled: true, // Needed for granted_host_permissions
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ granted_host_permissions: true,
+ host_permissions: ["*://example.com/*"],
+ permissions: ["declarativeNetRequest"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+
+ let otherExtension = ExtensionTestUtils.loadExtension({
+ async background() {
+ // The DNR extension has permissions for example.com, but not for this
+ // extension. Therefore the "redirect" action should not apply.
+ let req = await fetch("http://example.com/cors_202?other_ext");
+ browser.test.assertEq(202, req.status, "not redirected by DNR");
+ browser.test.assertEq("http://example.com/cors_202?other_ext", req.url);
+ browser.test.assertEq("cors_response", await req.text());
+ browser.test.sendMessage("other_extension_done");
+ },
+ });
+ await otherExtension.startup();
+ await otherExtension.awaitMessage("other_extension_done");
+ await otherExtension.unload();
+
+ await extension.unload();
+});
+
+// Verifies that DNR redirects requiring a CORS preflight behave as expected.
+add_task(async function redirect_request_with_dnr_cors_preflight() {
+ // Most other test tasks only test requests within the test extension. This
+ // test intentionally triggers requests outside the extension, to make sure
+ // that the usual CORS mechanisms is triggered (instead of exceptions from
+ // host permissions).
+ async function background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ condition: {
+ requestDomains: ["redir"],
+ excludedRequestMethods: ["options"],
+ },
+ action: {
+ type: "redirect",
+ redirect: {
+ url: "http://example.com/preflight_count",
+ },
+ },
+ },
+ {
+ id: 2,
+ condition: {
+ requestDomains: ["example.net"],
+ excludedRequestMethods: ["nonsimple"], // note: redirects "options"
+ },
+ action: {
+ type: "redirect",
+ redirect: {
+ url: "http://example.com/preflight_count",
+ },
+ },
+ },
+ ],
+ });
+ let req = await fetch("http://redir/never_reached", {
+ method: "NONSIMPLE",
+ });
+ // Extension has permission for "redir", but not for the redirect target.
+ // The request is non-simple (see below for explanation of non-simple), so
+ // a preflight (OPTIONS) request to /preflight_count is expected before the
+ // redirection target is requested.
+ browser.test.assertEq(
+ "count=1",
+ await req.text(),
+ "Got preflight before redirect target because of missing host_permissions"
+ );
+
+ browser.test.sendMessage("continue_preflight_tests");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ temporarilyInstalled: true, // Needed for granted_host_permissions
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ granted_host_permissions: true,
+ // "redir" and "example.net" are needed to allow redirection of these.
+ // "dummy" is needed to redirect requests initiated from http://dummy.
+ host_permissions: ["*://redir/*", "*://example.net/*", "*://dummy/*"],
+ permissions: ["declarativeNetRequest"],
+ },
+ });
+ gPreflightCount = 0;
+ await extension.startup();
+ await extension.awaitMessage("continue_preflight_tests");
+ gPreflightCount = 0; // value already checked before continue_preflight_tests.
+
+ // Simple request (i.e. without preflight requirement), that's redirected to
+ // another URL by the DNR rule. The redirect should be accepted, and in
+ // particular not be blocked by the same-origin policy. The redirect target
+ // (/preflight_count) is readable due to the CORS headers from the server.
+ Assert.deepEqual(
+ await contentFetch("http://dummy/", "http://redir/never_reached"),
+ // count=0: A simple request does not trigger a preflight (OPTIONS) request.
+ { status: 418, url: "http://example.com/preflight_count", body: "count=0" },
+ "Simple request should not have a preflight."
+ );
+
+ // Any request method other than "GET", "POST" and "PUT" (e.g "NONSIMPLE") is
+ // a non-simple request that triggers a preflight request ("OPTIONS").
+ //
+ // Usually, this happens (without extension-triggered redirects):
+ // 1. NONSIMPLE /never_reached : is started, but does NOT hit the server yet.
+ // 2. OPTIONS /never_reached + Access-Control-Request-Method: NONSIMPLE
+ // 3. NONSIMPLE /never_reached : reaches the server if allowed by OPTIONS.
+ //
+ // With an extension-initiated redirect to /preflight_count:
+ // 1. NONSIMPLE /never_reached : is started, but does not hit the server yet.
+ // 2. extension redirects to /preflight_count
+ // 3. OPTIONS /preflight_count + Access-Control-Request-Method: NONSIMPLE
+ // - This is because the redirect preserves the request method/body/etc.
+ // 4. NONSIMPLE /preflight_count : reaches the server if allowed by OPTIONS.
+ Assert.deepEqual(
+ await contentFetch("http://dummy/", "http://redir/never_reached", {
+ method: "NONSIMPLE",
+ }),
+ // Due to excludedRequestMethods: ["options"], the preflight for the
+ // redirect target is not intercepted, so the server sees a preflight.
+ { status: 418, url: "http://example.com/preflight_count", body: "count=1" },
+ "Initial URL redirected, redirection target has preflight"
+ );
+ gPreflightCount = 0;
+
+ // The "example.net" rule has "excludedRequestMethods": ["nonsimple"], so the
+ // initial "NONSIMPLE" request is not immediately redirected. Therefore the
+ // preflight request happens. This OPTIONS request is matched by the DNR rule
+ // and redirected to /preflight_count. While preflight_count offers a very
+ // permissive preflight response, it is not even fetched:
+ // Only a 2xx HTTP status is considered a valid response to a pre-flight.
+ // A redirect is like a 3xx HTTP status, so the whole request is rejected,
+ // and the redirect is not followed for the OPTIONS request.
+ Assert.deepEqual(
+ await contentFetch("http://dummy/", "http://example.net/never_reached", {
+ method: "NONSIMPLE",
+ }),
+ { error: "NetworkError when attempting to fetch resource." },
+ "Redirect of preflight request (OPTIONS) should be a CORS failure"
+ );
+
+ Assert.equal(gPreflightCount, 0, "Preflight OPTIONS has been intercepted");
+
+ await extension.unload();
+});
+
+// Tests that DNR redirect rules can be chained.
+add_task(async function redirect_request_with_dnr_multiple_hops() {
+ async function background() {
+ // Set up redirects from example.com up until dummy.
+ let hosts = ["example.com", "example.net", "example.org", "redir", "dummy"];
+ let rules = [];
+ for (let i = 1; i < hosts.length; ++i) {
+ const from = hosts[i - 1];
+ const to = hosts[i];
+ const end = hosts.length - 1 === i;
+ rules.push({
+ id: i,
+ condition: { requestDomains: [from] },
+ action: {
+ type: "redirect",
+ redirect: {
+ // All intermediate redirects should never hit the server, but the
+ // last one should..
+ url: end ? `http://${to}/?end` : `http://${to}/never_reached`,
+ },
+ },
+ });
+ }
+ await browser.declarativeNetRequest.updateSessionRules({ addRules: rules });
+ let req = await fetch("http://example.com/never_reached");
+ browser.test.assertEq(200, req.status, "redirected by DNR (multiple)");
+ browser.test.assertEq("http://dummy/?end", req.url, "Last URL in chain");
+ browser.test.assertEq("Dummy page", await req.text());
+
+ browser.test.notifyPass();
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ temporarilyInstalled: true, // Needed for granted_host_permissions
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ granted_host_permissions: true,
+ host_permissions: ["*://*/*"], // matches all in the |hosts| list.
+ permissions: ["declarativeNetRequest"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+
+ // Test again, but without special extension permissions to verify that DNR
+ // redirects pass CORS checks.
+ Assert.deepEqual(
+ await contentFetch("http://dummy/", "http://redir/never_reached"),
+ { status: 200, url: "http://dummy/?end", body: "Dummy page" },
+ "Multiple redirects by DNR, requested from web origin."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function redirect_request_with_dnr_with_redirect_loop() {
+ async function background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ // requestMethods is mutually exclusive with the other rule.
+ condition: { regexFilter: "^(.+)$", requestMethods: ["post"] },
+ action: {
+ type: "redirect",
+ redirect: {
+ // Appends "?loop" to the request URL
+ regexSubstitution: "\\1?loop",
+ },
+ },
+ },
+ {
+ id: 2,
+ // requestMethods is mutually exclusive with the other rule.
+ condition: { requestDomains: ["redir"], requestMethods: ["get"] },
+ action: {
+ type: "redirect",
+ redirect: {
+ // Despite redirect.url matching the condition, the redirect loop
+ // should be caught because of the obvious fact that the URL did
+ // not change.
+ url: "http://redir/cors_202?loop",
+ },
+ },
+ },
+ ],
+ });
+
+ // Redirect where the redirect URL changes at every redirect.
+ await browser.test.assertRejects(
+ fetch("http://redir/cors_202?loop", { method: "post" }),
+ "NetworkError when attempting to fetch resource.",
+ "Redirect loop caught (redirect target differs at every redirect)"
+ );
+
+ async function assertRedirect(url, expected, description) {
+ // method: "get" could only match rule 2.
+ let res = await fetch(url);
+ browser.test.assertDeepEq(
+ expected,
+ { status: res.status, url: res.url, redirected: res.redirected },
+ description
+ );
+ }
+
+ // Redirect with initially a different URL.
+ await assertRedirect(
+ "http://redir/never_reached?",
+ { status: 202, url: "http://redir/cors_202?loop", redirected: true },
+ "Redirect loop caught (initially different URL)"
+ );
+
+ // Redirect where redirect is exactly the same URL as requested.
+ await assertRedirect(
+ "http://redir/cors_202?loop",
+ { status: 202, url: "http://redir/cors_202?loop", redirected: false },
+ "Redirect loop caught (redirect target same as initial URL)"
+ );
+
+ browser.test.notifyPass();
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ temporarilyInstalled: true, // Needed for granted_host_permissions
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ granted_host_permissions: true,
+ host_permissions: ["*://redir/*"],
+ permissions: ["declarativeNetRequest"],
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+// Tests that redirect to extensionPath works, provided that the initiator is
+// either the extension itself, or in host_permissions. Moreover, the requested
+// resource must match a web_accessible_resources entry for both the initiator
+// AND the pre-redirect URL.
+add_task(async function redirect_request_with_dnr_to_extensionPath() {
+ async function background() {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ condition: { requestDomains: ["redir"], requestMethods: ["post"] },
+ action: {
+ type: "redirect",
+ redirect: {
+ extensionPath: "/war.txt?1",
+ },
+ },
+ },
+ {
+ id: 2,
+ condition: { requestDomains: ["redir"], requestMethods: ["put"] },
+ action: {
+ type: "redirect",
+ redirect: {
+ extensionPath: "/nonwar.txt?2",
+ },
+ },
+ },
+ ],
+ });
+ {
+ let req = await fetch("http://redir/never_reached", { method: "post" });
+ browser.test.assertEq(200, req.status, "redirected to extensionPath");
+ browser.test.assertEq(`${location.origin}/war.txt?1`, req.url);
+ browser.test.assertEq("war_ext_res", await req.text());
+ }
+ // Redirects to extensionPath that is not in web_accessible_resources.
+ // While the initiator (extension) would be allowed to read the resource
+ // due to it being same-origin, the pre-redirect URL (http://redir) is not
+ // matching web_accessible_resources[].matches, so the load is rejected.
+ //
+ // This behavior differs from Chrome (e.g. at least in Chrome 109) that
+ // does allow the load to complete. Extensions who really care about
+ // exposing a web-accessible resource to the world can just put an all_urls
+ // pattern in web_accessible_resources[].matches.
+ await browser.test.assertRejects(
+ fetch("http://redir/never_reached", { method: "put" }),
+ "NetworkError when attempting to fetch resource.",
+ "Redirect to nowar.txt, but pre-redirect host is not in web_accessible_resources[].matches"
+ );
+
+ browser.test.notifyPass();
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ temporarilyInstalled: true, // Needed for granted_host_permissions
+ allowInsecureRequests: true,
+ manifest: {
+ manifest_version: 3,
+ granted_host_permissions: true,
+ host_permissions: ["*://redir/*", "*://dummy/*"],
+ permissions: ["declarativeNetRequest"],
+ web_accessible_resources: [
+ // *://redir/* is in matches, because that is the pre-redirect host.
+ // *://dummy/* is in matches, because that is an initiator below.
+ { resources: ["war.txt"], matches: ["*://redir/*", "*://dummy/*"] },
+ // without "matches", this is almost equivalent to not being listed in
+ // web_accessible_resources at all. This entry is listed here to verify
+ // that the presence of extension_ids does not somehow allow a request
+ // with an extension initiator to complete.
+ { resources: ["nonwar.txt"], extension_ids: ["*"] },
+ ],
+ },
+ files: {
+ "war.txt": "war_ext_res",
+ "nonwar.txt": "non_war_ext_res",
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+ const extPrefix = `moz-extension://${extension.uuid}`;
+
+ // Request from origin in host_permissions, for web-accessible resource.
+ Assert.deepEqual(
+ await contentFetch(
+ "http://dummy/", // <-- Matching web_accessible_resources[].matches
+ "http://redir/never_reached", // <-- With matching host_permissions
+ { method: "post" }
+ ),
+ { status: 200, url: `${extPrefix}/war.txt?1`, body: "war_ext_res" },
+ "Should have got redirect to web_accessible_resources (war.txt)"
+ );
+
+ // Request from origin in host_permissions, for non-web-accessible resource.
+ let { messages } = await promiseConsoleOutput(async () => {
+ Assert.deepEqual(
+ await contentFetch(
+ "http://dummy/", // <-- Matching web_accessible_resources[].matches
+ "http://redir/never_reached", // <-- With matching host_permissions
+ { method: "put" }
+ ),
+ { error: "NetworkError when attempting to fetch resource." },
+ "Redirect to nowar.txt, without matching web_accessible_resources[].matches"
+ );
+ });
+ const EXPECTED_SECURITY_ERROR = `Content at http://redir/never_reached may not load or link to ${extPrefix}/nonwar.txt?2.`;
+ Assert.equal(
+ messages.filter(m => m.message.includes(EXPECTED_SECURITY_ERROR)).length,
+ 1,
+ `Should log SecurityError: ${EXPECTED_SECURITY_ERROR}`
+ );
+
+ // Request from origin not in host_permissions. DNR rule should not apply.
+ Assert.deepEqual(
+ await contentFetch(
+ "http://dummy/", // <-- Matching web_accessible_resources[].matches
+ "http://example.com/cors_202", // <-- NOT in host_permissions
+ { method: "post" }
+ ),
+ { status: 202, url: "http://example.com/cors_202", body: "cors_response" },
+ "Extension should not have redirected, due to lack of host permissions"
+ );
+
+ await extension.unload();
+});