High watch CVSS 8.8 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPrivilege elevation in Import
DescriptionPrivilege elevation in Import
ComponentImport
Sub-componentProfileImporter
Bug ClassLogic 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.
Tracker513757918
Fix commit6c1e51af346f (chromium/src) +260/-44
CWECWE-250
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-08-20
Patch gap16 days — fix public in source before the advisory

Background

ProfileImport utility
the desktop utility process, driven by ExternalProcessImporterClient over chrome.mojom.ProfileImport, that reads another browser’s profile data (bookmarks files, Firefox profiles) and streams it back to the browser through an ImporterBridge.
`ParseBookmarksUnsafe()`
the HTML parser for Netscape-format bookmark files; the commit message says it is documented as needing to run inside the dedicated BookmarkHtmlParser service rather than in an arbitrary process.
`user_data_importer::mojom::BookmarkHtmlParser`
a Mojo service, launched via content::ServiceProcessHost::Launch(), whose Parse() method takes raw HTML and returns BookmarkParser::ParsedBookmarks asynchronously, so the untrusted parsing happens in its own process.
`FAVORITES` item
the user_data_importer::FAVORITES bit in the items mask, which selects bookmark import.

Root Cause Analysis

The ProfileImport utility parsed untrusted bookmark HTML in its own process. The code that does this is in BookmarksFileImporter::StartImport (the raw contents of a user-chosen source_profile.source_path) and in FirefoxImporter::ImportBookmarks (the Firefox install’s defaults/.../bookmarks.html). Both called user_data_importer::ParseBookmarksUnsafe(raw_html) directly, and the removed lines show this. That broke the documented rule that this parser runs only inside the isolated BookmarkHtmlParser service.

As a result, any memory-safety or logic bug in the HTML parser was exposed in the import utility, a process with different privileges (the advisory calls the result “privilege elevation in Import”). The diff does not show the import utility’s sandbox type. The sandbox.mojom import in profile_import.mojom suggests a sandbox annotation exists, but whether that process is weaker than the parser service is (inferred).

The fix makes ExternalProcessImporterClient launch a BookmarkHtmlParser service whenever items_ & FAVORITES is set, and pass its remote through a new optional StartImport parameter. Both importers now CHECK(html_parser_remote_.is_valid()) and send the HTML to Parse() over Mojo. They handle the result in new callbacks (OnBookmarksParsed, OnDefaultBookmarksParsed). Most of the remaining diff restructures FirefoxImporter so it keeps its sql::Database, livemark set and items_ alive across the async reply. The new ImportRemainingItems() then continues with passwords and autofill after bookmarks finish.

Key insight
The import utility ran a parser labelled “unsafe” on attacker-influenced bookmark HTML in-process, which broke that parser’s isolation rule. The fix sends that parsing to a separate BookmarkHtmlParser service process and accepts only the structured ParsedBookmarks result back.

Attack Path

  1. Craft bookmark HTML An attacker prepares a malicious Netscape-format bookmarks HTML file and gets the victim to obtain it, for example by download.
  2. User-initiated import The victim chooses to import bookmarks from that HTML file, or from a Firefox install whose default bookmarks.html the attacker controls. Either way, ExternalProcessImporterClient starts the ProfileImport utility with the FAVORITES item. This needs a user action in the browser’s import UI on desktop, not web content.
  3. In-process unsafe parse Before the fix, BookmarksFileImporter::StartImport or FirefoxImporter::ImportBookmarks read the file and called ParseBookmarksUnsafe() inside the import utility.
  4. Parser bug in the wrong process A flaw triggered in the HTML parser would then run with the import utility’s privileges rather than inside the isolated parser service. Whether this gives code execution or access beyond the parser’s sandbox is (inferred); the patch does not show a specific parser bug.

Impact Assessment

An attacker who controls a bookmarks HTML file that the user chooses to import could reach the bookmark HTML parser inside the desktop ProfileImport utility instead of the isolated BookmarkHtmlParser service. The advisory describes this as privilege elevation in Import. Turning it into code execution with the import process’s privileges needs a separate parser bug, and both that bug and the resulting escalation are (inferred). Preconditions are a desktop build, a user-initiated import of bookmarks from an HTML file or Firefox, and an attacker-supplied file. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.

Changed Functions

FunctionChangeNotes
if
chrome/browser/importer/external_process_importer_client.cc
modified
BookmarksFileImporter
chrome/utility/importer/bookmarks_file_importer.h
modified
for
chrome/utility/importer/firefox_importer.cc
modified
while
chrome/utility/importer/firefox_importer.cc
modified
BookmarkHtmlParser
chrome/utility/importer/firefox_importer.h
modified
GURL
chrome/utility/importer/firefox_importer.h
modified

Files Changed

  • chrome/utility/importer/bookmarks_file_importer.cc
  • chrome/utility/importer/importer.h
  • chrome/browser/importer/external_process_importer_client.cc
  • chrome/utility/importer/firefox_importer_unittest.cc
  • chrome/utility/services.cc
  • chrome/common/importer/BUILD.gn
  • chrome/utility/importer/profile_import_impl.h
  • chrome/utility/importer/firefox_importer.cc
  • chrome/utility/BUILD.gn
  • chrome/utility/importer/bookmarks_file_importer.h
  • chrome/utility/importer/bookmarks_file_importer_unittest.cc
  • chrome/common/importer/profile_import.mojom
  • chrome/utility/importer/profile_import_impl.cc
  • chrome/utility/importer/importer.cc
  • chrome/utility/importer/firefox_importer.h
  • chrome/browser/importer/DEPS

