Overview

Medium
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in AppManifest
DescriptionMissing authorization in AppManifest
ComponentAppManifest
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker511062248
Fix commit9e594d53dece (chromium/src) +70/-0
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
Creditedpakhunov.anton.n
Reported2026-05-08 (per vendor advisory)
Fix landed2026-06-03 (public git)
Disclosed2026-09-15
Patch gap104 days — fix public in source before the advisory

Background

Web App Manifest
a JSON document a page declares that describes install/launch metadata (e.g. start_url, scope), which Blink parses in the renderer and forwards to the browser process as a blink::mojom::Manifest.
`ManifestManagerHost`
the browser-side content object that receives the parsed manifest over Mojo from the renderer and is responsible for revalidating security-sensitive invariants the renderer cannot be trusted to enforce.
Same-origin validation
the rule that manifest-supplied URLs used to launch or perform actions must share the document’s origin, checked here with url::Origin::IsSameOriginWith.
Bad message / `ReportBadMessage`
the browser’s mechanism for rejecting a malformed or malicious IPC and terminating the offending renderer; the patched code signals it by returning a non-empty reason string.

Root Cause Analysis

The browser-side manifest validation path (reached via ManifestManagerHost::ValidateAndMaybeOverrideManifest, exercised by the test through ValidateAndMaybeOverrideManifestForTesting) already re-checked several manifest URL fields for same-origin/same-site correctness — for example protocol_handlers and the migrate_from entries visible in the hunk context — but it never validated manifest.note_taking->new_note_url or manifest.lock_screen->start_url. Because these fields were only constrained by the renderer-side parser, a compromised renderer could bypass the parser and send a blink::mojom::Manifest whose note_taking->new_note_url or lock_screen->start_url pointed at an arbitrary cross-origin URL (e.g. https://evil.com). The invariant violated is that action/launch URLs derived from a document’s manifest must be same-origin with that document.

The patch TIGHTENS validation: it adds two guards that, when the field is_valid() and !document_origin.IsSameOriginWith(...), return a reason string (“Manifest note_taking new_note_url must be same-origin with the document.” / “Manifest lock_screen start_url must be same-origin with the document.”), which the caller treats as a bad message and kills the renderer before the untrusted URL is ever stored or used.

The fix works because it moves the trust boundary into the browser process, so a lying renderer can no longer smuggle a cross-origin URL past the now-authoritative check.

Key insight
The single mistake was relying on the renderer’s parser as the only enforcer of same-origin for note_taking->new_note_url and lock_screen->start_url while the browser omitted these two fields from its own revalidation; the fix closes the authorization gap by adding the same browser-process same-origin checks that the other launch-related manifest fields already had.

Attack Path

  1. Compromise a renderer The attacker gains code execution in a renderer process (the standard precondition for a browser-process authorization bypass) so it can forge Mojo messages instead of going through the trusted manifest parser.
  2. Forge a cross-origin manifest field The renderer sends a blink::mojom::Manifest to ManifestManagerHost with a valid note_taking->new_note_url or lock_screen->start_url set to an off-origin URL such as https://evil.com, which the pre-patch browser accepted without a same-origin check.
  3. Bypass missing authorization Because the vulnerable path validated other fields but not these two, the cross-origin URL passed browser-side validation and would be stored/used as the document’s own note-taking or lock-screen action URL.
  4. Malicious launch/action When that manifest URL is later used to launch or perform an action, the attacker-controlled cross-origin URL is invoked in a context that assumed same-origin, enabling cross-origin action under the victim app’s authority (inferred — the patch demonstrates only the missing check, not a specific downstream sink).

Impact Assessment

An attacker who already controls a renderer can bypass a missing browser-side authorization check to associate an arbitrary cross-origin URL with a document’s note_taking or lock_screen manifest fields, defeating the same-origin invariant these launch/action URLs are supposed to hold. The precondition is a compromised or malicious renderer, and the affected trust boundary is the browser process (content/browser/manifest). Any further escalation to a concrete cross-origin compromise or spoofed app action beyond acceptance of the bad URL is (inferred); the advisory classifies this as a LogicError / missing authorization in AppManifest and does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
for
content/browser/manifest/manifest_manager_host.cc
modified

Files Changed

  • content/browser/manifest/manifest_browsertest.cc
  • content/browser/manifest/manifest_manager_host.cc

Audit Directions

  • Renderer-supplied URL fields lacking browser-side same-origin checks
    Enumerate every URL/origin field in blink::mojom::Manifest (and similar renderer-to-browser structs) and confirm each launch/action URL is revalidated in ManifestManagerHost, not just in the renderer parser.
  • Asymmetry between parser validation and browser validation
    Look for fields the Blink manifest parser constrains for same-origin/same-site that the browser process does not re-check, since the renderer half of that pair is untrustworthy once compromised.
  • New manifest fields added without a matching browser guard
    When a manifest member (e.g. note_taking, lock_screen, protocol_handlers, migrate_from) is introduced, verify the corresponding IsSameOriginWith/IsSameSiteWith guard is added to the browser-side validation list so no field is silently exempt.
From 9e594d53dececc1605a9f9778ea2152b5e163894 Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
Date: Wed, 03 Jun 2026 12:06:27 -0700
Subject: [PATCH] Validate Manifest note_taking and lock_screen same-origin in browser

This CL adds same-origin validation in the browser process (specifically
in ManifestManagerHost) for the following manifest fields:
- `note_taking->new_note_url`
- `lock_screen->start_url`

This prevents a compromised renderer from bypassing the parser and
sending cross-origin URLs for these fields, which could lead to security
issues when these URLs are used to launch or perform actions.

TAG=agy
CONV=6ebd9b15-cbef-4370-915a-a209f317b895

Bug: 511062248
Change-Id: If2f53ca6e7b2c819c76103a6055edc49eaffdb70
Fixed: 511062248
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7897641
Auto-Submit: Nate Chapin <japhet@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1641100}
---
diff --git a/content/browser/manifest/manifest_manager_host.cc b/content/browser/manifest/manifest_manager_host.cc
index eb8b136..df8b1c4 100644
--- a/content/browser/manifest/manifest_manager_host.cc
+++ b/content/browser/manifest/manifest_manager_host.cc
@@ -114,6 +114,18 @@
       }
     }
 
