CVE-2026-11642
Overview
Background
- `TabbedWebAppNavigationThrottle`
- a
NavigationThrottlefor tabbed web-app (PWA) windows that intercepts navigations and can redirecttarget=_blanklinks aimed at the home tab. - `NavigationThrottle` lifecycle
- a throttle runs synchronously inside the navigation pipeline (e.g.
WillStartRequest) and the pipeline resumes using the throttle/NavigationRequestobjects after the handler returns. - `content::WebContents::ClosePage()`
- a call that can synchronously destroy the
WebContents, and with it the activeNavigationRequestand the runningNavigationThrottle. - `PostTask` + `WebContents::GetWeakPtr()`
- deferring work to a later task turn, guarded by a
WeakPtrso the closed-page call is skipped if theWebContentsis already gone.
Root Cause Analysis
Inside the throttle handler (the commit identifies it as WillStartRequest, though the @@ hunk header does not name it), a target=_blank link to the home tab opens a blank tab; when more than one tab exists and the blank tab has no valid committed URL, the code called web_contents->ClosePage() synchronously. Because the throttle executes within the navigation pipeline, synchronously closing the page destroys the WebContents and therefore the live NavigationRequest and the TabbedWebAppNavigationThrottle itself. Control then returns to the still-running handler (which proceeds to return FocusHomeTab(...)) and the pipeline resumes using the throttle/request objects that ClosePage() already freed, a use-after-free.
The fix replaces the synchronous call with base::SequencedTaskRunner::GetCurrentDefault()->PostTask(...) bound to content::WebContents::ClosePage via web_contents->GetWeakPtr(), so the close runs on a later task turn after the throttle has safely finished its lifecycle, and is skipped entirely if the WebContents no longer exists.
WebContents (and thus itself and the NavigationRequest) mid-handler by calling ClosePage() synchronously; the fix defers that close to a posted task guarded by a WeakPtr, so teardown cannot race the still-executing navigation pipeline.Attack Path
- Open a tabbed web app The victim uses an installed PWA that runs in tabbed mode with more than one tab open.
- Trigger a home-tab `_blank` navigation
A
target=_blanklink aimed at the home tab causes the browser to open a blank tab and route the navigation throughTabbedWebAppNavigationThrottle. - Enter the close-blank-tab branch
With
count() > 1and the blank tab’s last committed URL invalid, the throttle callsClosePage()on the blank tab’sWebContents. - Synchronous self-destruction
ClosePage()synchronously destroys theWebContents, the activeNavigationRequest, and the throttle while the handler is still on the stack. - Use-after-free on resume
The navigation pipeline resumes and uses the freed throttle/
NavigationRequest; the exact reused object and any further primitive are(inferred).
Impact Assessment
NavigationThrottle/NavigationRequest in the browser process by inducing the home-tab target=_blank close path. The vendor advisory rates this critical; preconditions are an installed tabbed web app with multiple tabs and a crafted _blank link to the home tab, so it requires a PWA context rather than arbitrary stock-build web content. Any escalation beyond the freed-then-used navigation objects the patch addresses (controlled reuse, code execution) is (inferred).Files Changed
chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.cc
Audit Directions
- Synchronous destruction inside throttles/observersAudit
NavigationThrottleand similar pipeline callbacks for synchronous calls (ClosePage,Close,DeleteContents) that can destroy the veryWebContents/request the caller continues to use. - Self-owned lifetime during callbacksFlag handlers that may free
thisor an object the surrounding framework still dereferences after the handler returns; preferPostTask+WeakPtrdeferral. - Tab-close paths in web-app UIReview PWA/tab-strip code that closes auxiliary or blank tabs mid-navigation, ensuring teardown is asynchronous relative to the navigation pipeline.
Patch
From c303de53a69279cd5b7b7a79aa46303a4cce0356 Mon Sep 17 00:00:00 2001
From: Dibyajyoti Pal <dibyapal@google.com>
Date: Fri, 29 May 2026 16:08:53 -0700
Subject: [PATCH] [PWA] Fix UAF in TabbedWebAppNavigationThrottle
In TabbedWebAppNavigationThrottle::WillStartRequest, target=_blank
links to the home tab cause a blank tab to be opened. The throttle
closes this blank tab by calling web_contents->ClosePage()
synchronously.
However, calling ClosePage() synchronously during throttle execution
destroys the WebContents (and consequently the active NavigationRequest
and NavigationThrottle), which leads to a User-After-Free (UAF) when
the navigation pipeline resumes execution.
This CL fixes the UAF by deferring the ClosePage() call asynchronously.
We use PostTask with a WeakPtr to the WebContents to ensure that the
current navigation throttle can finish its lifecycle safely before the
WebContents is closed.
Fixed: 517678820
Change-Id: I06dfdc109f77ee31381948d1a2705187bc42d608
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7886484
Commit-Queue: Dibyajyoti Pal <dibyapal@chromium.org>
Auto-Submit: Dibyajyoti Pal <dibyapal@chromium.org>
Reviewed-by: Nate Chapin <japhet@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1638795}
---
diff --git a/chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.cc b/chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.cc
index 5d6ba6d7..2b20ead 100644
--- a/chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.cc
+++ b/chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.cc
@@ -4,6 +4,8 @@
#include "chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.h"
+#include "base/functional/bind.h"
+#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -137,7 +139,9 @@
if (browser_window->GetTabStripModel()->count() > 1 &&
!web_contents->GetLastCommittedURL().is_valid()) {
DVLOG(1) << "TabbedWebAppNavigationThrottle: Closing blank tab";
- web_contents->ClosePage();
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindOnce(&content::WebContents::ClosePage,
+ web_contents->GetWeakPtr()));
}
DVLOG(1) << "TabbedWebAppNavigationThrottle: Redirecting to FocusHomeTab";
return FocusHomeTab(*app_controller, *browser_window->GetTabStripModel());
Original Bug Report
Potential Browser Process UAF in TabbedWebAppNavigationThrottle via Synchronous ClosePage
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in TabbedWebAppNavigationThrottle when a blank tab is navigated to a home-scope URL while a modal JavaScript dialog is active. Under this condition, calling ClosePage() synchronously destroys the WebContents, the NavigationRequest, and the throttle itself. Execution then continues into FocusHomeTab(), leading to virtual method calls on the freed throttle.
Affected files:
chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.ccchrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.h
Estimated timestamp from git blame: 2023-07-14
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in TabbedWebAppNavigationThrottle when a blank tab is navigated to a home-scope URL within a tabbed Progressive Web App (PWA). If a modal JavaScript dialog is active, calling web_contents->ClosePage() synchronously destroys the WebContents, the active NavigationRequest, and the TabbedWebAppNavigationThrottle itself. Execution then continues into FocusHomeTab(), performing virtual method calls on the freed this pointer and the deallocated NavigationRequest.
Vulnerability Analysis & Lifetime Flow
Under normal circumstances, calling web_contents->ClosePage() initiates an asynchronous teardown of the page. However, if a modal JavaScript dialog is showing on the page, the page is deemed “ready to be closed” immediately, causing RenderFrameHostImpl::ClosePage to bypass unload events and take a fully synchronous destruction path:
RenderFrameHostImpl::ClosePage()callsIsPageReadyToBeClosed(), which returnstruebecausedelegate_->IsJavaScriptDialogShowing()evaluates totrue.- This invokes
ClosePageIgnoringUnloadEvents(), which synchronously callsdelegate_->Close(this). - The delegate calls
Browser::CloseContents(), leading tochrome::CloseWebContents()andTabStripModel::CloseWebContentsAt(). - The
TabStripModelsynchronously detaches and resets the tab’sWebContentsImplobject. - Destroying the
WebContentsImplcancels the active navigation, synchronously deleting theNavigationRequestand the ownedNavigationThrottleRegistryImpl. - This registry destruction deletes all registered throttles, including the executing
TabbedWebAppNavigationThrottleinstance.
Upon returning from ClosePage() in TabbedWebAppNavigationThrottle::WillStartRequest() (line 140), the execution immediately calls:
return FocusHomeTab(*app_controller, *browser_window->GetTabStripModel());
Since this has been deallocated, FocusHomeTab is called on a dangling pointer. Inside FocusHomeTab, the code attempts to call navigation_handle(), which translates to ®istry_->GetNavigationHandle(), executing a virtual method call on a deallocated registry reference.
MiraclePtr (BackupRefPtr) Bypass
MiraclePtr does not protect against this Use-After-Free because:
- The dangling reference is the implicit stack pointer (
this) during member function execution, which is not wrapped. - The
raw_refreference toregistry_has already been destructed whenthiswas deleted, releasing its refcount and allowing the backing memory to be immediately reclaimed.
Potential Reproduction Steps
Note: These are potential/suggested steps; our tooling does not currently have the capability to execute code or run a live proof of concept.
- Launch a tabbed PWA window with a pinned home tab.
- Open a new blank tab (Tab B) in the same application window.
- On Tab B, trigger a modal JavaScript dialog state (e.g., via
RunModalAlertDialogMojo IPC) so thatis_showing_javascript_dialog_is set totrueon theWebContents. - Initiate a navigation on Tab B to a URL that is within the home tab’s scope.
- When
TabbedWebAppNavigationThrottle::WillStartRequest()is invoked, because the tab count is greater than 1 and Tab B’s last committed URL is invalid, it executesweb_contents->ClosePage(). - Due to the active JavaScript dialog, the synchronous destruction path is taken, deallocating the throttle.
- Execution resumes in
WillStartRequest(), which immediately entersFocusHomeTab()and performs virtual calls on the freed memory, leading to a potential browser process crash or control flow hijack.
Suggested Fix
To fix this issue, avoid closing the page synchronously within the execution frame of the navigation throttle. Instead of calling web_contents->ClosePage() synchronously during WillStartRequest, the throttle should return content::NavigationThrottle::CANCEL_AND_IGNORE and post a task to close the page asynchronously on the UI thread task runner via a WeakPtr to the WebContents:
// In chrome/browser/ui/web_applications/tabbed_web_app_navigation_throttle.cc:
if (browser_window->GetTabStripModel()->count() > 1 &&
!web_contents->GetLastCommittedURL().is_valid()) {
DVLOG(1) << "TabbedWebAppNavigationThrottle: Closing blank tab asynchronously";
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&content::WebContents::ClosePage,
web_contents->GetWeakPtr()));
}
This ensures the throttle can return safely and allows the navigation runner to terminate processing before the WebContents (and the throttle) are destroyed.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.