Audit Directions

  • "Unsafe" parsers called outside their service
    Grep for *Unsafe( helpers and parsers documented as “must run in a sandboxed service”, such as JSON, XML, HTML, image and archive parsers. Check for direct callers in browser or utility code that skip the intended ServiceProcessHost service.
  • Utility processes with weak sandboxes handling untrusted files
    Review other chrome/utility importers (Safari, Edge, Chromium-profile, CSV password import) and any service marked kNoSandbox or with a relaxed sandbox. Check that parsing of attacker-supplied file contents is delegated rather than done in-process.
  • Sync-to-async refactors
    When a synchronous step is converted to a Mojo callback, as in FirefoxImporter, check object lifetimes (refcounting via WrapRefCounted, ownership of sql::Database), cancellation, and that every early-return path still emits the matching NotifyItemEnded/NotifyEnded calls.
From 6c1e51af346f2ec01590fc45ea5b51eade1d2a66 Mon Sep 17 00:00:00 2001
From: Marc Treib <treib@chromium.org>
Date: Tue, 04 Aug 2026 07:37:27 -0700
Subject: [PATCH] ProfileImporter: Use mojom::BookmarkHtmlParser

ParseBookmarksUnsafe() is documented as needing to run inside the
BookmarkHtmlParser service, but the desktop ProfileImport utility still
called it directly from BookmarksFileImporter and from FirefoxImporter's
default-bookmarks loader.

After this CL, ExternalProcessImporterClient launches a
BookmarkHtmlParser service alongside the ProfileImport service itself,
and passes a remote to it through StartImport(). The Importer base class
now has a SetBookmarkHtmlParser() method to facilitate parsing over
Mojo, and both relevant importer implementations are updated to use the
new bookmarks parser and wait for the reply from the async Mojo
callback.

Fixed: 513757918
Change-Id: Ic2268611ccb446f75751d487e6ab59a26a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7864321
Reviewed-by: Filipa Senra <fsenra@google.com>
Reviewed-by: Dominic Farolino <dom@chromium.org>
Commit-Queue: Marc Treib <treib@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1673326}
---
diff --git a/chrome/browser/importer/external_process_importer_client.cc b/chrome/browser/importer/external_process_importer_client.cc
index 412452d7..cf31971 100644
--- a/chrome/browser/importer/external_process_importer_client.cc
+++ b/chrome/browser/importer/external_process_importer_client.cc
@@ -16,6 +16,7 @@
 #include "chrome/grit/generated_resources.h"
 #include "components/strings/grit/components_strings.h"
 #include "components/user_data_importer/common/imported_bookmark_entry.h"
+#include "components/user_data_importer/mojom/bookmark_html_parser.mojom.h"
 #include "content/public/browser/service_process_host.h"
 #include "ui/base/l10n/l10n_util.h"
 
@@ -69,10 +70,22 @@
       IDS_BOOKMARK_BAR_FOLDER_NAME,
       l10n_util::GetStringUTF8(IDS_BOOKMARK_BAR_FOLDER_NAME));
 
+  mojo::PendingRemote<user_data_importer::mojom::BookmarkHtmlParser>
+      bookmark_html_parser;
+  // Note: `FAVORITES` corresponds to bookmarks.
+  if (items_ & user_data_importer::FAVORITES) {
+    content::ServiceProcessHost::Launch(
+        bookmark_html_parser.InitWithNewPipeAndPassReceiver(),
+        content::ServiceProcessHost::Options()
+            .WithDisplayName(IDS_CONTENT_BOOKMARK_PARSER_SERVICE_DISPLAY_NAME)
+            .Pass());
+  }
+
   // If the utility process hasn't started yet the message will queue until it
   // does.
   profile_import_->StartImport(source_profile_, items_, localized_strings,
-                               receiver_.BindNewPipeAndPassRemote());
+                               receiver_.BindNewPipeAndPassRemote(),
+                               std::move(bookmark_html_parser));
 }
 
 void ExternalProcessImporterClient::Cancel() {
diff --git a/chrome/common/importer/profile_import.mojom b/chrome/common/importer/profile_import.mojom
index b28b732..815b911 100644
--- a/chrome/common/importer/profile_import.mojom
+++ b/chrome/common/importer/profile_import.mojom
@@ -4,6 +4,7 @@
 
 module chrome.mojom;
 
+import "components/user_data_importer/mojom/bookmark_html_parser.mojom";
 import "mojo/public/mojom/base/string16.mojom";
 import "sandbox/policy/mojom/sandbox.mojom";
 import "url/mojom/url.mojom";
@@ -93,7 +94,9 @@
       SourceProfile source_profile,
       uint16 items,
       map<uint32, string> localized_strings,
-      pending_remote<ProfileImportObserver> observer);
+      pending_remote<ProfileImportObserver> observer,
+      pending_remote<user_data_importer.mojom.BookmarkHtmlParser>?
+          bookmark_html_parser);
 
   // Stop the importer.
   CancelImport();
