Chrome · WebProtect
CVE-2026-14111
UAF in WebProtect
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc |
modified | |
BindLambdaForTestingcomponents/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc |
modified | |
ResumableUploadStringRequestTestcomponents/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc |
modified |
Files Changed
chrome/browser/enterprise/connectors/analysis/content_analysis_dialog_controller.cccomponents/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc
Patch
From 33b8433659943f67ac05e4d8e783cb5fc7391198 Mon Sep 17 00:00:00 2001
From: Alex Valadkevich <valadkevich@google.com>
Date: Thu, 28 May 2026 06:35:09 -0700
Subject: [PATCH] Defer early-cancel in tabless content analysis dialog
In background or tabless contexts without a page modal dialog manager,
receiving an early fast-track verdict synchronously triggers a
programmatic dialog cancellation. With the newly-implemented
MaybeCancelRequests active, this early cancel synchronously destructs
the uploader object while its own metadata functions are still active on
the call stack, which could lead to a crash.
This CL resolves the lifetime issue by deferring the early-cancel
asynchronously inside ShowDialogNow via a PostTask, allowing the
uploader's execution stack to resolve cleanly before deletion. Unit
tests have also been updated to reflect this deferred uploader
destruction timing.
Bug: 513710926
Change-Id: I0ec6777c6aaae6b297ba2a8faf66413cbf1793f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876183
Commit-Queue: Fabio Tirelo <ftirelo@chromium.org>
Reviewed-by: Fabio Tirelo <ftirelo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637656}
---
diff --git a/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog_controller.cc b/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog_controller.cc
index 4093e17d..17a0af6 100644
--- a/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog_controller.cc
+++ b/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog_controller.cc
@@ -130,7 +130,10 @@
// accept/cancel the result immediately. See crbug.com/374120523 and
// crbug.com/388049470 for more context.
if (!dialog_delegate_->is_pending()) {
- CancelButtonClicked();
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE,
+ base::BindOnce(&ContentAnalysisDialogController::CancelButtonClicked,
+ weak_ptr_factory_.GetWeakPtr()));
}
return;
}
diff --git a/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc b/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc
index 0e1aeab..bb58bad2 100644
--- a/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc
+++ b/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc
@@ -312,6 +312,130 @@
VerifyMetadataRequestHeaders(std::move(resource_request), "10", "test-token");
}
+TEST_F(ResumableUploadRequestBaseTest, OnMetadataUploadCompleted_UAF_Test) {
+ base::RunLoop run_loop;
+
+ std::unique_ptr<MockResumableUploadRequestBase> uploader;
+ auto callback = base::BindLambdaForTesting(
+ [&uploader, &run_loop](bool success, int http_status,
+ const std::string& response_body) {
+ // Asynchronously destroy the uploader to replicate the new service-side
+ // deferred deletion model (to prevent UAF inside active stack).
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindLambdaForTesting([&uploader, &run_loop]() {
+ uploader.reset();
+ run_loop.Quit();
+ }));
+ });
+
+ uploader = CreateFileRequest<MockResumableUploadRequestBase>(
+ "file content", ScanRequestUploadResult::kSuccess, std::move(callback),
+ base::DoNothing(), /*force_sync_upload=*/false, base::NullCallback());
+
+ test_url_loader_factory_.SetInterceptor(
+ base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+ if (request.url == GURL("https://google.com")) {
+ auto metadata_response_head =
+ network::CreateURLResponseHead(net::HTTP_OK);
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-Status",
+ "active");
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-URL",
+ kUploadUrl);
+ metadata_response_head->headers->AddHeader(
+ "X-Goog-Upload-Header-Cep-Response",
+ GetEncodedContentAnalysisResponse());
+ test_url_loader_factory_.AddResponse(
+ GURL("https://google.com"), std::move(metadata_response_head),
+ "metadata_response", network::URLLoaderCompletionStatus(net::OK));
+ }
+ }));
+
+ uploader->Start();
+ run_loop.Run();
+}
+
+TEST_F(ResumableUploadRequestBaseTest, Finish_UAF_Test) {
+ base::RunLoop run_loop;
+
+ std::unique_ptr<MockResumableUploadRequestBase> uploader;
+ auto callback = base::BindLambdaForTesting(
+ [&uploader, &run_loop](bool success, int http_status,
+ const std::string& response_body) {
+ // Asynchronously destroy the uploader to replicate the new service-side
+ // deferred deletion model (to prevent UAF inside active stack).
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindLambdaForTesting([&uploader, &run_loop]() {
+ uploader.reset();
+ run_loop.Quit();
+ }));
+ });
+
+ uploader = CreateFileRequest<MockResumableUploadRequestBase>(
+ "file content", ScanRequestUploadResult::kSuccess, std::move(callback),
+ base::DoNothing(), /*force_sync_upload=*/false, base::NullCallback());
+
+ test_url_loader_factory_.SetInterceptor(
+ base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+ if (request.url == GURL("https://google.com")) {
+ test_url_loader_factory_.AddResponse(
+ GURL("https://google.com"),
+ network::CreateURLResponseHead(net::HTTP_UNAUTHORIZED),
+ "unauthorized_response",
+ network::URLLoaderCompletionStatus(net::OK));
+ }
+ }));
+
+ uploader->Start();
+ run_loop.Run();
+}
+
+TEST_F(ResumableUploadRequestBaseTest, MaybeSendHashAndFinish_UAF_Test) {
+ base::RunLoop run_loop;
+
+ std::unique_ptr<MockResumableUploadRequestBase> uploader;
+ auto callback = base::BindLambdaForTesting(
+ [&uploader, &run_loop](bool success, int http_status,
+ const std::string& response_body) {
+ // Asynchronously destroy the uploader to replicate the new service-side
+ // deferred deletion model (to prevent UAF inside active stack).
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindLambdaForTesting([&uploader, &run_loop]() {
+ uploader.reset();
+ run_loop.Quit();
+ }));
+ });
+
+ auto register_on_got_hash_callback = base::BindLambdaForTesting(
+ [&](enterprise_connectors::OnGotHashCallback got_hash_callback) {
+ // Do nothing
+ });
+
+ uploader = CreateFileRequest<MockResumableUploadRequestBase>(
+ "file content", ScanRequestUploadResult::kSuccess, std::move(callback),
+ base::DoNothing(), /*force_sync_upload=*/false,
+ std::move(register_on_got_hash_callback));
+
+ test_url_loader_factory_.SetInterceptor(
+ base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+ if (request.url == GURL("https://google.com")) {
+ auto metadata_response_head =
+ network::CreateURLResponseHead(net::HTTP_OK);
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-Status",
+ "active");
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-URL",
+ kUploadUrl);
+ metadata_response_head->headers->AddHeader(
+ "X-Goog-Upload-Header-Cep-Response", "");
+ test_url_loader_factory_.AddResponse(
+ GURL("https://google.com"), std::move(metadata_response_head),
+ "metadata_response", network::URLLoaderCompletionStatus(net::OK));
+ }
+ }));
+
+ uploader->Start();
+ run_loop.Run();
+}
+
class ResumableUploadStringRequestTest : public ResumableUploadRequestBaseTest {
private:
base::test::ScopedFeatureList scoped_feature_list_{{kDlpScanPastedImages}};
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc b/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc
index 0e1aeab..bb58bad2 100644
--- a/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc
+++ b/components/enterprise/connectors/core/cloud_content_scanning/resumable_uploader_base_unittest.cc
@@ -312,6 +312,130 @@
VerifyMetadataRequestHeaders(std::move(resource_request), "10", "test-token");
}
+TEST_F(ResumableUploadRequestBaseTest, OnMetadataUploadCompleted_UAF_Test) {
+ base::RunLoop run_loop;
+
+ std::unique_ptr<MockResumableUploadRequestBase> uploader;
+ auto callback = base::BindLambdaForTesting(
+ [&uploader, &run_loop](bool success, int http_status,
+ const std::string& response_body) {
+ // Asynchronously destroy the uploader to replicate the new service-side
+ // deferred deletion model (to prevent UAF inside active stack).
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindLambdaForTesting([&uploader, &run_loop]() {
+ uploader.reset();
+ run_loop.Quit();
+ }));
+ });
+
+ uploader = CreateFileRequest<MockResumableUploadRequestBase>(
+ "file content", ScanRequestUploadResult::kSuccess, std::move(callback),
+ base::DoNothing(), /*force_sync_upload=*/false, base::NullCallback());
+
+ test_url_loader_factory_.SetInterceptor(
+ base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+ if (request.url == GURL("https://google.com")) {
+ auto metadata_response_head =
+ network::CreateURLResponseHead(net::HTTP_OK);
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-Status",
+ "active");
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-URL",
+ kUploadUrl);
+ metadata_response_head->headers->AddHeader(
+ "X-Goog-Upload-Header-Cep-Response",
+ GetEncodedContentAnalysisResponse());
+ test_url_loader_factory_.AddResponse(
+ GURL("https://google.com"), std::move(metadata_response_head),
+ "metadata_response", network::URLLoaderCompletionStatus(net::OK));
+ }
+ }));
+
+ uploader->Start();
+ run_loop.Run();
+}
+
+TEST_F(ResumableUploadRequestBaseTest, Finish_UAF_Test) {
+ base::RunLoop run_loop;
+
+ std::unique_ptr<MockResumableUploadRequestBase> uploader;
+ auto callback = base::BindLambdaForTesting(
+ [&uploader, &run_loop](bool success, int http_status,
+ const std::string& response_body) {
+ // Asynchronously destroy the uploader to replicate the new service-side
+ // deferred deletion model (to prevent UAF inside active stack).
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindLambdaForTesting([&uploader, &run_loop]() {
+ uploader.reset();
+ run_loop.Quit();
+ }));
+ });
+
+ uploader = CreateFileRequest<MockResumableUploadRequestBase>(
+ "file content", ScanRequestUploadResult::kSuccess, std::move(callback),
+ base::DoNothing(), /*force_sync_upload=*/false, base::NullCallback());
+
+ test_url_loader_factory_.SetInterceptor(
+ base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+ if (request.url == GURL("https://google.com")) {
+ test_url_loader_factory_.AddResponse(
+ GURL("https://google.com"),
+ network::CreateURLResponseHead(net::HTTP_UNAUTHORIZED),
+ "unauthorized_response",
+ network::URLLoaderCompletionStatus(net::OK));
+ }
+ }));
+
+ uploader->Start();
+ run_loop.Run();
+}
+
+TEST_F(ResumableUploadRequestBaseTest, MaybeSendHashAndFinish_UAF_Test) {
+ base::RunLoop run_loop;
+
+ std::unique_ptr<MockResumableUploadRequestBase> uploader;
+ auto callback = base::BindLambdaForTesting(
+ [&uploader, &run_loop](bool success, int http_status,
+ const std::string& response_body) {
+ // Asynchronously destroy the uploader to replicate the new service-side
+ // deferred deletion model (to prevent UAF inside active stack).
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindLambdaForTesting([&uploader, &run_loop]() {
+ uploader.reset();
+ run_loop.Quit();
+ }));
+ });
+
+ auto register_on_got_hash_callback = base::BindLambdaForTesting(
+ [&](enterprise_connectors::OnGotHashCallback got_hash_callback) {
+ // Do nothing
+ });
+
+ uploader = CreateFileRequest<MockResumableUploadRequestBase>(
+ "file content", ScanRequestUploadResult::kSuccess, std::move(callback),
+ base::DoNothing(), /*force_sync_upload=*/false,
+ std::move(register_on_got_hash_callback));
+
+ test_url_loader_factory_.SetInterceptor(
+ base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+ if (request.url == GURL("https://google.com")) {
+ auto metadata_response_head =
+ network::CreateURLResponseHead(net::HTTP_OK);
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-Status",
+ "active");
+ metadata_response_head->headers->AddHeader("X-Goog-Upload-URL",
+ kUploadUrl);
+ metadata_response_head->headers->AddHeader(
+ "X-Goog-Upload-Header-Cep-Response", "");
+ test_url_loader_factory_.AddResponse(
+ GURL("https://google.com"), std::move(metadata_response_head),
+ "metadata_response", network::URLLoaderCompletionStatus(net::OK));
+ }
+ }));
+
+ uploader->Start();
+ run_loop.Run();
+}
+
class ResumableUploadStringRequestTest : public ResumableUploadRequestBaseTest {
private:
base::test::ScopedFeatureList scoped_feature_list_{{kDlpScanPastedImages}};
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page