Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Chrome for iOS
DescriptionInappropriate implementation in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker508257850
Fix commit7620a18625ad (chromium/src) +29/-409
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/drive/model/drive_upload_task.mm
modified
DriveUploadTaskTest
ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
modified
TEST_P
ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
modified
TEST_F
ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
modified

Files Changed

  • chrome/browser/flag-metadata.json
  • ios/chrome/browser/drive/model/drive_file_uploader.h
  • ios/chrome/browser/drive/model/drive_upload_task.h
  • ios/chrome/browser/drive/model/drive_upload_task.mm
  • ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
From 7620a18625ad6148555f65151050d54d1146fe79 Mon Sep 17 00:00:00 2001
From: Quentin Pubert <[email protected]>
Date: Mon, 04 May 2026 07:30:18 -0700
Subject: [PATCH] [iOS] [SaveToDrive] Clean up kIOSSaveToDriveClientFolder feature flag

Clean up the kIOSSaveToDriveClientFolder feature flag (which was enabled
by default), making the client folder implementation the only behavior
for Save to Drive uploads. This removes the legacy two-step folder
search-then-create flow.

- Modified DriveUploadTask to directly call
  FetchClientFolderThenUploadFile() and deleted legacy private
  methods SearchFolderThenCreateFolderOrDirectlyUploadFile and
  CreateFolderOrDirectlyUploadFile.
- Modified DriveFileUploader to make SearchSaveToDriveFolder and
  CreateSaveToDriveFolder virtual with default empty implementations,
  preventing build breakages.
- Simplified TestDriveFileUploader by removing mock overrides and
  helper methods/fields for the legacy flows.
- Cleaned up features.h, features.mm, about_flags.mm, and
  ios_chrome_flag_descriptions to remove the feature flag.
- Simplified drive_upload_task_unittest.mm to remove the feature
  override and simplified the remaining tests.

Bug: 508257850
Change-Id: I56498957c3c2a4767f3ddbb32e8f7e5850c4320d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7813594
Auto-Submit: Quentin Pubert <[email protected]>
Commit-Queue: Olivier Robin <[email protected]>
Reviewed-by: Olivier Robin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1624647}
---

diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index ef20881c..dceb7c4 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -5929,11 +5929,6 @@
     "expiry_milestone": 150
   },
   {
-    "name": "ios-save-to-drive-client-folder",
-    "owners": [ "[email protected]", "[email protected]", "[email protected]" ],
-    "expiry_milestone": 150
-  },
-  {
     "name": "ios-save-to-drive-signed-out",
     "owners": [ "[email protected]", "[email protected]", "[email protected]"],
     "expiry_milestone": 153
diff --git a/ios/chrome/browser/drive/model/drive_file_uploader.h b/ios/chrome/browser/drive/model/drive_file_uploader.h
index b02e3cd..ec105531 100644
--- a/ios/chrome/browser/drive/model/drive_file_uploader.h
+++ b/ios/chrome/browser/drive/model/drive_file_uploader.h
@@ -93,7 +93,7 @@
   // through `completion_callback`.
   virtual void SearchSaveToDriveFolder(
       NSString* folder_name,
-      DriveFolderCompletionCallback completion_callback) = 0;
+      DriveFolderCompletionCallback completion_callback) {}
 
   // Creates the destination Drive folder. The name of the created folder is
   // `folder_name` and a custom property is added in the folder's metadata to
@@ -102,7 +102,7 @@
   // through `completion_callback`.
   virtual void CreateSaveToDriveFolder(
       NSString* folder_name,
-      DriveFolderCompletionCallback completion_callback) = 0;
+      DriveFolderCompletionCallback completion_callback) {}
 
   // Gets or creates the destination Drive folder as a client folder. The name
   // of the created folder is `folder_name`. The result, including possible
diff --git a/ios/chrome/browser/drive/model/drive_upload_task.h b/ios/chrome/browser/drive/model/drive_upload_task.h
index 9358ac8..300a943b 100644
--- a/ios/chrome/browser/drive/model/drive_upload_task.h
+++ b/ios/chrome/browser/drive/model/drive_upload_task.h
@@ -37,19 +37,6 @@
   NSError* GetError() const final;
 
  private:
-  // Performs the first step of this upload task i.e. search a destination Drive
-  // folder using `uploader_->SearchSaveToDriveFolder(folder_name, ...)`.
-  // The result will be reported to `CreateFolderOrDirectlyUploadFile()`;
-  void SearchFolderThenCreateFolderOrDirectlyUploadFile();
-
-  // Performs the second step of this upload task i.e.
-  // if the first step returned an existing folder, directly upload the file to
-  // this existing folder using `UploadFile()`. Otherwise, create a destination
-  // folder using `uploader_->CreateSaveToDriveFolder(folder_name, ...)` and
-  // report the result to `UploadFile()`;
-  void CreateFolderOrDirectlyUploadFile(
-      const DriveFolderResult& folder_search_result);
-
   // Performs the first and second steps of this upload task i.e. search a
   // destination Drive folder and create it if it does not exist in a single
   // operation using `uploader_->FetchSaveToDriveClientFolder(folder_name,
diff --git a/ios/chrome/browser/drive/model/drive_upload_task.mm b/ios/chrome/browser/drive/model/drive_upload_task.mm
index b5ce9889..d4a760a 100644
--- a/ios/chrome/browser/drive/model/drive_upload_task.mm
+++ b/ios/chrome/browser/drive/model/drive_upload_task.mm
@@ -152,11 +152,7 @@
   upload_result_.reset();
   SetState(State::kInProgress);
   number_of_attempts_++;