diff --git a/chrome/utility/importer/bookmarks_file_importer.cc b/chrome/utility/importer/bookmarks_file_importer.cc
index 6603970..18840df6 100644
--- a/chrome/utility/importer/bookmarks_file_importer.cc
+++ b/chrome/utility/importer/bookmarks_file_importer.cc
@@ -21,8 +21,10 @@
 #include "components/user_data_importer/common/imported_bookmark_entry.h"
 #include "components/user_data_importer/common/importer_data_types.h"
 #include "components/user_data_importer/content/content_bookmark_parser_utils.h"
+#include "components/user_data_importer/mojom/bookmark_html_parser.mojom.h"
 #include "components/user_data_importer/utility/bookmark_parser.h"
 #include "content/public/common/url_constants.h"
+#include "mojo/public/cpp/bindings/remote.h"
 
 namespace internal {
 
@@ -82,6 +84,11 @@
 
 BookmarksFileImporter::~BookmarksFileImporter() = default;
 
+void BookmarksFileImporter::SetBookmarkHtmlParser(
+    mojo::PendingRemote<user_data_importer::mojom::BookmarkHtmlParser> parser) {
+  html_parser_remote_ = std::move(parser);
+}
+
 void BookmarksFileImporter::StartImport(
     const user_data_importer::SourceProfile& source_profile,
     uint16_t items,
@@ -99,9 +106,24 @@
   // ReadFileToString can return false, but still populate something into
   // `raw_html`. In that case, try to recover as much data as possible.
   base::ReadFileToString(source_profile.source_path, &raw_html);
-  user_data_importer::BookmarkParser::ParsedBookmarks parsed_bookmarks =
-      user_data_importer::ParseBookmarksUnsafe(raw_html);
 
+  CHECK(html_parser_remote_.is_valid());
+  auto html_parser = std::make_unique<
+      mojo::Remote<user_data_importer::mojom::BookmarkHtmlParser>>(
+      std::move(html_parser_remote_));
+
+  auto* raw_parser = html_parser.get();
+  (*raw_parser)
+      ->Parse(
+          raw_html,
+          base::BindOnce(&BookmarksFileImporter::OnBookmarksParsed,
+                         base::WrapRefCounted(this), std::move(html_parser)));
+}
+
+void BookmarksFileImporter::OnBookmarksParsed(
+    std::unique_ptr<mojo::Remote<user_data_importer::mojom::BookmarkHtmlParser>>
+        html_parser,
+    user_data_importer::BookmarkParser::ParsedBookmarks parsed_bookmarks) {
   if (!parsed_bookmarks.bookmarks.empty()) {
     std::u16string first_folder_name =
         bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP);
diff --git a/chrome/utility/importer/bookmarks_file_importer.h b/chrome/utility/importer/bookmarks_file_importer.h
index 1b0adbe..3371999 100644
--- a/chrome/utility/importer/bookmarks_file_importer.h
+++ b/chrome/utility/importer/bookmarks_file_importer.h
@@ -8,6 +8,10 @@
 #include <stdint.h>
 
 #include "chrome/utility/importer/importer.h"
+#include "components/user_data_importer/mojom/bookmark_html_parser.mojom-forward.h"
+#include "components/user_data_importer/utility/bookmark_parser.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
 
 // Importer for bookmarks files.
 class BookmarksFileImporter : public Importer {
@@ -21,8 +25,20 @@
                    uint16_t items,
                    ImporterBridge* bridge) override;
 
+  void SetBookmarkHtmlParser(
+      mojo::PendingRemote<user_data_importer::mojom::BookmarkHtmlParser> parser)
+      override;
+
  private:
   ~BookmarksFileImporter() override;
+
+  void OnBookmarksParsed(
+      std::unique_ptr<mojo::Remote<
+          user_data_importer::mojom::BookmarkHtmlParser>> html_parser,
+      user_data_importer::BookmarkParser::ParsedBookmarks parsed_bookmarks);
+
+  mojo::PendingRemote<user_data_importer::mojom::BookmarkHtmlParser>
+      html_parser_remote_;
 };
 
 #endif  // CHROME_UTILITY_IMPORTER_BOOKMARKS_FILE_IMPORTER_H_
diff --git a/chrome/utility/importer/firefox_importer.cc b/chrome/utility/importer/firefox_importer.cc
index 9e8a8d5..e2bf3edc 100644
--- a/chrome/utility/importer/firefox_importer.cc
+++ b/chrome/utility/importer/firefox_importer.cc
@@ -25,7 +25,9 @@
 #include "components/user_data_importer/common/importer_url_row.h"
 #include "components/user_data_importer/content/content_bookmark_parser_utils.h"
 #include "components/user_data_importer/content/favicon_reencode.h"
+#include "components/user_data_importer/mojom/bookmark_html_parser.mojom.h"
 #include "components/user_data_importer/utility/bookmark_parser.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "sql/database.h"
 #include "sql/statement.h"
 #include "url/gurl.h"
@@ -115,6 +117,7 @@
   bridge_ = bridge;
   source_path_ = source_profile.source_path;
   app_path_ = source_profile.app_path;
+  items_ = items;
 
 #if BUILDFLAG(IS_POSIX)
   locale_ = source_profile.locale;
@@ -144,17 +147,21 @@
   if ((items & user_data_importer::FAVORITES) && !cancelled()) {
     bridge_->NotifyItemStarted(user_data_importer::FAVORITES);
     ImportBookmarks();
-    bridge_->NotifyItemEnded(user_data_importer::FAVORITES);
+    return;
   }
 
+  ImportRemainingItems();
+}
+
+void FirefoxImporter::ImportRemainingItems() {
 #if !BUILDFLAG(IS_MAC)
-  if ((items & user_data_importer::PASSWORDS) && !cancelled()) {
+  if ((items_ & user_data_importer::PASSWORDS) && !cancelled()) {
     bridge_->NotifyItemStarted(user_data_importer::PASSWORDS);
     ImportPasswords();
     bridge_->NotifyItemEnded(user_data_importer::PASSWORDS);
   }
 #endif  // !BUILDFLAG(IS_MAC)
-  if ((items & user_data_importer::AUTOFILL_FORM_DATA) && !cancelled()) {
+  if ((items_ & user_data_importer::AUTOFILL_FORM_DATA) && !cancelled()) {
     bridge_->NotifyItemStarted(user_data_importer::AUTOFILL_FORM_DATA);
     ImportAutofillFormData();
     bridge_->NotifyItemEnded(user_data_importer::AUTOFILL_FORM_DATA);
@@ -213,32 +220,41 @@
                              user_data_importer::VISIT_SOURCE_FIREFOX_IMPORTED);
 }
 
+void FirefoxImporter::SetBookmarkHtmlParser(
+    mojo::PendingRemote<user_data_importer::mojom::BookmarkHtmlParser> parser) {
+  html_parser_remote_ = std::move(parser);
+}
+
 void FirefoxImporter::ImportBookmarks() {
   base::FilePath sqlite_file = GetCopiedSourcePath("places.sqlite");
   if (!base::PathExists(sqlite_file)) {
+    bridge_->NotifyItemEnded(user_data_importer::FAVORITES);
+    ImportRemainingItems();
     return;
   }
 
-  sql::Database db(kDatabaseTag);
-  if (!db.Open(sqlite_file)) {
+  auto db = std::make_unique<sql::Database>(kDatabaseTag);
+  if (!db->Open(sqlite_file)) {
+    bridge_->NotifyItemEnded(user_data_importer::FAVORITES);
+    ImportRemainingItems();
     return;
   }
 
   // |moz_favicons| table has been introduced in Firefox 55 and is not available
   // in older Firefox profiles.
   FaviconsLocation favicons_location =
-      db.IsSQLValid("SELECT count(*) FROM moz_favicons")
+      db->IsSQLValid("SELECT count(*) FROM moz_favicons")
           ? FaviconsLocation::kPlacesDatabase
           : FaviconsLocation::kFaviconsDatabase;
 
   // Get the bookmark folders that we are interested in.
-  int toolbar_folder_id = LoadNodeIDByGUID(&db, "toolbar_____");
-  int menu_folder_id = LoadNodeIDByGUID(&db, "menu________");
-  int unsorted_folder_id = LoadNodeIDByGUID(&db, "unfiled_____");
+  int toolbar_folder_id = LoadNodeIDByGUID(db.get(), "toolbar_____");
+  int menu_folder_id = LoadNodeIDByGUID(db.get(), "menu________");
+  int unsorted_folder_id = LoadNodeIDByGUID(db.get(), "unfiled_____");
 
   // Load livemark IDs.
-  std::set<int> livemark_id;
-  LoadLivemarkIDs(&db, &livemark_id);
+  auto livemark_id = std::make_unique<std::set<int>>();
+  LoadLivemarkIDs(db.get(), livemark_id.get());
 
   // Load the default bookmarks.
   base::FilePath bookmarks_file = app_path_.AppendASCII("defaults")
@@ -249,21 +265,44 @@
   // ReadFileToString can return false, but still populate something into
   // `raw_html`. In that case, try to recover as much data as possible.
   base::ReadFileToString(bookmarks_file, &raw_html);
-  user_data_importer::BookmarkParser::ParsedBookmarks default_bookmarks =
-      user_data_importer::ParseBookmarksUnsafe(raw_html);
 
+  CHECK(html_parser_remote_.is_valid());
+  auto html_parser = std::make_unique<
+      mojo::Remote<user_data_importer::mojom::BookmarkHtmlParser>>(
+      std::move(html_parser_remote_));
+
+  auto* raw_parser = html_parser.get();
+  (*raw_parser)
+      ->Parse(raw_html,
+              base::BindOnce(&FirefoxImporter::OnDefaultBookmarksParsed,
+                             base::WrapRefCounted(this), std::move(db),
+                             toolbar_folder_id, menu_folder_id,
+                             unsorted_folder_id, std::move(livemark_id),
+                             favicons_location, std::move(html_parser)));
+}
+
+void FirefoxImporter::OnDefaultBookmarksParsed(
+    std::unique_ptr<sql::Database> db,
+    int toolbar_folder_id,
+    int menu_folder_id,
+    int unsorted_folder_id,
+    std::unique_ptr<std::set<int>> livemark_id,
+    FaviconsLocation favicons_location,
+    std::unique_ptr<mojo::Remote<user_data_importer::mojom::BookmarkHtmlParser>>
+        html_parser,
+    user_data_importer::BookmarkParser::ParsedBookmarks default_bookmarks) {
   std::set<GURL> default_urls;
   for (const auto& bookmark : default_bookmarks.bookmarks) {
     default_urls.insert(bookmark.url);
   }
 
   BookmarkList list;
-  GetTopBookmarkFolder(&db, toolbar_folder_id, &list);
-  GetTopBookmarkFolder(&db, menu_folder_id, &list);
-  GetTopBookmarkFolder(&db, unsorted_folder_id, &list);
+  GetTopBookmarkFolder(db.get(), toolbar_folder_id, &list);
+  GetTopBookmarkFolder(db.get(), menu_folder_id, &list);
+  GetTopBookmarkFolder(db.get(), unsorted_folder_id, &list);
   size_t count = list.size();
   for (size_t i = 0; i < count; ++i) {
-    GetWholeBookmarkFolder(&db, &list, i, favicons_location, nullptr);
+    GetWholeBookmarkFolder(db.get(), &list, i, favicons_location, nullptr);
   }
 
   std::vector<user_data_importer::ImportedBookmarkEntry> bookmarks;
@@ -278,9 +317,11 @@
       "INNER JOIN moz_items_annos ia ON ia.item_id = b.id "
       "INNER JOIN moz_anno_attributes aa ON ia.anno_attribute_id = aa.id "
       "WHERE aa.name = 'bookmarkProperties/POSTData'";
-  sql::Statement s(db.GetUniqueStatement(query));
+  sql::Statement s(db->GetUniqueStatement(query));
 
   if (!s.is_valid()) {
+    bridge_->NotifyItemEnded(user_data_importer::FAVORITES);
+    ImportRemainingItems();
     return;
   }
 
@@ -310,7 +351,7 @@
       bool is_in_toolbar = false;
       while (child->parent >= 0) {
         BookmarkItem* parent = list[child->parent].get();
-        if (livemark_id.find(parent->id) != livemark_id.end()) {
+        if (livemark_id->find(parent->id) != livemark_id->end()) {
           // Don't import live bookmarks.
           break;
         }
@@ -394,12 +435,15 @@
       DCHECK(favicon_map.empty());
       LoadFavicons(bookmarks, &favicons);
     } else if (!favicon_map.empty()) {
-      LoadFavicons(&db, favicon_map, &favicons);
+      LoadFavicons(db.get(), favicon_map, &favicons);
     }
     if (!favicons.empty()) {
       bridge_->SetFavicons(favicons);
     }
   }
+
+  bridge_->NotifyItemEnded(user_data_importer::FAVORITES);
+  ImportRemainingItems();
 }
 
 #if !BUILDFLAG(IS_MAC)
@@ -577,7 +621,7 @@
     item->title = s.ColumnString16(2);
     item->type = static_cast<BookmarkItemType>(s.ColumnInt(3));
     item->keyword = s.ColumnString(4);
-    item->date_added = base::Time::FromTimeT(s.ColumnInt64(5)/1000000);
+    item->date_added = base::Time::FromTimeT(s.ColumnInt64(5) / 1000000);
     item->favicon = favicons_location == FaviconsLocation::kPlacesDatabase
                         ? s.ColumnInt64(6)
                         : 0;
diff --git a/chrome/utility/importer/firefox_importer.h b/chrome/utility/importer/firefox_importer.h
index eabc8359..fea3060 100644
--- a/chrome/utility/importer/firefox_importer.h
+++ b/chrome/utility/importer/firefox_importer.h
@@ -23,6 +23,13 @@
 #include "chrome/utility/importer/importer.h"
 #include "components/favicon_base/favicon_usage_data.h"
 #include "components/user_data_importer/common/imported_bookmark_entry.h"
+#include "components/user_data_importer/utility/bookmark_parser.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+
+namespace user_data_importer::mojom {
+class BookmarkHtmlParser;
+}  // namespace user_data_importer::mojom
 
 class GURL;
 
@@ -45,6 +52,10 @@
                    uint16_t items,
                    ImporterBridge* bridge) override;
 
+  void SetBookmarkHtmlParser(
+      mojo::PendingRemote<user_data_importer::mojom::BookmarkHtmlParser> parser)
+      override;
+
  private:
   // Location of favicons in Firefox profile. It may vary depending on Firefox
   // version.
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/utility/importer/bookmarks_file_importer_unittest.cc b/chrome/utility/importer/bookmarks_file_importer_unittest.cc
index 253407b..cb786262 100644
--- a/chrome/utility/importer/bookmarks_file_importer_unittest.cc
+++ b/chrome/utility/importer/bookmarks_file_importer_unittest.cc
@@ -11,12 +11,16 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/memory/ref_counted.h"
+#include "base/run_loop.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/time/time.h"
 #include "chrome/common/importer/importer_autofill_form_data_entry.h"
 #include "chrome/common/importer/importer_bridge.h"
 #include "components/user_data_importer/common/imported_bookmark_entry.h"
 #include "components/user_data_importer/common/importer_data_types.h"
+#include "components/user_data_importer/content/fake_bookmark_html_parser.h"
+#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/receiver.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
@@ -87,8 +91,15 @@
   ~MockImporterBridge() override = default;
 };
 
+class BookmarksFileImporterTest : public testing::Test {
+ protected:
+  content::BrowserTaskEnvironment task_environment_;
+  user_data_importer::FakeBookmarkHtmlParser fake_parser_;
+  mojo::Receiver<user_data_importer::mojom::BookmarkHtmlParser> receiver_{
+      &fake_parser_};
+};
 
-TEST(BookmarksFileImporterTest, CanImportURL) {
+TEST_F(BookmarksFileImporterTest, CanImportURL) {
   struct TestCase {
     const std::string url;
     const bool can_be_imported;
@@ -117,7 +128,7 @@
   }
 }
 
-TEST(BookmarksFileImporterTest, ImportBookmarks) {
+TEST_F(BookmarksFileImporterTest, ImportBookmarks) {
   base::ScopedTempDir temp_dir;
   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
   base::FilePath file_path = temp_dir.GetPath().AppendASCII("bookmarks.html");
@@ -148,15 +159,19 @@
   bookmark.creation_time = base::Time::UnixEpoch() + base::Seconds(456);
   expected_bookmarks.push_back(bookmark);
 
+  base::RunLoop run_loop;
   EXPECT_CALL(*bridge, NotifyStarted());
-  EXPECT_CALL(*bridge, NotifyEnded());
+  EXPECT_CALL(*bridge, NotifyEnded())
+      .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
   EXPECT_CALL(*bridge, AddBookmarks(expected_bookmarks, _));
 
+  importer->SetBookmarkHtmlParser(receiver_.BindNewPipeAndPassRemote());
   importer->StartImport(source_profile, user_data_importer::FAVORITES,
                         bridge.get());
+  run_loop.Run();
 }
 
-TEST(BookmarksFileImporterTest, ImportEmptyFile) {
+TEST_F(BookmarksFileImporterTest, ImportEmptyFile) {
   base::ScopedTempDir temp_dir;
   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
   base::FilePath file_path = temp_dir.GetPath().AppendASCII("bookmarks.html");
@@ -168,17 +183,21 @@
   user_data_importer::SourceProfile source_profile;
   source_profile.source_path = file_path;
 
+  base::RunLoop run_loop;
   EXPECT_CALL(*bridge, NotifyStarted());
-  EXPECT_CALL(*bridge, NotifyEnded());
+  EXPECT_CALL(*bridge, NotifyEnded())
+      .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
   EXPECT_CALL(*bridge, AddBookmarks(_, _)).Times(0);
   EXPECT_CALL(*bridge, SetKeywords(_, _)).Times(0);
   EXPECT_CALL(*bridge, SetFavicons(_)).Times(0);
 
+  importer->SetBookmarkHtmlParser(receiver_.BindNewPipeAndPassRemote());
   importer->StartImport(source_profile, user_data_importer::FAVORITES,
                         bridge.get());
+  run_loop.Run();
 }
 
-TEST(BookmarksFileImporterTest, ImportWithInvalidBookmarks) {
+TEST_F(BookmarksFileImporterTest, ImportWithInvalidBookmarks) {
   base::ScopedTempDir temp_dir;
   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
   base::FilePath file_path = temp_dir.GetPath().AppendASCII("bookmarks.html");
@@ -204,15 +223,19 @@
   bookmark.creation_time = base::Time::UnixEpoch() + base::Seconds(123);
   expected_bookmarks.push_back(bookmark);
 
+  base::RunLoop run_loop;
   EXPECT_CALL(*bridge, NotifyStarted());
-  EXPECT_CALL(*bridge, NotifyEnded());
+  EXPECT_CALL(*bridge, NotifyEnded())
+      .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
   EXPECT_CALL(*bridge, AddBookmarks(expected_bookmarks, _));
 
+  importer->SetBookmarkHtmlParser(receiver_.BindNewPipeAndPassRemote());
   importer->StartImport(source_profile, user_data_importer::FAVORITES,
                         bridge.get());
+  run_loop.Run();
 }
 
-TEST(BookmarksFileImporterTest, ImportSearchEngine) {
+TEST_F(BookmarksFileImporterTest, ImportSearchEngine) {
   base::ScopedTempDir temp_dir;
   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
   base::FilePath file_path = temp_dir.GetPath().AppendASCII("bookmarks.html");
@@ -235,16 +258,20 @@
   search_engine.display_name = u"Google Search";
   expected_search_engines.push_back(search_engine);
 
+  base::RunLoop run_loop;
   EXPECT_CALL(*bridge, NotifyStarted());
-  EXPECT_CALL(*bridge, NotifyEnded());
+  EXPECT_CALL(*bridge, NotifyEnded())
+      .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
   EXPECT_CALL(*bridge, AddBookmarks(_, _)).Times(0);
   EXPECT_CALL(*bridge, SetKeywords(expected_search_engines, false));
 
+  importer->SetBookmarkHtmlParser(receiver_.BindNewPipeAndPassRemote());
   importer->StartImport(source_profile, user_data_importer::FAVORITES,
                         bridge.get());
+  run_loop.Run();
 }
 
-TEST(BookmarksFileImporterTest, ImportWithFavicon) {
+TEST_F(BookmarksFileImporterTest, ImportWithFavicon) {
   base::ScopedTempDir temp_dir;
   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
   base::FilePath file_path = temp_dir.GetPath().AppendASCII("bookmarks.html");
@@ -261,8 +288,10 @@
   user_data_importer::SourceProfile source_profile;
   source_profile.source_path = file_path;
 
+  base::RunLoop run_loop;
   EXPECT_CALL(*bridge, NotifyStarted());
-  EXPECT_CALL(*bridge, NotifyEnded());
+  EXPECT_CALL(*bridge, NotifyEnded())
+      .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
   EXPECT_CALL(*bridge,
               AddBookmarks(_, _));  // Expecting a bookmark to be added.
   EXPECT_CALL(
@@ -274,7 +303,9 @@
                 Contains(GURL("http://www.google.com/"))),
           Field(&favicon_base::FaviconUsageData::png_data, Not(IsEmpty()))))));
 
+  importer->SetBookmarkHtmlParser(receiver_.BindNewPipeAndPassRemote());
   importer->StartImport(source_profile, user_data_importer::FAVORITES,
                         bridge.get());
+  run_loop.Run();
 }
 }  // namespace
diff --git a/chrome/utility/importer/firefox_importer_unittest.cc b/chrome/utility/importer/firefox_importer_unittest.cc
index 6e202ee..d5ae875 100644
--- a/chrome/utility/importer/firefox_importer_unittest.cc
+++ b/chrome/utility/importer/firefox_importer_unittest.cc
@@ -12,6 +12,7 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/path_service.h"
+#include "base/run_loop.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
 #include "chrome/common/chrome_paths.h"
@@ -20,7 +21,9 @@
 #include "components/user_data_importer/common/imported_bookmark_entry.h"
 #include "components/user_data_importer/common/importer_data_types.h"
 #include "components/user_data_importer/common/importer_url_row.h"
+#include "components/user_data_importer/content/fake_bookmark_html_parser.h"
 #include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/receiver.h"
 #include "sql/database.h"
 #include "sql/test/test_helpers.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -45,6 +48,7 @@
     user_data_importer::SourceProfile profile;
     profile.source_path = places_path;
 
+    base::RunLoop run_loop;
     EXPECT_CALL(*bridge_, NotifyStarted());
     EXPECT_CALL(*bridge_, NotifyItemStarted(user_data_importer::FAVORITES));
     EXPECT_CALL(*bridge_, AddBookmarks(_, _))
@@ -52,17 +56,24 @@
     EXPECT_CALL(*bridge_, SetFavicons(_))
         .WillOnce(::testing::SaveArg<0>(favicons));
     EXPECT_CALL(*bridge_, NotifyItemEnded(user_data_importer::FAVORITES));
-    EXPECT_CALL(*bridge_, NotifyEnded());
+    EXPECT_CALL(*bridge_, NotifyEnded())
+        .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
+
+    importer_->SetBookmarkHtmlParser(receiver_.BindNewPipeAndPassRemote());
     importer_->StartImport(profile, user_data_importer::FAVORITES,
                            bridge_.get());
+    run_loop.Run();
   }
 
- private:
+ protected:
   content::BrowserTaskEnvironment task_environment_;
   scoped_refptr<MockImporterBridge> bridge_ =
       base::MakeRefCounted<MockImporterBridge>();
   scoped_refptr<FirefoxImporter> importer_ =
       base::MakeRefCounted<FirefoxImporter>();
+  user_data_importer::FakeBookmarkHtmlParser fake_parser_;
+  mojo::Receiver<user_data_importer::mojom::BookmarkHtmlParser> receiver_{
+      &fake_parser_};
 };
 
 TEST_F(FirefoxImporterTest, ImportBookmarks_Firefox48) {
@@ -155,14 +166,23 @@
   profile.source_path = source_temp_dir.GetPath();
   scoped_refptr<MockImporterBridge> bridge =
       base::MakeRefCounted<MockImporterBridge>();
+  base::RunLoop run_loop1;
   EXPECT_CALL(*bridge, NotifyStarted());
   EXPECT_CALL(*bridge, NotifyItemStarted(user_data_importer::FAVORITES));
   EXPECT_CALL(*bridge, AddBookmarks(_, _)).Times(0);
   EXPECT_CALL(*bridge, SetFavicons(_)).Times(0);
   EXPECT_CALL(*bridge, NotifyItemEnded(user_data_importer::FAVORITES));
-  EXPECT_CALL(*bridge, NotifyEnded());
+  EXPECT_CALL(*bridge, NotifyEnded())
+      .WillOnce(testing::InvokeWithoutArgs(&run_loop1, &base::RunLoop::Quit));
+
+  user_data_importer::FakeBookmarkHtmlParser fake_parser1;
+  mojo::Receiver<user_data_importer::mojom::BookmarkHtmlParser> receiver1{
+      &fake_parser1};
+  first_importer->SetBookmarkHtmlParser(receiver1.BindNewPipeAndPassRemote());
+
   first_importer->StartImport(profile, user_data_importer::FAVORITES,
                               bridge.get());
+  run_loop1.Run();
 
   // Part 2: Test GetWholeBookmarkFolder validation
   base::ScopedTempDir second_source_dir;
@@ -201,14 +221,23 @@
   second_profile.source_path = second_source_dir.GetPath();
   scoped_refptr<MockImporterBridge> second_bridge =
       base::MakeRefCounted<MockImporterBridge>();
+  base::RunLoop run_loop2;
   EXPECT_CALL(*second_bridge, NotifyStarted());
   EXPECT_CALL(*second_bridge, NotifyItemStarted(user_data_importer::FAVORITES));
   EXPECT_CALL(*second_bridge, AddBookmarks(_, _)).Times(0);
   EXPECT_CALL(*second_bridge, SetFavicons(_)).Times(0);
   EXPECT_CALL(*second_bridge, NotifyItemEnded(user_data_importer::FAVORITES));
-  EXPECT_CALL(*second_bridge, NotifyEnded());
+  EXPECT_CALL(*second_bridge, NotifyEnded())
+      .WillOnce(testing::InvokeWithoutArgs(&run_loop2, &base::RunLoop::Quit));
+
+  user_data_importer::FakeBookmarkHtmlParser fake_parser2;
+  mojo::Receiver<user_data_importer::mojom::BookmarkHtmlParser> receiver2{
+      &fake_parser2};
+  second_importer->SetBookmarkHtmlParser(receiver2.BindNewPipeAndPassRemote());
+
   second_importer->StartImport(second_profile, user_data_importer::FAVORITES,
                                second_bridge.get());
+  run_loop2.Run();
 }
 
 TEST_F(FirefoxImporterTest, ImportHistorySchema) {
Loading diff…

Original Bug Report

reported by vm...@google.com

RCE in Unsandboxed ProfileImport via Blink Image Codecs

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: The ProfileImport utility process runs without a sandbox but processes untrusted bookmark favicons using the complex Blink image-codec stack. This architectural flaw violates the ‘Rule-of-2’ by performing dangerous parsing of untrusted data in a high-privilege process. A malicious bookmarks file could potentially trigger memory corruption in a codec and achieve Remote Code Execution (RCE) at the OS-user privilege level.

Affected files:

  • chrome/utility/importer/bookmarks_file_importer.cc
  • chrome/utility/importer/firefox_importer.cc
  • components/user_data_importer/content/content_bookmark_parser_utils.cc
  • components/user_data_importer/content/favicon_reencode.cc
  • chrome/common/importer/profile_import.mojom
  • chrome/utility/importer/profile_import_impl.cc

Estimated timestamp from git blame: 2025-07-30

Summary

A potential “Rule-of-2” security violation exists in the desktop bookmark import system. When a user imports a bookmarks.html file, the ProfileImport utility process—which is explicitly unsandboxed—decodes favicon data-URLs found in the file using the full Blink image-codec stack. This allows the large attack surface of C++ image codecs (WebP, AVIF, PNG, etc.) to be targeted from a process with OS-user privileges.

Technical Details

  1. Unsandboxed Process: The ProfileImport utility service is configured with [ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox] in chrome/common/importer/profile_import.mojom. This is intended to allow the process to access profile directories of other browsers on the local disk.
  2. Dangerous Parsing Path: In chrome/utility/importer/bookmarks_file_importer.cc, the StartImport method invokes user_data_importer::ParseBookmarksUnsafe(raw_html). This function is documented in components/user_data_importer/content/content_bookmark_parser_utils.h with a contract stating: “This function must be run in a sandboxed process.”
  3. Codec Invocation: Within ParseBookmarksUnsafe, extracted ICON attributes (which are data: URLs) are processed via importer::ReencodeFavicon. This leads to content::DecodeImage, which initializes and runs the Blink image-codec stack (blink::ImageDecoder).
  4. Blink Initialization: The utility process explicitly prepares for this by calling content::UtilityThread::Get()->EnsureBlinkInitialized() in ProfileImportImpl::StartImport (chrome/utility/importer/profile_import_impl.cc).

Potential Security Impact

By embedding a crafted image payload within a bookmark’s ICON attribute, an attacker could potentially trigger a memory corruption vulnerability (such as a heap overflow) in one of the Blink image codecs. Because the ProfileImport process is unsandboxed, successful exploitation would grant the attacker Remote Code Execution (RCE) with the full privileges of the OS user, bypassing the Chrome sandbox entirely.

Potential Trigger Path

  1. An attacker induces a user to download a malicious bookmarks.html file.
  2. The user navigates to chrome://settings/importData, selects “Bookmarks HTML File”, and selects the malicious file.
  3. The Browser process launches the unsandboxed ProfileImport utility process.
  4. The utility process reads the file and invokes the Blink codec stack to decode the malicious ICON data, potentially triggering a vulnerability.

The legacy desktop importers (BookmarksFileImporter and FirefoxImporter) should be refactored to avoid calling ParseBookmarksUnsafe directly in the unsandboxed process. Instead, they should utilize the existing sandboxed mojom::BookmarkHtmlParser service (defined in components/user_data_importer/mojom/bookmark_html_parser.mojom), which is designed to parse this untrusted data within a restricted environment.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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.

View on issue tracker