Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in LocalNetworkAccess
DescriptionPolicy bypass in LocalNetworkAccess
ComponentLocalNetworkAccess
Bug ClassLogic Error
Tracker454162508
Fix commitffbeba922379 (chromium/src) +248/-56
CISA KEVNot listed
Creditedasnine
Disclosed2026-04-07

Changed Functions

FunctionChangeNotes
LocalNetworkAccessPoliciesBrowserTest
chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
modified
LocalNetworkAccessWorkersBrowserTest
chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
modified
LocalNetworkAccessWorkersWebTransportBrowserTest
chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
modified

Files Changed

  • chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
  • chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
  • chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html
From ffbeba92237994d1bad06c7a13138620b0246f68 Mon Sep 17 00:00:00 2001
From: Hubert Chao <[email protected]>
Date: Thu, 26 Feb 2026 14:22:40 -0800
Subject: [PATCH] [LNA] Enforce LNA checks on Service Worker's WindowClient.navigate

Change WindowClient.navigate calls
(https://developer.mozilla.org/en-US/docs/Web/API/WindowClient/navigate)
to properly set the initiator to be the WindowClient so that initiator
client security state is used for LNA checks.

Added test, which involved refactoring some test files.

Bypass-Check-License: moved files
Bug: 454162508
Change-Id: I735a0b3c35b901da688bbb2398fb3f567de64510
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7535636
Reviewed-by: Avi Drissman <[email protected]>
Commit-Queue: Hubert Chao <[email protected]>
Reviewed-by: Yoshisato Yanagisawa <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1591100}
---

diff --git a/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc b/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
index cfdeca9..74c562e 100644
--- a/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
+++ b/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
@@ -30,7 +30,7 @@
     "/local_network_access/fetch-from-shared-worker-as-public-address.html";
 
 constexpr char kServiceWorkerHtmlPath[] =
-    "/local_network_access/fetch-from-service-worker-as-public-address.html";
+    "/local_network_access/request-from-service-worker-as-public-address.html";
 
 class LocalNetworkAccessPoliciesBrowserTest
     : public LocalNetworkAccessBrowserTestBase {};
diff --git a/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc b/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
index 40e8493e..11543754 100644
--- a/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
+++ b/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
@@ -11,6 +11,7 @@
 #include "components/metrics/content/subprocess_metrics_provider.h"
 #include "components/permissions/permission_request_manager.h"
 #include "components/policy/policy_constants.h"
+#include "content/common/features.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
@@ -54,10 +55,14 @@
     "/local_network_access/fetch-from-shared-worker-as-public-address.html";
 
 constexpr char kServiceWorkerHtmlPath[] =
-    "/local_network_access/fetch-from-service-worker-as-public-address.html";
+    "/local_network_access/request-from-service-worker-as-public-address.html";
 
 class LocalNetworkAccessWorkersBrowserTest
-    : public LocalNetworkAccessBrowserTestBase {};
+    : public LocalNetworkAccessBrowserTestBase {
+ private:
+  base::test::ScopedFeatureList feature_list_{
+      features::kServiceWorkerWindowClientInitiator};
+};
 
 class LocalNetworkAccessWorkersWebTransportBrowserTest
     : public LocalNetworkAccessBrowserTestBase {
@@ -72,7 +77,8 @@
 
  private:
   base::test::ScopedFeatureList feature_list_{
-      network::features::kLocalNetworkAccessChecksWebTransport};
+      network::features::kLocalNetworkAccessChecksWebTransport,
+  };
   content::WebTransportSimpleTestServer server_;
 };
 
@@ -238,6 +244,107 @@
                             content::JsReplace(script_template, fetch_url)));
 }
 