-  if (base::FeatureList::IsEnabled(kIOSSaveToDriveClientFolder)) {
-    FetchClientFolderThenUploadFile();
-  } else {
-    SearchFolderThenCreateFolderOrDirectlyUploadFile();
-  }
+  FetchClientFolderThenUploadFile();
 }
 
 void DriveUploadTask::Cancel() {
@@ -205,49 +201,6 @@
 
 #pragma mark - Private
 
-void DriveUploadTask::SearchFolderThenCreateFolderOrDirectlyUploadFile() {
-  // Search a destination Drive folder using
-  // `SearchSaveToDriveFolder(folder_name, ...)`;
-  uploader_->SearchSaveToDriveFolder(
-      base::SysUTF8ToNSString(folder_name_),
-      base::BindOnce(&DriveUploadTask::CreateFolderOrDirectlyUploadFile,
-                     weak_ptr_factory_.GetWeakPtr()));
-}
-
-void DriveUploadTask::CreateFolderOrDirectlyUploadFile(
-    const DriveFolderResult& folder_search_result) {
-  // Record folder search success histogram.
-  base::UmaHistogramBoolean(kDriveSearchFolderResultSuccessful,
-                            !folder_search_result.error);
-  // If folder search failed, update state and result with the error object.
-  if (folder_search_result.error) {
-    base::UmaHistogramSparse(kDriveSearchFolderResultErrorCode,
-                             folder_search_result.error.code);
-    upload_result_ =
-        DriveFileUploadResult({.error = folder_search_result.error});
-    SetState(State::kFailed);
-    return;
-  }
-  // If the first step returned an existing folder, upload file directly.
-  if (folder_search_result.folder_identifier) {
-    UploadFile(folder_search_result);
-    return;
-  }
-  // Otherwise, create a destination Drive folder using
-  // `CreateSaveToDriveFolder(folder_name, ...)`;
-  auto record_result_successful_callback = base::BindOnce(
-      RecordDriveFolderResultSuccessful, kDriveCreateFolderResultSuccessful);
-  auto record_result_error_code_callback = base::BindOnce(
-      RecordDriveFolderResultErrorCode, kDriveCreateFolderResultErrorCode);
-  auto upload_file_callback = base::BindOnce(&DriveUploadTask::UploadFile,
-                                             weak_ptr_factory_.GetWeakPtr());
-  uploader_->CreateSaveToDriveFolder(
-      base::SysUTF8ToNSString(folder_name_),
-      std::move(record_result_successful_callback)
-          .Then(std::move(record_result_error_code_callback))
-          .Then(std::move(upload_file_callback)));
-}
-
 void DriveUploadTask::FetchClientFolderThenUploadFile() {
   // Get or create a destination Drive folder using
   // `FetchSaveToDriveClientFolder(folder_name, ...)`;
diff --git a/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm b/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
index 48e06c4b..9635cf3 100644
--- a/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
+++ b/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
@@ -8,7 +8,6 @@
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/metrics/histogram_tester.h"
 #import "base/test/task_environment.h"
-#import "base/test/with_feature_override.h"
 #import "ios/chrome/browser/download/model/download_mimetype_util.h"
 #import "ios/chrome/browser/drive/model/drive_metrics.h"
 #import "ios/chrome/browser/drive/model/test_drive_file_uploader.h"
@@ -23,11 +22,9 @@
 using State = UploadTask::State;
 
 // DriveUploadTask unit tests.
-class DriveUploadTaskTest : public base::test::WithFeatureOverride,
-                            public PlatformTest {
+class DriveUploadTaskTest : public PlatformTest {
  protected:
-  DriveUploadTaskTest()
-      : base::test::WithFeatureOverride(kIOSSaveToDriveClientFolder) {}
+  DriveUploadTaskTest() = default;
 
   void SetUp() final {
     PlatformTest::SetUp();
@@ -50,7 +47,7 @@
 
 // Tests that upon first starting and later cancelling an upload task, the state
 // of the task is correctly updated.
-TEST_P(DriveUploadTaskTest, TaskCanBeStartedAndCancelled) {
+TEST_F(DriveUploadTaskTest, TaskCanBeStartedAndCancelled) {
   EXPECT_EQ(0, task_->GetProgress());
   EXPECT_EQ(State::kNotStarted, task_->GetState());
   EXPECT_EQ(nil, task_->GetError());
@@ -70,181 +67,12 @@
   EXPECT_TRUE(task_->IsDone());
 }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm b/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
index 48e06c4b..9635cf3 100644
--- a/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
+++ b/ios/chrome/browser/drive/model/drive_upload_task_unittest.mm
@@ -8,7 +8,6 @@
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/metrics/histogram_tester.h"
 #import "base/test/task_environment.h"
-#import "base/test/with_feature_override.h"
 #import "ios/chrome/browser/download/model/download_mimetype_util.h"
 #import "ios/chrome/browser/drive/model/drive_metrics.h"
 #import "ios/chrome/browser/drive/model/test_drive_file_uploader.h"
@@ -23,11 +22,9 @@
 using State = UploadTask::State;
 
 // DriveUploadTask unit tests.
-class DriveUploadTaskTest : public base::test::WithFeatureOverride,
-                            public PlatformTest {
+class DriveUploadTaskTest : public PlatformTest {
  protected:
-  DriveUploadTaskTest()
-      : base::test::WithFeatureOverride(kIOSSaveToDriveClientFolder) {}
+  DriveUploadTaskTest() = default;
 
   void SetUp() final {
     PlatformTest::SetUp();
@@ -50,7 +47,7 @@
 
 // Tests that upon first starting and later cancelling an upload task, the state
 // of the task is correctly updated.
-TEST_P(DriveUploadTaskTest, TaskCanBeStartedAndCancelled) {
+TEST_F(DriveUploadTaskTest, TaskCanBeStartedAndCancelled) {
   EXPECT_EQ(0, task_->GetProgress());
   EXPECT_EQ(State::kNotStarted, task_->GetState());
   EXPECT_EQ(nil, task_->GetError());
@@ -70,181 +67,12 @@
   EXPECT_TRUE(task_->IsDone());
 }
 
-// Tests that if the destination folder does *NOT* exist, the task will create
-// it and upload the file as expected.
-TEST_P(DriveUploadTaskTest, CreatesFolderIfNotFound) {
-  if (IsParamFeatureEnabled()) {
-    return;
-  }
-  base::HistogramTester histogram_tester;
-  // Set up the uploader to simulate empty search results, a successful folder
-  // creation and file upload progress and successful completion.
-  uploader_->SetFolderSearchResult({.folder_identifier = nil, .error = nil});
-  uploader_->SetFolderCreationResult(
-      {.folder_identifier = @"test_folder_identifier", .error = nil});
-  const std::vector<DriveFileUploadProgress> progress_elements{
-      {0, 100}, {10, 100}, {25, 100}, {50, 100}, {99, 100}, {100, 100},
-  };
-  uploader_->SetFileUploadProgressElements(progress_elements);
-  const char* response_link_str = "https://test_response.file_link";
-  const GURL response_link(response_link_str);
-  const GURL response_link_with_identifier = net::AppendOrReplaceQueryParameter(
-      response_link, "huid",
-      base::SysNSStringToUTF8(uploader_->GetIdentity().hashedGaiaID));
-  uploader_->SetFileUploadResult(
-      { .file_link = @(response_link_str), .error = nil });
-  // Set up the task with the name of the parent folder as well as the path,
-  // suggested name and MIME type of the file to upload.
-  task_->SetDestinationFolderName("test_folder_name");
-  const base::FilePath file_path_to_upload{"/test/path/of/file/to/upload"};
-  const base::FilePath file_suggested_name{"test_uploaded_file_name"};
-  task_->SetFileToUpload(file_path_to_upload, file_suggested_name,
-                         "test_mime_type", 42);
-  // Test that a folder with appropriate name is searched.
-  uploader_->SetSearchFolderQuitClosure(task_environment_.QuitClosure());
-  EXPECT_EQ(State::kNotStarted, task_->GetState());
-  task_->Start();
-  EXPECT_EQ(State::kInProgress, task_->GetState());
-  task_environment_.RunUntilQuit();
-  EXPECT_NSEQ(@"test_folder_name", uploader_->GetSearchedFolderName());
-  // Test that a folder with appropriate name is created.
-  uploader_->SetCreateFolderQuitClosure(task_environment_.QuitClosure());
-  task_environment_.RunUntilQuit();
-  EXPECT_NSEQ(@"test_folder_name", uploader_->GetCreatedFolderName());
-  // Test that the file parameters provided earlier are forwarded to the
-  // uploader.
-  EXPECT_NSEQ(base::apple::FilePathToNSURL(file_path_to_upload),
-              uploader_->GetUploadedFileUrl());
-  EXPECT_NSEQ(base::apple::FilePathToNSString(file_suggested_name),
-              uploader_->GetUploadedFileName());
-  EXPECT_NSEQ(@"test_mime_type", uploader_->GetUploadedFileMimeType());
-  // Test that the parent folder identifier is the one returned by the uploader.
-  EXPECT_NSEQ(@"test_folder_identifier",
-              uploader_->GetUploadedFileFolderIdentifier());
-  // Test that progress is reported as expected.
-  for (const DriveFileUploadProgress& progress : progress_elements) {
-    uploader_->SetUploadFileProgressQuitClosure(
-        task_environment_.QuitClosure());
-    observer_->ResetUpdatedUpload();
-    task_environment_.RunUntilQuit();
-    EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
-    const float progress_float =
-        static_cast<float>(progress.total_bytes_uploaded) /
-        progress.total_bytes_expected_to_upload;
-    EXPECT_EQ(progress_float, task_->GetProgress());
-  }
-  // Test that the result is reported as expected.
-  uploader_->SetUploadFileCompletionQuitClosure(
-      task_environment_.QuitClosure());
-  observer_->ResetUpdatedUpload();
-  task_environment_.RunUntilQuit();
-  EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
-  EXPECT_EQ(response_link,
-            task_->GetResponseLink(/* add_user_identifier= */ false));
-  EXPECT_EQ(response_link_with_identifier,
-            task_->GetResponseLink(/* add_user_identifier= */ true));
-  EXPECT_NSEQ(nil, task_->GetError());
-  EXPECT_EQ(State::kComplete, task_->GetState());
-  // Test that expected histograms were recorded.
-  histogram_tester.ExpectUniqueSample(kDriveSearchFolderResultSuccessful, true,
-                                      1);
-  histogram_tester.ExpectUniqueSample(kDriveCreateFolderResultSuccessful, true,
-                                      1);
-  histogram_tester.ExpectUniqueSample(kDriveFileUploadResultSuccessful, true,
-                                      1);
-}
-
-// Tests that if the destination folder *DOES* exist, the task will use it as-is
-// and upload the file as expected.
-TEST_P(DriveUploadTaskTest, UsesExistingFolderIfFound) {
-  if (IsParamFeatureEnabled()) {
-    return;
-  }
-  base::HistogramTester histogram_tester;
-  // Set up the uploader to simulate non-empty search result, and file upload
-  // progress and successful completion.
-  uploader_->SetFolderSearchResult(
-      {.folder_identifier = @"test_folder_identifier", .error = nil});
-  const std::vector<DriveFileUploadProgress> progress_elements{
-      {0, 100}, {10, 100}, {25, 100}, {50, 100}, {99, 100}, {100, 100},
-  };
-  uploader_->SetFileUploadProgressElements(progress_elements);
-  const char* response_link_str = "https://test_response.file_link";
-  const GURL response_link(response_link_str);
-  const GURL response_link_with_identifier = net::AppendOrReplaceQueryParameter(
-      response_link, "huid",
-      base::SysNSStringToUTF8(uploader_->GetIdentity().hashedGaiaID));
-  uploader_->SetFileUploadResult(
-      { .file_link = @(response_link_str), .error = nil });
-  // Set up the task with the name of the parent folder as well as the path,
-  // suggested name and MIME type of the file to upload.
-  task_->SetDestinationFolderName("test_folder_name");
-  const base::FilePath file_path_to_upload{"/test/path/of/file/to/upload"};
-  const base::FilePath file_suggested_name{"test_uploaded_file_name"};
-  task_->SetFileToUpload(file_path_to_upload, file_suggested_name,
-                         "test_mime_type", 42);
-  // Test that a folder with appropriate name is searched.
-  uploader_->SetSearchFolderQuitClosure(task_environment_.QuitClosure());
-  EXPECT_EQ(State::kNotStarted, task_->GetState());
-  task_->Start();
-  EXPECT_EQ(State::kInProgress, task_->GetState());
-  task_environment_.RunUntilQuit();
-  EXPECT_NSEQ(@"test_folder_name", uploader_->GetSearchedFolderName());
-  // Test that the file parameters provided earlier are forwarded to the
-  // uploader.
-  EXPECT_NSEQ(base::apple::FilePathToNSURL(file_path_to_upload),
-              uploader_->GetUploadedFileUrl());
-  EXPECT_NSEQ(base::apple::FilePathToNSString(file_suggested_name),
-              uploader_->GetUploadedFileName());
-  EXPECT_NSEQ(@"test_mime_type", uploader_->GetUploadedFileMimeType());
-  // Test that the parent folder identifier is the one returned by the uploader.
-  EXPECT_NSEQ(@"test_folder_identifier",
-              uploader_->GetUploadedFileFolderIdentifier());
-  // Test that progress is reported as expected.
-  for (const DriveFileUploadProgress& progress : progress_elements) {
-    uploader_->SetUploadFileProgressQuitClosure(
-        task_environment_.QuitClosure());
-    observer_->ResetUpdatedUpload();
-    task_environment_.RunUntilQuit();
-    EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
-    const float progress_float =
-        static_cast<float>(progress.total_bytes_uploaded) /
-        progress.total_bytes_expected_to_upload;
-    EXPECT_EQ(progress_float, task_->GetProgress());
-  }
-  // Test that the result is reported as expected.
-  uploader_->SetUploadFileCompletionQuitClosure(
-      task_environment_.QuitClosure());
-  observer_->ResetUpdatedUpload();
-  task_environment_.RunUntilQuit();
-  EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
-  EXPECT_EQ(response_link,
-            task_->GetResponseLink(/* add_user_identifier= */ false));
-  EXPECT_EQ(response_link_with_identifier,
-            task_->GetResponseLink(/* add_user_identifier= */ true));
-  EXPECT_NSEQ(nil, task_->GetError());
-  EXPECT_EQ(State::kComplete, task_->GetState());
-  // Test that expected histograms were recorded.
-  histogram_tester.ExpectUniqueSample(kDriveSearchFolderResultSuccessful, true,
-                                      1);
-  histogram_tester.ExpectUniqueSample(kDriveCreateFolderResultSuccessful, true,
-                                      0);
-  histogram_tester.ExpectUniqueSample(kDriveFileUploadResultSuccessful, true,
-                                      1);
-}
-
 // Tests that if the file upload fails, failure is correctly reported.
-TEST_P(DriveUploadTaskTest, ReportsFileUploadFailure) {
+TEST_F(DriveUploadTaskTest, ReportsFileUploadFailure) {
   base::HistogramTester histogram_tester;
-  if (IsParamFeatureEnabled()) {
-    // Set up the uploader to simulate a successful client folder fetch.
-    uploader_->SetClientFolderResult(
-        {.folder_identifier = @"test_folder_identifier", .error = nil});
-  } else {
-    // Set up the uploader to simulate non-empty search result.
-    uploader_->SetFolderSearchResult(
-        {.folder_identifier = @"test_folder_identifier", .error = nil});
-  }
+  // Set up the uploader to simulate a successful client folder fetch.
+  uploader_->SetClientFolderResult(
+      {.folder_identifier = @"test_folder_identifier", .error = nil});
 
   // Set up the uploader to simulate file upload progress and unsuccessful
   // completion.
@@ -269,20 +97,12 @@
                          "test_mime_type", 42);
 
   // Test that the correct folder API is called.
-  if (IsParamFeatureEnabled()) {
-    uploader_->SetFetchClientFolderQuitClosure(task_environment_.QuitClosure());
-  } else {
-    uploader_->SetSearchFolderQuitClosure(task_environment_.QuitClosure());
-  }
+  uploader_->SetFetchClientFolderQuitClosure(task_environment_.QuitClosure());
   EXPECT_EQ(State::kNotStarted, task_->GetState());
   task_->Start();
   EXPECT_EQ(State::kInProgress, task_->GetState());
   task_environment_.RunUntilQuit();
-  if (IsParamFeatureEnabled()) {
-    EXPECT_NSEQ(@"test_folder_name", uploader_->GetFetchedClientFolderName());
-  } else {
-    EXPECT_NSEQ(@"test_folder_name", uploader_->GetSearchedFolderName());
-  }
+  EXPECT_NSEQ(@"test_folder_name", uploader_->GetFetchedClientFolderName());
 
   // Test that the file parameters provided earlier are forwarded to the
   // uploader.
@@ -316,27 +136,15 @@
   EXPECT_NSEQ(file_upload_error, task_->GetError());
   EXPECT_EQ(State::kFailed, task_->GetState());
   // Test that expected histograms were recorded.
-  if (IsParamFeatureEnabled()) {
-    histogram_tester.ExpectUniqueSample(kDriveFetchClientFolderResultSuccessful,
-                                        true, 1);
-  } else {
-    histogram_tester.ExpectUniqueSample(kDriveSearchFolderResultSuccessful,
-                                        true, 1);
-    histogram_tester.ExpectUniqueSample(kDriveCreateFolderResultSuccessful,
-                                        true, 0);
-  }
+  histogram_tester.ExpectUniqueSample(kDriveFetchClientFolderResultSuccessful,
+                                      true, 1);
   histogram_tester.ExpectUniqueSample(kDriveFileUploadResultSuccessful, false,
                                       1);
   histogram_tester.ExpectUniqueSample(kDriveFileUploadResultErrorCode, 400, 1);
 }
 
-// Tests that `FetchSaveToDriveClientFolder()` is called on the uploader if the
-// corresponding feature is enabled.
-TEST_P(DriveUploadTaskTest, FetchesClientFolderIfFeatureEnabled) {
-  if (!IsParamFeatureEnabled()) {
-    return;
-  }
-
+// Tests that `FetchSaveToDriveClientFolder()` is called on the uploader.
+TEST_F(DriveUploadTaskTest, FetchesClientFolder) {
   base::HistogramTester histogram_tester;
 
   // Set up the uploader to simulate a successful client folder fetch.
@@ -369,11 +177,7 @@
 }
 
 // Tests that if fetching the client folder fails, the task also fails.
-TEST_P(DriveUploadTaskTest, FetchesClientFolderIfFeatureEnabledFailure) {
-  if (!IsParamFeatureEnabled()) {
-    return;
-  }
-
+TEST_F(DriveUploadTaskTest, FetchesClientFolderFailure) {
   base::HistogramTester histogram_tester;
 
   // Set up the uploader to simulate a failed client folder fetch.
@@ -409,7 +213,7 @@
 
 // Tests that a task that is destroyed before being started records the expected
 // histograms.
-TEST_P(DriveUploadTaskTest, TaskNotStartedDestructorRecordsMetrics) {
+TEST_F(DriveUploadTaskTest, TaskNotStartedDestructorRecordsMetrics) {
   base::HistogramTester histogram_tester;
   // Give file to task.
   task_->SetFileToUpload(base::FilePath("file_name.txt"),
@@ -431,7 +235,7 @@
 
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Potential Folder Hijacking in iOS Save to Drive via Shared Folders

Flapjack, 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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The GCRDriveFileUploader class on iOS searches for the ‘Save to Drive’ destination folder without restricting the query to folders owned by the user. An attacker can create a folder using Chrome’s public Client ID, set the expected application properties, and share it with a victim. When the victim uses the ‘Save to Drive’ feature, their files may be silently uploaded to the attacker’s shared folder, leading to potential data exfiltration.

Affected files:

  • google_internal/Source/Drive/GCRDriveFileUploader.m

Estimated timestamp from git blame: Unknown (Google3 checkout)

Description

There is a potential folder hijacking vulnerability in the iOS Chrome “Save to Drive” feature. The GCRDriveFileUploader class identifies the destination folder by searching Google Drive for a specific name and a custom appProperties flag (isSavedFromChromeToDriveFolder). However, the search query fails to restrict the results to folders owned by the current user.

Because Google Drive API appProperties are scoped to the Application ID (OAuth Client ID) and not the individual user, any user authenticated via the same Client ID can view and set these properties. The Chrome iOS Client ID is embedded in the application and easily accessible. An attacker can use this Client ID to create a matching folder and share it with a victim.

In google_internal/Source/Drive/GCRDriveFileUploader.m (lines 72-109), the query is built as follows:

74:   GTLRDriveQuery_FilesList *query = [GTLRDriveQuery_FilesList query];
75:   query.q =
76:       [NSString stringWithFormat:@"mimeType='%@'", ...];
80:   query.q =
81:       [query.q stringByAppendingFormat:@" and name='%@'", ...];
84:   query.q =
85:       [query.q stringByAppendingFormat:@" and appProperties has { key='%@' and value='true' }", ...];
89:   query.q = [query.q stringByAppendingFormat:@" and trashed=false"];
91:   query.orderBy = @"createdTime desc";

The query orders results by createdTime desc and selects the first match (items.files.firstObject.identifier). If an attacker shares a newly created malicious folder with the victim, it will appear at the top of the search results, bypassing the user’s legitimate, older “Saved from Chrome” folder.

Potential Attack Scenario

The following are suggested steps an attacker might take to trigger this issue (note that these are theoretical steps as our tooling cannot run live PoC code):

  1. The attacker extracts the public OAuth Client ID for the Chrome iOS application (e.g., from network traffic or internal configuration files).
  2. The attacker authenticates with the Google Drive API using this Client ID and their own Google account.
  3. The attacker creates a folder with the expected localized name (e.g., “Chrome Uploads”) and sets the isSavedFromChromeToDriveFolder application property to true.
  4. The attacker shares this folder with the victim’s email address, granting them “writer” access.
  5. The victim uses Chrome iOS and triggers a “Save to Drive” action.
  6. searchChromeFolderWithName:completion: executes the query, which includes the attacker’s shared folder.
  7. Due to createdTime desc ordering, the newer attacker folder is selected as the destination.
  8. Chrome uploads the victim’s file to the attacker’s folder, allowing the attacker to view and exfiltrate the victim’s private data.

Suggested Fix

The search query in searchChromeFolderWithName: should explicitly verify folder ownership by adding 'me' in owners to the query string. This ensures that folders shared by third parties are explicitly excluded from the results.

query.q = [query.q stringByAppendingFormat:@" and 'me' in owners"];

Evaluated with Chrome root at commit: HEAD (Google3)


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.

View on issue tracker