+    if (manifest.note_taking && manifest.note_taking->new_note_url.is_valid() &&
+        !document_origin.IsSameOriginWith(manifest.note_taking->new_note_url)) {
+      return "Manifest note_taking new_note_url must be same-origin with the "
+             "document.";
+    }
+
+    if (manifest.lock_screen && manifest.lock_screen->start_url.is_valid() &&
+        !document_origin.IsSameOriginWith(manifest.lock_screen->start_url)) {
+      return "Manifest lock_screen start_url must be same-origin with the "
+             "document.";
+    }
+
     net::SchemefulSite document_site(document_origin);
     for (const auto& migrate_from : manifest.migrate_from) {
       if (!document_site.IsSameSiteWith(migrate_from->id)) {
diff --git a/content/browser/manifest/manifest_browsertest.cc b/content/browser/manifest/manifest_browsertest.cc
index a992ddec3..a07a027 100644
--- a/content/browser/manifest/manifest_browsertest.cc
+++ b/content/browser/manifest/manifest_browsertest.cc
@@ -1218,6 +1218,64 @@
           "Manifest protocol_handlers must be same-origin with the document."));
 }
 
+IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
+                       BadMessage_NoteTakingNewNoteUrlCrossOrigin) {
+  const GURL test_url =
+      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
+  ASSERT_TRUE(NavigateToURL(shell(), test_url));
+
+  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
+      shell()->web_contents()->GetPrimaryPage());
+
+  // Test that a cross-site note_taking new_note_url triggers a bad message.
+  mojo::FakeMessageDispatchContext fake_dispatch_context;
+  auto bad_manifest = blink::mojom::Manifest::New();
+  bad_manifest->start_url = test_url;
+  bad_manifest->id = test_url;
+  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");
+
+  blink::mojom::ManifestNoteTakingPtr note_taking =
+      blink::mojom::ManifestNoteTaking::New();
+  note_taking->new_note_url = GURL("https://evil.com");
+  bad_manifest->note_taking = std::move(note_taking);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  host->ValidateAndMaybeOverrideManifestForTesting(
+      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
+  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
+              ::testing::StartsWith("Manifest note_taking new_note_url must be "
+                                    "same-origin with the document."));
+}
+
+IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
+                       BadMessage_LockScreenStartUrlCrossOrigin) {
+  const GURL test_url =
+      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
+  ASSERT_TRUE(NavigateToURL(shell(), test_url));
+
+  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
+      shell()->web_contents()->GetPrimaryPage());
+
+  // Test that a cross-site lock_screen start_url triggers a bad message.
+  mojo::FakeMessageDispatchContext fake_dispatch_context;
+  auto bad_manifest = blink::mojom::Manifest::New();
+  bad_manifest->start_url = test_url;
+  bad_manifest->id = test_url;
+  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");
+
+  blink::mojom::ManifestLockScreenPtr lock_screen =
+      blink::mojom::ManifestLockScreen::New();
+  lock_screen->start_url = GURL("https://evil.com");
+  bad_manifest->lock_screen = std::move(lock_screen);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  host->ValidateAndMaybeOverrideManifestForTesting(
+      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
+  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
+              ::testing::StartsWith("Manifest lock_screen start_url must be "
+                                    "same-origin with the document."));
+}
+
 // Tests that if a compromised renderer bypasses the manifest parser and sends
 // cross-site migration data directly, the browser correctly rejects it and
 // kills the renderer with a bad message.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/manifest/manifest_browsertest.cc b/content/browser/manifest/manifest_browsertest.cc