+// Regression tests for crbug.com/454162508
+IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
+                       ServiceWorkerWindowClientNavigateFail) {
+  // Because the navigate happens with the window client as the initiator, a
+  // permission prompt is triggered. Have the permission prompt deny the
+  // permission.
+  bubble_factory()->set_response_type(
+      permissions::PermissionRequestManager::AutoResponseType::DENY_ALL);
+
+  GURL initial_url = https_public_server().GetURL(
+      "a.com", "/local_network_access/no-favicon-treat-as-public-address.html");
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
+
+  GURL nav_url = https_server().GetURL("c.com", kLnaPath);
+  GURL iframe_url = https_public_server().GetURL(
+      "b.com", std::string(kServiceWorkerHtmlPath) + "?url=" + nav_url.spec() +
+                   "&method=navigate");
+
+  content::TestNavigationManager iframe_url_nav_manager(web_contents(),
+                                                        iframe_url);
+  content::TestNavigationManager nav_url_nav_manager(web_contents(), nav_url);
+  std::string_view script_template = R"(
+    const child = document.createElement("iframe");
+    child.src = $1;
+    child.allow = "local-network-access";
+    document.body.appendChild(child);
+  )";
+
+  EXPECT_THAT(content::EvalJs(web_contents(),
+                              content::JsReplace(script_template, iframe_url)),
+              content::EvalJsResult::IsOk());
+  // Check that the child iframe was successfully fetched.
+  ASSERT_TRUE(iframe_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(iframe_url_nav_manager.was_successful());
+
+  // Fail navigation through windowclient.navigate
+  ASSERT_TRUE(nav_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_FALSE(nav_url_nav_manager.was_successful());
+}
+
+// Regression tests for crbug.com/454162508
+IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
+                       ServiceWorkerWindowClientNavigateSuccess) {
+  // Because the navigate happens with the window client as the initiator, a
+  // permission prompt is triggered. Have the permission prompt accept the
+  // permission.
+  bubble_factory()->set_response_type(
+      permissions::PermissionRequestManager::AutoResponseType::ACCEPT_ALL);
+
+  GURL initial_url = https_public_server().GetURL(
+      "a.com", "/local_network_access/no-favicon-treat-as-public-address.html");
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
+
+  GURL nav_url = https_server().GetURL("c.com", kLnaPath);
+  GURL iframe_url = https_public_server().GetURL(
+      "b.com", std::string(kServiceWorkerHtmlPath) + "?url=" + nav_url.spec() +
+                   "&method=navigate");
+
+  content::TestNavigationManager iframe_url_nav_manager(web_contents(),
+                                                        iframe_url);
+  content::TestNavigationManager nav_url_nav_manager(web_contents(), nav_url);
+  std::string_view script_template = R"(
+    const child = document.createElement("iframe");
+    child.src = $1;
+    child.allow = "local-network-access";
+    document.body.appendChild(child);
+  )";
+
+  EXPECT_THAT(content::EvalJs(web_contents(),
+                              content::JsReplace(script_template, iframe_url)),
+              content::EvalJsResult::IsOk());
+  // Check that the child iframe was successfully fetched.
+  ASSERT_TRUE(iframe_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(iframe_url_nav_manager.was_successful());
+
+  // Navigation through windowclient.navigate should succeed.
+  ASSERT_TRUE(nav_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(nav_url_nav_manager.was_successful());
+}
+
+// Regression tests for crbug.com/454162508
+IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
+                       ServiceWorkerWindowClientNavigateMainFrame) {
+  // Permission prompt shouldn't be triggered since this is a main frame
+  // navigation. Reject all permissions in case we do get a permission prompt so
+  // test fails quickly.
+  bubble_factory()->set_response_type(
+      permissions::PermissionRequestManager::AutoResponseType::DENY_ALL);
+
+  GURL nav_url = https_server().GetURL("c.com", kLnaPath);
+  GURL initial_url = https_public_server().GetURL(
+      "b.com", std::string(kServiceWorkerHtmlPath) + "?url=" + nav_url.spec() +
+                   "&method=navigate");
+  content::TestNavigationManager nav_url_nav_manager(web_contents(), nav_url);
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
+
+  // Main frame navigation through windowclient.navigate should succeed.
+  ASSERT_TRUE(nav_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(nav_url_nav_manager.was_successful());
+}
+
 IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
                        SharedWorkerDenyPermission) {
   ASSERT_TRUE(content::NavigateToURL(
diff --git a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html b/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html
deleted file mode 100644
index c12df34..0000000
--- a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<script>
-async function setup() {
-  await navigator.serviceWorker.register('fetch-from-service-worker-as-public-address.js');
-  await navigator.serviceWorker.ready;
-  return 'ready';
-}
-
-function fetch_from_service_worker(url) {
-  return new Promise(async resolve => {
-    const registration = await navigator.serviceWorker.ready;
-    const channel = new MessageChannel();
-    channel.port1.onmessage = e => { resolve(e.data); };
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc b/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
index cfdeca9..74c562e 100644
--- a/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
+++ b/chrome/browser/local_network_access/local_network_access_policies_browsertest.cc
@@ -30,7 +30,7 @@
     "/local_network_access/fetch-from-shared-worker-as-public-address.html";
 
 constexpr char kServiceWorkerHtmlPath[] =
-    "/local_network_access/fetch-from-service-worker-as-public-address.html";
+    "/local_network_access/request-from-service-worker-as-public-address.html";
 
 class LocalNetworkAccessPoliciesBrowserTest
     : public LocalNetworkAccessBrowserTestBase {};
diff --git a/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc b/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
index 40e8493e..11543754 100644
--- a/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
+++ b/chrome/browser/local_network_access/local_network_access_workers_browsertest.cc
@@ -11,6 +11,7 @@
 #include "components/metrics/content/subprocess_metrics_provider.h"
 #include "components/permissions/permission_request_manager.h"
 #include "components/policy/policy_constants.h"
+#include "content/common/features.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
@@ -54,10 +55,14 @@
     "/local_network_access/fetch-from-shared-worker-as-public-address.html";
 
 constexpr char kServiceWorkerHtmlPath[] =
-    "/local_network_access/fetch-from-service-worker-as-public-address.html";
+    "/local_network_access/request-from-service-worker-as-public-address.html";
 
 class LocalNetworkAccessWorkersBrowserTest
-    : public LocalNetworkAccessBrowserTestBase {};
+    : public LocalNetworkAccessBrowserTestBase {
+ private:
+  base::test::ScopedFeatureList feature_list_{
+      features::kServiceWorkerWindowClientInitiator};
+};
 
 class LocalNetworkAccessWorkersWebTransportBrowserTest
     : public LocalNetworkAccessBrowserTestBase {
@@ -72,7 +77,8 @@
 
  private:
   base::test::ScopedFeatureList feature_list_{
-      network::features::kLocalNetworkAccessChecksWebTransport};
+      network::features::kLocalNetworkAccessChecksWebTransport,
+  };
   content::WebTransportSimpleTestServer server_;
 };
 
@@ -238,6 +244,107 @@
                             content::JsReplace(script_template, fetch_url)));
 }
 
+// Regression tests for crbug.com/454162508
+IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
+                       ServiceWorkerWindowClientNavigateFail) {
+  // Because the navigate happens with the window client as the initiator, a
+  // permission prompt is triggered. Have the permission prompt deny the
+  // permission.
+  bubble_factory()->set_response_type(
+      permissions::PermissionRequestManager::AutoResponseType::DENY_ALL);
+
+  GURL initial_url = https_public_server().GetURL(
+      "a.com", "/local_network_access/no-favicon-treat-as-public-address.html");
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
+
+  GURL nav_url = https_server().GetURL("c.com", kLnaPath);
+  GURL iframe_url = https_public_server().GetURL(
+      "b.com", std::string(kServiceWorkerHtmlPath) + "?url=" + nav_url.spec() +
+                   "&method=navigate");
+
+  content::TestNavigationManager iframe_url_nav_manager(web_contents(),
+                                                        iframe_url);
+  content::TestNavigationManager nav_url_nav_manager(web_contents(), nav_url);
+  std::string_view script_template = R"(
+    const child = document.createElement("iframe");
+    child.src = $1;
+    child.allow = "local-network-access";
+    document.body.appendChild(child);
+  )";
+
+  EXPECT_THAT(content::EvalJs(web_contents(),
+                              content::JsReplace(script_template, iframe_url)),
+              content::EvalJsResult::IsOk());
+  // Check that the child iframe was successfully fetched.
+  ASSERT_TRUE(iframe_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(iframe_url_nav_manager.was_successful());
+
+  // Fail navigation through windowclient.navigate
+  ASSERT_TRUE(nav_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_FALSE(nav_url_nav_manager.was_successful());
+}
+
+// Regression tests for crbug.com/454162508
+IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
+                       ServiceWorkerWindowClientNavigateSuccess) {
+  // Because the navigate happens with the window client as the initiator, a
+  // permission prompt is triggered. Have the permission prompt accept the
+  // permission.
+  bubble_factory()->set_response_type(
+      permissions::PermissionRequestManager::AutoResponseType::ACCEPT_ALL);
+
+  GURL initial_url = https_public_server().GetURL(
+      "a.com", "/local_network_access/no-favicon-treat-as-public-address.html");
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
+
+  GURL nav_url = https_server().GetURL("c.com", kLnaPath);
+  GURL iframe_url = https_public_server().GetURL(
+      "b.com", std::string(kServiceWorkerHtmlPath) + "?url=" + nav_url.spec() +
+                   "&method=navigate");
+
+  content::TestNavigationManager iframe_url_nav_manager(web_contents(),
+                                                        iframe_url);
+  content::TestNavigationManager nav_url_nav_manager(web_contents(), nav_url);
+  std::string_view script_template = R"(
+    const child = document.createElement("iframe");
+    child.src = $1;
+    child.allow = "local-network-access";
+    document.body.appendChild(child);
+  )";
+
+  EXPECT_THAT(content::EvalJs(web_contents(),
+                              content::JsReplace(script_template, iframe_url)),
+              content::EvalJsResult::IsOk());
+  // Check that the child iframe was successfully fetched.
+  ASSERT_TRUE(iframe_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(iframe_url_nav_manager.was_successful());
+
+  // Navigation through windowclient.navigate should succeed.
+  ASSERT_TRUE(nav_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(nav_url_nav_manager.was_successful());
+}
+
+// Regression tests for crbug.com/454162508
+IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
+                       ServiceWorkerWindowClientNavigateMainFrame) {
+  // Permission prompt shouldn't be triggered since this is a main frame
+  // navigation. Reject all permissions in case we do get a permission prompt so
+  // test fails quickly.
+  bubble_factory()->set_response_type(
+      permissions::PermissionRequestManager::AutoResponseType::DENY_ALL);
+
+  GURL nav_url = https_server().GetURL("c.com", kLnaPath);
+  GURL initial_url = https_public_server().GetURL(
+      "b.com", std::string(kServiceWorkerHtmlPath) + "?url=" + nav_url.spec() +
+                   "&method=navigate");
+  content::TestNavigationManager nav_url_nav_manager(web_contents(), nav_url);
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
+
+  // Main frame navigation through windowclient.navigate should succeed.
+  ASSERT_TRUE(nav_url_nav_manager.WaitForNavigationFinished());
+  EXPECT_TRUE(nav_url_nav_manager.was_successful());
+}
+
 IN_PROC_BROWSER_TEST_F(LocalNetworkAccessWorkersBrowserTest,
                        SharedWorkerDenyPermission) {
   ASSERT_TRUE(content::NavigateToURL(
diff --git a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html b/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html
deleted file mode 100644
index c12df34..0000000
--- a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<script>
-async function setup() {
-  await navigator.serviceWorker.register('fetch-from-service-worker-as-public-address.js');
-  await navigator.serviceWorker.ready;
-  return 'ready';
-}
-
-function fetch_from_service_worker(url) {
-  return new Promise(async resolve => {
-    const registration = await navigator.serviceWorker.ready;
-    const channel = new MessageChannel();
-    channel.port1.onmessage = e => { resolve(e.data); };
-    registration.active.postMessage({url}, [channel.port2]);
-  });
-}
-</script>
diff --git a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html.mock-http-headers b/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html.mock-http-headers
deleted file mode 100644
index 374554ae1..0000000
--- a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.html.mock-http-headers
+++ /dev/null
@@ -1,2 +0,0 @@
-HTTP/1.1 200 OK
-Content-Security-Policy: treat-as-public-address
diff --git a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.js b/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.js
deleted file mode 100644
index cedc135a..0000000
--- a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright 2025 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-async function handleMessage(e) {
-  try {
-    const response = await fetch(e.data.url);
-    if (!response.ok) {
-      e.ports[0].postMessage('bad response');
-      return;
-    }
-    const text = await response.text();
-    e.ports[0].postMessage(text);
-  } catch (error) {
-    e.ports[0].postMessage(`${error}`);
-  }
-}
-
-self.addEventListener('message', e => {
-  e.waitUntil(handleMessage(e));
-});
diff --git a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.js.mock-http-headers b/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.js.mock-http-headers
deleted file mode 100644
index 56a3564c..0000000
--- a/chrome/test/data/local_network_access/fetch-from-service-worker-as-public-address.js.mock-http-headers
+++ /dev/null
@@ -1,3 +0,0 @@
-HTTP/1.1 200 OK
-Content-Type: application/javascript
-Content-Security-Policy: treat-as-public-address
diff --git a/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.html b/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.html
new file mode 100644
index 0000000..446a2bf7
--- /dev/null
+++ b/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.html
@@ -0,0 +1,30 @@
+<script>
+async function setup() {
+  await navigator.serviceWorker.register('request-from-service-worker-as-public-address.js');
+  await navigator.serviceWorker.ready;
+  return 'ready';
+}
+
+function service_worker_request(url, method) {
+  return new Promise(async resolve => {
+    const registration = await navigator.serviceWorker.ready;
+    const channel = new MessageChannel();
+    channel.port1.onmessage = e => { resolve(e.data); };
+    registration.active.postMessage({url, method}, [channel.port2]);
+  });
+}
+
+function fetch_from_service_worker(url) {
+  return service_worker_request(url, 'fetch');
+}
+
+const searchParams = new URLSearchParams(window.location.search);
+
+if (searchParams.has('url')) {
+  const method = searchParams.has('method') ? searchParams.get('method') : 'fetch';
+  setup();
+  console.log(searchParams.get('url'));
+  console.log(method);
+  service_worker_request(searchParams.get('url'), method);
+}
+</script>
diff --git a/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.html.mock-http-headers b/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.html.mock-http-headers
new file mode 100644
index 0000000..374554ae1
--- /dev/null
+++ b/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.html.mock-http-headers
@@ -0,0 +1,2 @@
+HTTP/1.1 200 OK
+Content-Security-Policy: treat-as-public-address
diff --git a/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.js b/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.js
new file mode 100644
index 0000000..cd362beb
--- /dev/null
+++ b/chrome/test/data/local_network_access/request-from-service-worker-as-public-address.js
@@ -0,0 +1,55 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+async function handleFetch(e) {
+  try {
+    const response = await fetch(e.data.url);
+    if (!response.ok) {
+      e.ports[0].postMessage('bad response');
+      return;
+    }
+    const text = await response.text();
+    e.ports[0].postMessage(text);
+  } catch (error) {
+    e.ports[0].postMessage(`${error}`);
+  }
+}
+
+async function handleNavigate(e) {
+  try {
+    const navigation =
+        clients.matchAll({type: 'window'}).then(function(clientList) {
+          if (clientList.length === 0) {
+            e.ports[0].postMessage('Error: no clients');
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Service Worker Navigation bypass LNA checks for localhost

VULNERABILITY DETAILS Chromium has a Local Network Access feature that displays a PNA bubble to alert users when a webpage attempts to access private addresses. However, this security restriction can be bypassed by using the Service Worker navigation feature.

VERSION Chromium 143.0.7469.0 (Developer Build) (64-bit) OS Windows 11 Version 25H2 (Build 26200.6584)

REPRODUCTION CASE

  1. put the html/js into a online webserver https://domainxxx.com/
  2. visit https://lna-testing.notyetsecure.com/ , select “Subframe loading” to load the url(https://xxxxx.com/poc.html) in frame

Result: The iframe navitaor to the http://locahost/xxxxx

The Service Worker Navigation function will bypass the PNA feature.

PS: If you change the URL redirect method in HTML to location.href = “http://localhost/xxxx”, then the Chrome browser will pop up the PNA permission bubble. PS: Firefox can block the request and pop up the PNA permission bubble.

View on issue tracker