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 <[email protected]> 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 <[email protected]> Reviewed-by: Fabio Tirelo <[email protected]> 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