index a992ddec3..a07a027 100644
--- a/content/browser/manifest/manifest_browsertest.cc
+++ b/content/browser/manifest/manifest_browsertest.cc
@@ -1218,6 +1218,64 @@
           "Manifest protocol_handlers must be same-origin with the document."));
 }
 
+IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
+                       BadMessage_NoteTakingNewNoteUrlCrossOrigin) {
+  const GURL test_url =
+      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
+  ASSERT_TRUE(NavigateToURL(shell(), test_url));
+
+  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
+      shell()->web_contents()->GetPrimaryPage());
+
+  // Test that a cross-site note_taking new_note_url triggers a bad message.
+  mojo::FakeMessageDispatchContext fake_dispatch_context;
+  auto bad_manifest = blink::mojom::Manifest::New();
+  bad_manifest->start_url = test_url;
+  bad_manifest->id = test_url;
+  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");
+
+  blink::mojom::ManifestNoteTakingPtr note_taking =
+      blink::mojom::ManifestNoteTaking::New();
+  note_taking->new_note_url = GURL("https://evil.com");
+  bad_manifest->note_taking = std::move(note_taking);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  host->ValidateAndMaybeOverrideManifestForTesting(
+      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
+  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
+              ::testing::StartsWith("Manifest note_taking new_note_url must be "
+                                    "same-origin with the document."));
+}
+
+IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
+                       BadMessage_LockScreenStartUrlCrossOrigin) {
+  const GURL test_url =
+      embedded_test_server()->GetURL("/manifest/empty-manifest.html");
+  ASSERT_TRUE(NavigateToURL(shell(), test_url));
+
+  ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
+      shell()->web_contents()->GetPrimaryPage());
+
+  // Test that a cross-site lock_screen start_url triggers a bad message.
+  mojo::FakeMessageDispatchContext fake_dispatch_context;
+  auto bad_manifest = blink::mojom::Manifest::New();
+  bad_manifest->start_url = test_url;
+  bad_manifest->id = test_url;
+  bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");
+
+  blink::mojom::ManifestLockScreenPtr lock_screen =
+      blink::mojom::ManifestLockScreen::New();
+  lock_screen->start_url = GURL("https://evil.com");
+  bad_manifest->lock_screen = std::move(lock_screen);
+
+  mojo::test::BadMessageObserver bad_message_observer;
+  host->ValidateAndMaybeOverrideManifestForTesting(
+      blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
+  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
+              ::testing::StartsWith("Manifest lock_screen start_url must be "
+                                    "same-origin with the document."));
+}
+
 // Tests that if a compromised renderer bypasses the manifest parser and sends
 // cross-site migration data directly, the browser correctly rejects it and
 // kills the renderer with a bad message.
Loading diff…

Original Bug Report

reported by pa...@gmail.com

Manifest note_taking->new_note_url not validated same-origin in browser process (missed sibling of 0b080cee89)


Report description

