Firefox · DOM
CVE-2026-92039
Logic Error in DOM
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnetwerk/base/LNAPermissionRequest.cpp |
modified | |
ifnetwerk/base/nsNetUtil.cpp |
modified | |
ifnetwerk/protocol/http/nsHttpChannel.cpp |
modified | |
add_taskdom/notification/test/mochitest/test_notification_icon_csp.html |
modified | |
add_setupnetwerk/test/browser/browser_test_local_network_access_notification.js |
modified | |
add_tasknetwerk/test/browser/browser_test_local_network_access_notification.js |
modified |
Files Changed
dom/notification/test/mochitest/file_notification_icon_csp.htmldom/notification/test/mochitest/file_notification_icon_csp_self.htmldom/notification/test/mochitest/mochitest.tomldom/notification/test/mochitest/test_notification_icon_csp.htmlnetwerk/base/LNAPermissionRequest.cppnetwerk/base/nsNetUtil.cppnetwerk/base/nsNetUtil.hnetwerk/protocol/http/nsHttpChannel.cppnetwerk/test/browser/browser.tomlnetwerk/test/browser/browser_test_local_network_access_notification.jsnetwerk/test/browser/page_notification_icon.html
Patch
diff --git a/netwerk/base/LNAPermissionRequest.cpp b/netwerk/base/LNAPermissionRequest.cpp
index 25001ee56bf..35fa63ea9b1 100644
--- a/netwerk/base/LNAPermissionRequest.cpp
+++ b/netwerk/base/LNAPermissionRequest.cpp
@@ -15,6 +15,7 @@
#include "nsContentUtils.h"
#include "nsGlobalWindowInner.h"
#include "nsIConsoleService.h"
+#include "nsIContentPolicy.h"
#include "nsIIOService.h"
#include "nsIOService.h"
#include "nsIPermissionManager.h"
@@ -215,15 +216,22 @@ nsresult LNAPermissionRequest::RequestPermission() {
// For shared and service workers, do not show a permission prompt.
// Only grant access if the origin already has a persistent LNA permission.
+ //
+ // Notification icons are fetched without a requesting node, and possibly
+ // long after the page that created the notification is gone, so there is no
+ // context to prompt in either.
+ bool isNotificationIcon = mLoadInfo->InternalContentPolicyType() ==
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE_NOTIFICATION;
Maybe<dom::ClientInfo> clientInfo = mLoadInfo->GetClientInfo();
- if (clientInfo.isSome() &&
- (clientInfo->Type() == dom::ClientType::Sharedworker ||
- clientInfo->Type() == dom::ClientType::Serviceworker)) {
+ if (isNotificationIcon ||
+ (clientInfo.isSome() &&
+ (clientInfo->Type() == dom::ClientType::Sharedworker ||
+ clientInfo->Type() == dom::ClientType::Serviceworker))) {
nsCOMPtr<nsIPermissionManager> permMgr =
mozilla::components::PermissionManager::Service();
if (!permMgr || !mPrincipal) {
NS_WARNING(
- "LNA worker permission check failed: no permission manager or "
+ "LNA non-prompting permission check failed: no permission manager or "
"principal");
return Cancel();
}
@@ -234,14 +242,20 @@ nsresult LNAPermissionRequest::RequestPermission() {
return Allow(JS::UndefinedHandleValue);
}
// Log the denial to the browser console so developers can diagnose why
- // worker fetch requests are being blocked.
+ // these requests are being blocked.
nsCOMPtr<nsIConsoleService> console =
do_GetService(NS_CONSOLESERVICE_CONTRACTID);
if (console && mPrincipal) {
nsAutoCString origin;
mPrincipal->GetOrigin(origin);
nsAutoString msg;
- msg.AppendLiteral("Local Network Access blocked: worker from origin ");
+ msg.AppendLiteral("Local Network Access blocked: ");
+ if (isNotificationIcon) {
+ msg.AppendLiteral("notification icon load");
+ } else {
+ msg.AppendLiteral("worker");
+ }
+ msg.AppendLiteral(" from origin ");
msg.Append(NS_ConvertUTF8toUTF16(origin));
msg.AppendLiteral(" attempted ");
msg.Append(NS_ConvertUTF8toUTF16(mType));
diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp
index c182ce1d7f2..0ffd7651d2d 100644
--- a/netwerk/base/nsNetUtil.cpp
+++ b/netwerk/base/nsNetUtil.cpp
@@ -27,7 +27,9 @@
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/TaskQueue.h"
#include "mozilla/dom/BlobURLProtocolHandler.h"
+#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/Document.h"
+#include "mozilla/dom/PolicyContainer.h"
#include "mozilla/dom/nsCSPUtils.h"
#include "mozilla/dom/nsHTTPSOnlyUtils.h"
#include "mozilla/dom/nsMixedContentBlocker.h"
@@ -4303,6 +4305,31 @@ nsresult AddExtraHeaders(nsIHttpChannel* aHttpChannel,
return NS_OK;
}
+nsILoadInfo::IPAddressSpace GetParentIPAddressSpace(nsILoadInfo* aLoadInfo) {
+ MOZ_ASSERT(aLoadInfo);
+
+ RefPtr<mozilla::dom::BrowsingContext> bc;
+ aLoadInfo->GetBrowsingContext(getter_AddRefs(bc));
+ if (bc) {
+ return bc->GetCurrentIPAddressSpace();
+ }
+
+ // Loads that are not tied to a browsing context (worker requests, and
+ // notification icon loads which have no requesting node) read the address
+ // space from the policy container propagated from the parent document.
+ nsCOMPtr<nsIPolicyContainer> policyContainer =
+ aLoadInfo->GetPolicyContainer();
+ if (policyContainer) {
+ nsILoadInfo::IPAddressSpace addressSpace =
+ PolicyContainer::Cast(policyContainer)->GetIPAddressSpace();
+ if (addressSpace != nsILoadInfo::Unknown) {
+ return addressSpace;
+ }
+ }
+
+ return aLoadInfo->GetParentIpAddressSpace();
+}
+
bool IsLocalHostAccess(
const nsILoadInfo::IPAddressSpace aParentIPAddressSpace,
const nsILoadInfo::IPAddressSpace aTargetIPAddressSpace) {
diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h
index 7b332efa051..c6614a657df 100644
--- a/netwerk/base/nsNetUtil.h
+++ b/netwerk/base/nsNetUtil.h
@@ -1204,6 +1204,10 @@ void ParseSimpleURISchemes(const nsACString& schemeList);
nsresult AddExtraHeaders(nsIHttpChannel* aHttpChannel,
const nsACString& aExtraHeaders, bool aMerge = true);
+// Returns the IP address space the load is initiated from, which is what a
+// Local Network Access check compares the target address space against.
+nsILoadInfo::IPAddressSpace GetParentIPAddressSpace(nsILoadInfo* aLoadInfo);
+
bool IsLocalOrPrivateNetworkAccess(
nsILoadInfo::IPAddressSpace aParentIPAddressSpace,
nsILoadInfo::IPAddressSpace aTargetIPAddressSpace);
diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp
index b9464cb2189..fe50944ebbb 100644
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -2209,22 +2209,7 @@ nsresult nsHttpChannel::InitTransaction() {
mLoadInfo->GetBrowsingContext(getter_AddRefs(bc));
nsILoadInfo::IPAddressSpace parentAddressSpace =
- nsILoadInfo::IPAddressSpace::Unknown;
- // For worker-initiated requests, read IP address space from the policy
- // container which carries the parent document's address space.
- Maybe<dom::ClientInfo> clientInfo = mLoadInfo->GetClientInfo();
- if (clientInfo.isSome() && clientInfo->Type() != dom::ClientType::Window) {
- nsCOMPtr<nsIPolicyContainer> policyContainer =
- mLoadInfo->GetPolicyContainer();
- if (policyContainer) {
- parentAddressSpace =
- PolicyContainer::Cast(policyContainer)->GetIPAddressSpace();
- }
- } else if (!bc) {
- parentAddressSpace = mLoadInfo->GetParentIpAddressSpace();
- } else {
- parentAddressSpace = bc->GetCurrentIPAddressSpace();
- }
+ mozilla::net::GetParentIPAddressSpace(mLoadInfo);
// Check if this is a top-level navigation load and grant LNA permissions
// to skip local network access verification for navigational loads
@@ -10044,20 +10029,7 @@ static void RecordLNATelemetry(nsHttpChannel* aChannel, bool aLoadSuccess) {
loadInfo->GetBrowsingContext(getter_AddRefs(bc));
nsILoadInfo::IPAddressSpace parentAddressSpace =
- nsILoadInfo::IPAddressSpace::Unknown;
- Maybe<dom::ClientInfo> clientInfo = loadInfo->GetClientInfo();
- if (clientInfo.isSome() && clientInfo->Type() != dom::ClientType::Window) {
- nsCOMPtr<nsIPolicyContainer> policyContainer =
- loadInfo->GetPolicyContainer();
- if (policyContainer) {
- parentAddressSpace =
- PolicyContainer::Cast(policyContainer)->GetIPAddressSpace();
- }
- } else if (!bc) {
- parentAddressSpace = loadInfo->GetParentIpAddressSpace();
- } else {
- parentAddressSpace = bc->GetCurrentIPAddressSpace();
- }
+ mozilla::net::GetParentIPAddressSpace(loadInfo);
// Early return if NOT LNA - don't record telemetry or log
if (!mozilla::net::IsLocalOrPrivateNetworkAccess(
diff --git a/dom/notification/test/mochitest/file_notification_icon_csp.html b/dom/notification/test/mochitest/file_notification_icon_csp.html
new file mode 100644
index 00000000000..b7a4f407993
--- /dev/null
+++ b/dom/notification/test/mochitest/file_notification_icon_csp.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta http-equiv="Content-Security-Policy" content="img-src 'none'">
+<title>Document with an img-src 'none' policy</title>
diff --git a/dom/notification/test/mochitest/file_notification_icon_csp_self.html b/dom/notification/test/mochitest/file_notification_icon_csp_self.html
new file mode 100644
index 00000000000..68ee210f4b9
--- /dev/null
+++ b/dom/notification/test/mochitest/file_notification_icon_csp_self.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta http-equiv="Content-Security-Policy" content="img-src 'self'">
+<title>Document with an img-src 'self' policy</title>
diff --git a/dom/notification/test/mochitest/mochitest.toml b/dom/notification/test/mochitest/mochitest.toml
index 081b76a22be..a6f5af8dad0 100644
--- a/dom/notification/test/mochitest/mochitest.toml
+++ b/dom/notification/test/mochitest/mochitest.toml
@@ -34,6 +34,17 @@ support-files = ["blank.html"]
["test_notification_crossorigin_iframe_nested_glean.html"]
support-files = ["blank.html"]
+["test_notification_icon_csp.html"]
+skip-if = [
+ "xorigin", # Bug 1792790
+]
+support-files = [
+ "blank.html",
+ "file_notification_icon_csp.html",
+ "file_notification_icon_csp_self.html",
+ "file_pink.gif",
+]
+
["test_notification_image.html"]
skip-if = [
"xorigin", # Bug 1792790
diff --git a/dom/notification/test/mochitest/test_notification_icon_csp.html b/dom/notification/test/mochitest/test_notification_icon_csp.html
new file mode 100644
index 00000000000..0024139afb1
--- /dev/null
+++ b/dom/notification/test/mochitest/test_notification_icon_csp.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="MockAlertsService.js"></script>
+<script src="NotificationTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+
+const SAME_ORIGIN_ICON = new URL("./file_pink.gif", location.href).href;
+const CROSS_ORIGIN_ICON =
+ "https://example.org/tests/dom/notification/test/mochitest/file_pink.gif";
+
+add_setup(async () => {
+ await NotificationTest.allowNotifications();
+ await MockAlertsService.register();
+});
+
+async function getIconImage(win, icon) {
+ let notification = new win.Notification("test", { icon });
+
+ info("Waiting for notification show");
+ await new Promise(resolve => notification.addEventListener("show", resolve, {once: true}));
+
+ let ids = await MockAlertsService.getNotificationIds();
+ is(ids.length, 1, "Got one notification");
+
+ let image = await MockAlertsService.getIconImage(ids[0]);
+ await MockAlertsService.closeNotifications();
+ return image;
+}
+
+async function createIframe(src) {
+ let iframe = document.createElement("iframe");
+ iframe.src = src;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true}));
+ SimpleTest.registerCleanupFunction(() => iframe.remove());
+ return iframe.contentWindow;
+}
+
+// The icon load must be subject to the content security policy of the document
+// that created the notification, even though the image is never loaded into
+// that document.
+add_task(async function test_icon_blocked_by_document_csp() {
+ let win = await createIframe("file_notification_icon_csp.html");
+
+ is(await getIconImage(win, SAME_ORIGIN_ICON), null,
+ "img-src 'none' blocks the icon load");
+ is(await getIconImage(win, "data:image/gif;base64,R0lGODlhAQABAPAAAPIu7////yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="), null,
+ "img-src 'none' blocks a data: icon load");
+});
+
+// The policy is enforced per directive value, not as a blanket block.
+add_task(async function test_icon_self_csp() {
+ let win = await createIframe("file_notification_icon_csp_self.html");
+
+ ok(await getIconImage(win, SAME_ORIGIN_ICON),
+ "img-src 'self' allows a same-origin icon");
+ is(await getIconImage(win, CROSS_ORIGIN_ICON), null,
+ "img-src 'self' blocks a cross-origin icon");
+});
+
+// Sanity check that the same loads succeed without a policy, so that the
+// assertions above can't pass for an unrelated reason.
+add_task(async function test_icon_allowed_without_csp() {
+ let win = await createIframe("blank.html");
+
+ ok(await getIconImage(win, SAME_ORIGIN_ICON),
+ "Same-origin icon loads when the document has no policy");
+ ok(await getIconImage(win, CROSS_ORIGIN_ICON),
+ "Cross-origin icon loads when the document has no policy");
+});
+</script>
diff --git a/netwerk/test/browser/browser.toml b/netwerk/test/browser/browser.toml
index d270ab9df6d..24097aa6963 100644
--- a/netwerk/test/browser/browser.toml
+++ b/netwerk/test/browser/browser.toml
@@ -283,6 +283,12 @@ support-files = [
"page_with_non_trackers.html^headers^",
]
+["browser_test_local_network_access_notification.js"]
+support-files = [
+ "head_local_network_access.js",
+ "page_notification_icon.html",
+]
+
["browser_test_local_network_access_permissions.js"]
support-files = [
"head_local_network_access.js",
diff --git a/netwerk/test/browser/browser_test_local_network_access_notification.js b/netwerk/test/browser/browser_test_local_network_access_notification.js
new file mode 100644
index 00000000000..cc408a18fae
--- /dev/null
+++ b/netwerk/test/browser/browser_test_local_network_access_notification.js
@@ -0,0 +1,96 @@
+"use strict";
+
+// Tests that Local Network Access (LNA) checks are enforced for Notification
+// icon loads. Those loads have no requesting node, so the address space of the
+// document that created the notification is read from its policy container.
+
+Services.scriptloader.loadSubScript(
+ new URL("head_local_network_access.js", gTestPath).href,
+ this
+);
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const TEST_PAGE = `${LNA_BASE_URL}page_notification_icon.html`;
+const TEST_ORIGIN = "https://example.com";
+
+add_setup(async function () {
+ await setupLnaPrefs();
+ await setupLnaServer();
+ PermissionTestUtils.add(
+ TEST_ORIGIN,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+ registerCleanupFunction(() => {
+ PermissionTestUtils.remove(TEST_ORIGIN, "desktop-notification");
+ });
+});
+
+async function showNotificationWithIcon(rand, expectedStatus, description) {
+ const promise = observeAndCheck("img", rand, expectedStatus, description);
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [rand], async iconRand => {
+ const notification = new content.Notification("lna", {
+ icon: `http://localhost:21555/?type=img&rand=${iconRand}`,
+ });
+ // The notification is shown whether or not the icon load succeeds.
+ await new Promise(resolve =>
+ notification.addEventListener("show", resolve, { once: true })
+ );
+ notification.close();
+ });
+
+ await promise;
+ gBrowser.removeTab(tab);
+}
+
+add_task(async function test_notification_icon_denied_without_permission() {
+ Services.prefs.setCharPref(
+ "network.lna.address_space.public.override",
+ "127.0.0.1:4443"
+ );
+
+ await showNotificationWithIcon(
+ Math.random(),
+ Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
+ "Notification icon load to localhost is denied without permission"
+ );
+
+ Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
+
+add_task(async function test_notification_icon_allowed_with_permission() {
+ Services.prefs.setCharPref(
+ "network.lna.address_space.public.override",
+ "127.0.0.1:4443"
+ );
+
+ PermissionTestUtils.add(
+ TEST_ORIGIN,
+ "loopback-network",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await showNotificationWithIcon(
+ Math.random(),
+ Cr.NS_OK,
+ "Notification icon load to localhost is allowed with persistent permission"
+ );
+
+ PermissionTestUtils.remove(TEST_ORIGIN, "loopback-network");
+ Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/dom/notification/test/mochitest/file_notification_icon_csp.html b/dom/notification/test/mochitest/file_notification_icon_csp.html
new file mode 100644
index 00000000000..b7a4f407993
--- /dev/null
+++ b/dom/notification/test/mochitest/file_notification_icon_csp.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta http-equiv="Content-Security-Policy" content="img-src 'none'">
+<title>Document with an img-src 'none' policy</title>
diff --git a/dom/notification/test/mochitest/file_notification_icon_csp_self.html b/dom/notification/test/mochitest/file_notification_icon_csp_self.html
new file mode 100644
index 00000000000..68ee210f4b9
--- /dev/null
+++ b/dom/notification/test/mochitest/file_notification_icon_csp_self.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta http-equiv="Content-Security-Policy" content="img-src 'self'">
+<title>Document with an img-src 'self' policy</title>
diff --git a/dom/notification/test/mochitest/mochitest.toml b/dom/notification/test/mochitest/mochitest.toml
index 081b76a22be..a6f5af8dad0 100644
--- a/dom/notification/test/mochitest/mochitest.toml
+++ b/dom/notification/test/mochitest/mochitest.toml
@@ -34,6 +34,17 @@ support-files = ["blank.html"]
["test_notification_crossorigin_iframe_nested_glean.html"]
support-files = ["blank.html"]
+["test_notification_icon_csp.html"]
+skip-if = [
+ "xorigin", # Bug 1792790
+]
+support-files = [
+ "blank.html",
+ "file_notification_icon_csp.html",
+ "file_notification_icon_csp_self.html",
+ "file_pink.gif",
+]
+
["test_notification_image.html"]
skip-if = [
"xorigin", # Bug 1792790
diff --git a/dom/notification/test/mochitest/test_notification_icon_csp.html b/dom/notification/test/mochitest/test_notification_icon_csp.html
new file mode 100644
index 00000000000..0024139afb1
--- /dev/null
+++ b/dom/notification/test/mochitest/test_notification_icon_csp.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="MockAlertsService.js"></script>
+<script src="NotificationTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+
+const SAME_ORIGIN_ICON = new URL("./file_pink.gif", location.href).href;
+const CROSS_ORIGIN_ICON =
+ "https://example.org/tests/dom/notification/test/mochitest/file_pink.gif";
+
+add_setup(async () => {
+ await NotificationTest.allowNotifications();
+ await MockAlertsService.register();
+});
+
+async function getIconImage(win, icon) {
+ let notification = new win.Notification("test", { icon });
+
+ info("Waiting for notification show");
+ await new Promise(resolve => notification.addEventListener("show", resolve, {once: true}));
+
+ let ids = await MockAlertsService.getNotificationIds();
+ is(ids.length, 1, "Got one notification");
+
+ let image = await MockAlertsService.getIconImage(ids[0]);
+ await MockAlertsService.closeNotifications();
+ return image;
+}
+
+async function createIframe(src) {
+ let iframe = document.createElement("iframe");
+ iframe.src = src;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true}));
+ SimpleTest.registerCleanupFunction(() => iframe.remove());
+ return iframe.contentWindow;
+}
+
+// The icon load must be subject to the content security policy of the document
+// that created the notification, even though the image is never loaded into
+// that document.
+add_task(async function test_icon_blocked_by_document_csp() {
+ let win = await createIframe("file_notification_icon_csp.html");
+
+ is(await getIconImage(win, SAME_ORIGIN_ICON), null,
+ "img-src 'none' blocks the icon load");
+ is(await getIconImage(win, "data:image/gif;base64,R0lGODlhAQABAPAAAPIu7////yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="), null,
+ "img-src 'none' blocks a data: icon load");
+});
+
+// The policy is enforced per directive value, not as a blanket block.
+add_task(async function test_icon_self_csp() {
+ let win = await createIframe("file_notification_icon_csp_self.html");
+
+ ok(await getIconImage(win, SAME_ORIGIN_ICON),
+ "img-src 'self' allows a same-origin icon");
+ is(await getIconImage(win, CROSS_ORIGIN_ICON), null,
+ "img-src 'self' blocks a cross-origin icon");
+});
+
+// Sanity check that the same loads succeed without a policy, so that the
+// assertions above can't pass for an unrelated reason.
+add_task(async function test_icon_allowed_without_csp() {
+ let win = await createIframe("blank.html");
+
+ ok(await getIconImage(win, SAME_ORIGIN_ICON),
+ "Same-origin icon loads when the document has no policy");
+ ok(await getIconImage(win, CROSS_ORIGIN_ICON),
+ "Cross-origin icon loads when the document has no policy");
+});
+</script>
diff --git a/netwerk/test/browser/browser.toml b/netwerk/test/browser/browser.toml
index d270ab9df6d..24097aa6963 100644
--- a/netwerk/test/browser/browser.toml
+++ b/netwerk/test/browser/browser.toml
@@ -283,6 +283,12 @@ support-files = [
"page_with_non_trackers.html^headers^",
]
+["browser_test_local_network_access_notification.js"]
+support-files = [
+ "head_local_network_access.js",
+ "page_notification_icon.html",
+]
+
["browser_test_local_network_access_permissions.js"]
support-files = [
"head_local_network_access.js",
diff --git a/netwerk/test/browser/browser_test_local_network_access_notification.js b/netwerk/test/browser/browser_test_local_network_access_notification.js
new file mode 100644
index 00000000000..cc408a18fae
--- /dev/null
+++ b/netwerk/test/browser/browser_test_local_network_access_notification.js
@@ -0,0 +1,96 @@
+"use strict";
+
+// Tests that Local Network Access (LNA) checks are enforced for Notification
+// icon loads. Those loads have no requesting node, so the address space of the
+// document that created the notification is read from its policy container.
+
+Services.scriptloader.loadSubScript(
+ new URL("head_local_network_access.js", gTestPath).href,
+ this
+);
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const TEST_PAGE = `${LNA_BASE_URL}page_notification_icon.html`;
+const TEST_ORIGIN = "https://example.com";
+
+add_setup(async function () {
+ await setupLnaPrefs();
+ await setupLnaServer();
+ PermissionTestUtils.add(
+ TEST_ORIGIN,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+ registerCleanupFunction(() => {
+ PermissionTestUtils.remove(TEST_ORIGIN, "desktop-notification");
+ });
+});
+
+async function showNotificationWithIcon(rand, expectedStatus, description) {
+ const promise = observeAndCheck("img", rand, expectedStatus, description);
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [rand], async iconRand => {
+ const notification = new content.Notification("lna", {
+ icon: `http://localhost:21555/?type=img&rand=${iconRand}`,
+ });
+ // The notification is shown whether or not the icon load succeeds.
+ await new Promise(resolve =>
+ notification.addEventListener("show", resolve, { once: true })
+ );
+ notification.close();
+ });
+
+ await promise;
+ gBrowser.removeTab(tab);
+}
+
+add_task(async function test_notification_icon_denied_without_permission() {
+ Services.prefs.setCharPref(
+ "network.lna.address_space.public.override",
+ "127.0.0.1:4443"
+ );
+
+ await showNotificationWithIcon(
+ Math.random(),
+ Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
+ "Notification icon load to localhost is denied without permission"
+ );
+
+ Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
+
+add_task(async function test_notification_icon_allowed_with_permission() {
+ Services.prefs.setCharPref(
+ "network.lna.address_space.public.override",
+ "127.0.0.1:4443"
+ );
+
+ PermissionTestUtils.add(
+ TEST_ORIGIN,
+ "loopback-network",
+ Services.perms.ALLOW_ACTION
+ );
+
+ await showNotificationWithIcon(
+ Math.random(),
+ Cr.NS_OK,
+ "Notification icon load to localhost is allowed with persistent permission"
+ );
+
+ PermissionTestUtils.remove(TEST_ORIGIN, "loopback-network");
+ Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
+
+// Without the public override the document itself is loopback, so the icon load
+// is not a local network access and must not be affected.
+add_task(async function test_notification_icon_local_to_local_allowed() {
+ await showNotificationWithIcon(
+ Math.random(),
+ Cr.NS_OK,
+ "Notification icon load from a local document is not an LNA"
+ );
+});
diff --git a/netwerk/test/browser/page_notification_icon.html b/netwerk/test/browser/page_notification_icon.html
new file mode 100644
index 00000000000..bab0cdf861c
--- /dev/null
+++ b/netwerk/test/browser/page_notification_icon.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Notification icon LNA test</title>
+</head>
+<body></body>
+</html>
Loading diff…
References
On This Page