Manifest note_taking->new_note_url not validated same-origin in browser process (missed sibling of 0b080cee89)


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://source.chromium.org/chromium/chromium/src/+/main:content/browser/manifest/manifest_manager_host.cc;l=68


The problem

Please describe the technical details of the vulnerability

Vulnerability Description

ManifestManagerHost::MaybeGetBadMessageStringForManifest (manifest_manager_host.cc:42) validates same-origin for start_url, id, scope, share_target->action, file_handlers->action, protocol_handlers->url after the recent fix 0b080cee89 (2026-04-24, “Valid Manifest start_url, id, scope, share_target->action are same origin in the browser process”) but does not validate note_taking->new_note_url.

A compromised renderer at victim.com submits a manifest mojo with cross-origin note_taking->new_note_url:

bad_manifest->note_taking = blink::mojom::ManifestNoteTaking::New();
bad_manifest->note_taking->new_note_url = GURL("https://attacker.com/");

The browser accepts it. After PWA install, the URL is stored in WebApp::note_taking_new_note_url_. When the user invokes the Create Note intent on the installed PWA, WebAppLaunchProcess::GetLaunchUrl (chrome/browser/ui/web_applications/web_app_launch_process.cc:275) opens the attacker URL inside the PWA window:

} else if (is_note_taking_intent &&
           web_app_->note_taking_new_note_url().is_valid()) {
  launch_url = web_app_->note_taking_new_note_url();
}

The fix is the same one-liner pattern already applied to share_target/file_handlers/protocol_handlers in 0b080cee89: add a document_origin.IsSameOriginWith(...) check.

Attack Preconditions

  • Compromised renderer at the document origin (the same renderer that delivers the manifest mojo).
  • User installs the PWA from the document origin.
  • User invokes the Create Note intent on the installed PWA.

Reproduction Steps

A regression browsertest mirroring BadMessage_FileHandlersActionCrossOrigin and BadMessage_ProtocolHandlersActionCrossOrigin is provided.

  1. Apply the test patch (manifest_note_taking_origin_poc.patch) to content/browser/manifest/manifest_browsertest.cc.
  2. Build:
    autoninja -C out/asan content_browsertests
    
  3. Run:
    out/asan/content_browsertests \
        --gtest_filter=ManifestBrowserTest.BadMessage_NoteTakingNewNoteUrlCrossOrigin
    

Captured outputs:

  • manifest_note_taking_origin_test_output_without_fix.txt - on unmodified Chromium HEAD: FAILED. “Expected: starts with "Manifest note_taking new_note_url must be same-origin with the document." / Actual: ""” - no bad message issued.
  • manifest_note_taking_origin_test_output_with_fix.txt - with manifest_note_taking_origin_fix.patch applied: PASSED in 2 seconds.

Impact analysis

A page that successfully induces the user to install a PWA can plant any cross-origin URL into the PWA’s note-taking action. When the user invokes Create Note (e.g. via ChromeOS quick-action, Lens, or app launcher), the PWA window opens the attacker URL while the PWA chrome reflects the victim PWA’s identity. This is a cross-origin URL hijack of an installed PWA action.

Severity:

  • Vulnerability type: Permissions Bypass (cross-origin URL launched in PWA context that should be same-origin), with Security UI Spoofing as a secondary framing.
  • Process: browser.
  • Mitigations: none. Same-origin enforcement was added for sibling URL fields in 0b080cee89 (file_handlers, protocol_handlers, share_target) but missed note_taking->new_note_url.

Other URL fields in the manifest mojom that are also not currently validated and would benefit from the same fix in a follow-up sweep: lock_screen->start_url, update_manifest_url, tab_strip->new_tab_button->url, scope_extensions[].origin. Of these, tab_strip->new_tab_button->url is consumed by WebAppRegistrar::GetAppNewTabUrl for tabbed PWAs and exhibits the same launch-URL pattern.


The cause

What version of Chrome have you found the security issue in?

150.0.7831.0 (HEAD) Stable at the time of writing: 148.0.7778.97. Pre-existing on HEAD; the recent CL 0b080cee89 introduced the same-origin enforcement for several manifest URL fields but did not extend it to note_taking->new_note_url.

No, it is not related to a crash.

Choose the type of vulnerability

Permissions Bypass

View on issue tracker