CVE-2026-113261
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/shell_integration_linux.cc |
modified | |
forchrome/browser/web_applications/os_integration/file_handling_sub_manager.cc |
modified | |
forchrome/browser/web_applications/web_app_install_utils.cc |
modified | |
ifchrome/browser/web_applications/web_app_install_utils.cc |
modified | |
forcontent/browser/manifest/manifest_manager_host.cc |
modified | |
ifnet/base/mime_util.cc |
modified |
Files Changed
third_party/blink/public/mojom/manifest/manifest.mojomchrome/browser/web_applications/web_app_install_utils_unittest.ccchrome/browser/web_applications/os_integration/web_app_shortcut_linux_unittest.ccchrome/browser/web_applications/docs/os_integration.mdchrome/browser/web_applications/os_integration/file_handling_sub_manager.ccchrome/browser/shell_integration_linux.ccnet/base/mime_util.hchrome/test/data/banners/manifest_with_x_scheme_handler_in_file_handlers.jsonnet/base/mime_util.ccchrome/browser/web_applications/web_app_install_utils.ccchrome/browser/web_applications/os_integration/web_app_shortcut_linux.ccthird_party/blink/renderer/modules/manifest/manifest_parser_unittest.ccnet/base/mime_util_unittest.cccontent/browser/manifest/manifest_browsertest.ccchrome/browser/shell_integration_linux_unittest.cccontent/browser/manifest/README.mdthird_party/blink/renderer/modules/manifest/manifest_parser.ccchrome/browser/web_applications/os_integration/file_handling_sub_manager_unittest.cccontent/browser/manifest/manifest_manager_host.ccchrome/browser/web_applications/os_integration/web_app_file_handler_registration_linux_browsertest.cc
Patch
From 64bbb4794b224f6e1bf86b69c9982b62f2e1d151 Mon Sep 17 00:00:00 2001
From: Vincent Scheib <scheib@chromium.org>
Date: Thu, 17 Sep 2026 14:21:24 -0700
Subject: [PATCH] [webapps] Multi-layered validation filtering x-scheme-handler from file handlers
FreeDesktop desktop entry specifications define URL scheme associations
using pseudo-MIME types prefixed with "x-scheme-handler/". Manifest
file_handlers should only associate file media types and must not register
arbitrary protocol schemes.
This CL establishes defense-in-depth across multiple architectural layers:
1. //net/base: Excludes "x-scheme-handler" from net::IsValidTopLevelMimeType().
2. Blink parser: Drops "x-scheme-handler/*" and bare "." file extensions in
manifest parsing.
3. Content IPC: Validates manifest file handlers in ManifestManagerHost,
reporting bad messages for invalid MIME types or extensions.
4. WebAppInstallUtils: Validates MIME types and extension syntax when
populating WebAppInstallInfo in the browser process.
5. Linux OS Integration: Filters "x-scheme-handler" pseudo-types when
generating .desktop files and shared MIME-info registration XML files.
TAG=agy
CONV=66f7f990-d070-4f96-9606-d4f93349ae09
Bug: 515440087
Change-Id: I366d33277f8c97387624cfb70ec792d0cc531686
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8393906
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Commit-Queue: Vincent Scheib <scheib@chromium.org>
Reviewed-by: Thomas Anderson <thomasanderson@chromium.org>
Reviewed-by: Elly <ellyjones@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700130}
---
diff --git a/chrome/browser/shell_integration_linux.cc b/chrome/browser/shell_integration_linux.cc
index 2c071e6..bc6dc20d 100644
--- a/chrome/browser/shell_integration_linux.cc
+++ b/chrome/browser/shell_integration_linux.cc
@@ -785,6 +785,16 @@
for (const auto& file_handler : file_handlers) {
for (const auto& accept_entry : file_handler.accept) {
+ // FreeDesktop URL scheme associations use "x-scheme-handler/*"
+ // pseudo-MIME types in .desktop entries, which belong to protocol
+ // handlers rather than file handlers. Skip them here so they are
+ // filtered out from shared MIME-info registration XML files.
+ if (base::EqualsCaseInsensitiveASCII(accept_entry.mime_type,
+ "x-scheme-handler") ||
+ base::StartsWith(accept_entry.mime_type, "x-scheme-handler/",
+ base::CompareCase::INSENSITIVE_ASCII)) {
+ continue;
+ }
writer.StartElement("mime-type");
writer.AddAttribute("type", accept_entry.mime_type);
diff --git a/chrome/browser/web_applications/docs/os_integration.md b/chrome/browser/web_applications/docs/os_integration.md
index 16d89df4..e5028d1 100644
--- a/chrome/browser/web_applications/docs/os_integration.md
+++ b/chrome/browser/web_applications/docs/os_integration.md
@@ -10,8 +10,8 @@
main responsibility is support the following operations:
1. Install operating system integration for a given web app.
-1. Update operating system integration for a given web app.
-1. Uninstall/remove operating system integration for a given web app.
+2. Update operating system integration for a given web app.
+3. Uninstall/remove operating system integration for a given web app.
It owns sub-managers who are responsible for each individual operating system
integration functionality (e.g.
@@ -125,7 +125,7 @@
For each size, `xdg-icon-resource install` is called to register the icon
globally for the user. This allows the `.desktop` file to reference the icon
by its registered name.
-1. **`.desktop` File Generation**: A `.desktop` file string is generated (via
+2. **`.desktop` File Generation**: A `.desktop` file string is generated (via
`shell_integration_linux::GetDesktopFileContents`). This file contains proper
instructions for the desktop environment, such as executing the browser with
the correct app ID, the app's title, the installed icon's name, and any file
@@ -137,7 +137,7 @@
icon).
- `NoDisplay=true`: Included if the shortcut is meant to be hidden from
user-facing menus.
-1. **Location-Specific Installation**:
+3. **Location-Specific Installation**:
- **Desktop**: The `.desktop` file is written directly to the user's desktop
directory (e.g., `~/Desktop`).
- **Run on OS Login (Autostart)**: If the app is configured to launch on OS
@@ -158,7 +158,16 @@
1. An XML string containing the MIME type registrations is generated and written
to a temporary file.
-1. `xdg-mime install` is executed to register the MIME types with the system.
-1. Finally, as with shortcut creation, `update-desktop-database` is called to
+2. `xdg-mime install` is executed to register the MIME types with the system.
+3. Finally, as with shortcut creation, `update-desktop-database` is called to
refresh the desktop cache so that the new file associations take effect
immediately in the user's file manager.
+
+> **Note on FreeDesktop Protocol vs. File Handlers:** Under FreeDesktop
+> specifications, URL scheme associations are stored in `.desktop` entries using
+> pseudo-MIME types prefixed with `x-scheme-handler/` (e.g.
+> `x-scheme-handler/mailto`). These are exclusively protocol handlers and are
+> filtered out during file handler registration, so they never appear in
+> generated shared MIME-info XML files or file handler `.desktop` shortcuts.
+> Legitimate `x-scheme-handler/*` associations are populated solely from
+> validated `protocol_handlers`.
diff --git a/chrome/browser/web_applications/os_integration/file_handling_sub_manager.cc b/chrome/browser/web_applications/os_integration/file_handling_sub_manager.cc
index 4833870..23793d0b 100644
--- a/chrome/browser/web_applications/os_integration/file_handling_sub_manager.cc
+++ b/chrome/browser/web_applications/os_integration/file_handling_sub_manager.cc
@@ -10,6 +10,7 @@
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
+#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/web_applications/os_integration/os_integration_test_override.h"
#include "chrome/browser/web_applications/os_integration/web_app_file_handler_registration.h"
@@ -59,6 +60,12 @@
os_integration_state.file_handling().file_handlers_size() > 0);
}
+bool IsSchemeHandlerMimeType(std::string_view mime_type) {
+ return base::EqualsCaseInsensitiveASCII(mime_type, "x-scheme-handler") ||
+ base::StartsWith(mime_type, "x-scheme-handler/",
+ base::CompareCase::INSENSITIVE_ASCII);
+}
+
} // namespace
std::set<std::string> GetFileExtensionsFromFileHandlingProto(
@@ -80,7 +87,12 @@
std::set<std::string> mime_types;
for (const auto& file_handler : file_handling.file_handlers()) {
for (const auto& accept_entry : file_handler.accept()) {
- mime_types.insert(accept_entry.mimetype());
+ // "x-scheme-handler/*" is a FreeDesktop pseudo-MIME type used for URL
+ // scheme associations, not a media type for file handling. Exclude it
+ // when extracting file handling MIME types.
+ if (!IsSchemeHandlerMimeType(accept_entry.mimetype())) {
+ mime_types.insert(accept_entry.mimetype());
+ }
}
}
return mime_types;
@@ -122,15 +134,20 @@
// checks above.
for (const auto& file_handler :
*provider_->registrar_unsafe().GetAppFileHandlers(app_id)) {
- proto::os_state::FileHandling::FileHandler* file_handler_proto =
- os_file_handling->add_file_handlers();
+ proto::os_state::FileHandling::FileHandler file_handler_proto;
DCHECK(file_handler.action.is_valid());
- file_handler_proto->set_action(file_handler.action.spec());
- file_handler_proto->set_display_name(
+ file_handler_proto.set_action(file_handler.action.spec());
+ file_handler_proto.set_display_name(
base::UTF16ToUTF8(file_handler.display_name));
for (const auto& accept_entry : file_handler.accept) {
- auto* accept_entry_proto = file_handler_proto->add_accept();
+ // Exclude "x-scheme-handler/*" pseudo-MIME types so that URL scheme
+ // registrations cannot be injected via file handlers into OS file
+ // handling integration.
+ if (IsSchemeHandlerMimeType(accept_entry.mime_type)) {
+ continue;
+ }
+ auto* accept_entry_proto = file_handler_proto.add_accept();
accept_entry_proto->set_mimetype(accept_entry.mime_type);
for (const auto& file_extension : accept_entry.file_extensions) {
if (provider_->registrar_unsafe().GetAppFileHandlerApprovalState(
@@ -139,6 +156,10 @@
}
}
}
+
+ if (file_handler_proto.accept_size() > 0) {
+ *os_file_handling->add_file_handlers() = std::move(file_handler_proto);
+ }
}
std::move(configure_done).Run();
diff --git a/chrome/browser/web_applications/os_integration/web_app_shortcut_linux.cc b/chrome/browser/web_applications/os_integration/web_app_shortcut_linux.cc
index 4754ad610..1c8d206 100644
--- a/chrome/browser/web_applications/os_integration/web_app_shortcut_linux.cc
+++ b/chrome/browser/web_applications/os_integration/web_app_shortcut_linux.cc
@@ -597,9 +597,19 @@
NOTREACHED();
}
- std::vector<std::string> mime_types(
- shortcut_info.file_handler_mime_types.begin(),
- shortcut_info.file_handler_mime_types.end());
+ std::vector<std::string> mime_types;
+ // Filter out any "x-scheme-handler/*" pseudo-MIME types from file handlers.
+ // In FreeDesktop .desktop files, "x-scheme-handler/*" entries associate URL
+ // schemes and must only originate from validated protocol_handlers below.
+ for (const auto& file_handler_mime_type :
+ shortcut_info.file_handler_mime_types) {
+ if (!base::EqualsCaseInsensitiveASCII(file_handler_mime_type,
+ "x-scheme-handler") &&
+ !base::StartsWith(file_handler_mime_type, "x-scheme-handler/",
+ base::CompareCase::INSENSITIVE_ASCII)) {
+ mime_types.push_back(file_handler_mime_type);
+ }
+ }
// Convert protocol handlers into mime types for registration in the
// .desktop file.
diff --git a/chrome/browser/web_applications/web_app_install_utils.cc b/chrome/browser/web_applications/web_app_install_utils.cc
index 2f2e2c21..c3378ad 100644
--- a/chrome/browser/web_applications/web_app_install_utils.cc
+++ b/chrome/browser/web_applications/web_app_install_utils.cc
@@ -58,6 +58,7 @@
#include "components/webapps/browser/installable/installable_metrics.h"
#include "content/public/common/content_features.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
+#include "net/base/mime_util.h"
#include "net/http/http_util.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/manifest/manifest.h"
@@ -317,7 +318,9 @@
apps::FileHandlers web_app_file_handlers;
for (const auto& manifest_file_handler : manifest_file_handlers) {
- DCHECK(manifest_file_handler);
+ if (!manifest_file_handler) {
+ continue;
+ }
apps::FileHandler web_app_file_handler;
web_app_file_handler.action = manifest_file_handler->action;
web_app_file_handler.display_name = manifest_file_handler->name;
@@ -325,16 +328,30 @@
ToFileHandlerLaunchType(manifest_file_handler->launch_type);
for (const auto& it : manifest_file_handler->accept) {
- apps::FileHandler::AcceptEntry web_app_accept_entry;
- web_app_accept_entry.mime_type = base::UTF16ToUTF8(it.first);
- for (const auto& manifest_file_extension : it.second) {
- web_app_accept_entry.file_extensions.insert(
- base::UTF16ToUTF8(manifest_file_extension));
+ std::string mime_type = base::UTF16ToUTF8(it.first);
+ std::string top_level_mime_type;
+ if (!net::ParseMimeTypeWithoutParameter(mime_type, &top_level_mime_type,
+ nullptr) ||
+ !net::IsValidTopLevelMimeType(top_level_mime_type)) {
+ continue;
}
- web_app_file_handler.accept.push_back(std::move(web_app_accept_entry));
+ apps::FileHandler::AcceptEntry web_app_accept_entry;
+ web_app_accept_entry.mime_type = std::move(mime_type);
+ for (const auto& manifest_file_extension : it.second) {
+ std::string file_extension = base::UTF16ToUTF8(manifest_file_extension);
+ if (file_extension.length() <= 1 || file_extension[0] != '.') {
+ continue;
+ }
+ web_app_accept_entry.file_extensions.insert(std::move(file_extension));
+ }
+ if (!web_app_accept_entry.file_extensions.empty()) {
+ web_app_file_handler.accept.push_back(std::move(web_app_accept_entry));
+ }
}
- web_app_file_handlers.push_back(std::move(web_app_file_handler));
+ if (!web_app_file_handler.accept.empty()) {
+ web_app_file_handlers.push_back(std::move(web_app_file_handler));
+ }
}
web_app_info->file_handlers = std::move(web_app_file_handlers);
diff --git a/content/browser/manifest/README.md b/content/browser/manifest/README.md
index 038b5d0..33453e8 100644
--- a/content/browser/manifest/README.md
+++ b/content/browser/manifest/README.md
@@ -19,7 +19,8 @@
The browser process does not trust manifest parsing from the untrusted renderer.
In `ManifestManagerHost`, the browser enforces strict same-origin invariants on
`start_url`, `id`, `scope`, and action handlers (`share_target`,
-`file_handlers`), terminating any violating renderer process via
+`file_handlers`), as well as syntactic validity on `file_handlers` accept MIME
+types and file extensions, terminating any violating renderer process via
`mojo::ReportBadMessage`.
For manifest parsing and Mojo type definitions in Blink, see
diff --git a/content/browser/manifest/manifest_manager_host.cc b/content/browser/manifest/manifest_manager_host.cc
index 46b43ff..c14317c 100644
--- a/content/browser/manifest/manifest_manager_host.cc
+++ b/content/browser/manifest/manifest_manager_host.cc
@@ -18,6 +18,7 @@
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "mojo/public/cpp/bindings/message.h"
+#include "net/base/mime_util.h"
#include "net/base/schemeful_site.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
@@ -98,7 +99,23 @@
return "Manifest file_handlers must be same-origin with the document.";
}
for (const auto& [mime_type, extensions] : file_handler->accept) {
+ std::string mime_type_utf8 = base::UTF16ToUTF8(mime_type);
+ std::string top_level_mime_type;
+ if (!net::ParseMimeTypeWithoutParameter(mime_type_utf8,
+ &top_level_mime_type,
+ /*subtype=*/nullptr) ||
+ !net::IsValidTopLevelMimeType(top_level_mime_type)) {
+ return "Manifest file_handlers accept MIME type is invalid.";
+ }
+ if (extensions.empty()) {
+ return "Manifest file_handlers accept extensions list cannot be "
+ "empty.";
+ }
for (const auto& extension : extensions) {
+ if (!extension.starts_with(u".") || extension.length() <= 1) {
+ return "Manifest file_handlers accept extension must start with a "
+ "'.' and contain at least one extension character.";
+ }
for (size_t i = 0; i < extension.length();) {
UChar32 c;
U16_NEXT(extension, i, extension.length(), c);
diff --git a/net/base/mime_util.cc b/net/base/mime_util.cc
index 6117fc90..5677a69 100644
--- a/net/base/mime_util.cc
+++ b/net/base/mime_util.cc
@@ -661,9 +661,13 @@
}
}
- return type_string.size() > 2 &&
- base::StartsWith(type_string, "x-",
- base::CompareCase::INSENSITIVE_ASCII);
+ // "x-scheme-handler" is a pseudo-MIME type (used e.g. for URL scheme
+ // associations), not an IANA media type.
+ if (lower_type == "x-scheme-handler") {
+ return false;
+ }
+
+ return lower_type.size() > 2 && lower_type.starts_with("x-");
}
//----------------------------------------------------------------------------
diff --git a/net/base/mime_util.h b/net/base/mime_util.h
index 66045834..5ad6e6f9 100644
--- a/net/base/mime_util.h
+++ b/net/base/mime_util.h
@@ -139,7 +139,8 @@
// Returns true if the |type_string| is a top-level type of any media type
// registered with IANA media types registry at
// http://www.iana.org/assignments/media-types/media-types.xhtml or an
-// experimental type (type with x- prefix).
+// experimental type (type with x- prefix, excluding pseudo-types such as
+// x-scheme-handler).
//
// This method doesn't check that the input conforms to token ABNF, so if input
// is experimental type strings, you need to check check that before using
diff --git a/third_party/blink/public/mojom/manifest/manifest.mojom b/third_party/blink/public/mojom/manifest/manifest.mojom
index 39ae47f..6d180555 100644
--- a/third_party/blink/public/mojom/manifest/manifest.mojom
+++ b/third_party/blink/public/mojom/manifest/manifest.mojom
@@ -347,7 +347,10 @@
url.mojom.Url action;
// Human-readable name for the file handler. May be displayed in file manager.
mojo_base.mojom.String16 name;
- // A mapping of a MIME types to a corresponding list of file extensions.
+ // A mapping of valid MIME types to a corresponding list of file extensions.
+ // Each MIME type must be syntactically valid and must NOT contain protocol
+ // scheme handlers or FreeDesktop pseudo-MIME types (e.g. "x-scheme-handler/*").
+ // Each file extension must start with '.' and be longer than 1 character.
map<mojo_base.mojom.String16, array<mojo_base.mojom.String16>> accept;
// How the app should be launched when there are multiple files being opened.
diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser.cc b/third_party/blink/renderer/modules/manifest/manifest_parser.cc
index ed62d26..7bea6e9 100644
--- a/third_party/blink/renderer/modules/manifest/manifest_parser.cc
+++ b/third_party/blink/renderer/modules/manifest/manifest_parser.cc
@@ -90,7 +90,12 @@
if (mime_type.starts_with('.')) {
return true;
}
- return net::ParseMimeTypeWithoutParameter(mime_type.Utf8(), nullptr, nullptr);
+ std::string top_level_mime_type;
+ std::string subtype;
+ return net::ParseMimeTypeWithoutParameter(mime_type.Utf8(),
+ &top_level_mime_type, &subtype) &&
+ ((top_level_mime_type == "*" && subtype == "*") ||
+ net::IsValidTopLevelMimeType(top_level_mime_type));
}
bool VerifyFiles(const Vector<mojom::blink::ManifestFileFilterPtr>& files) {
@@ -1802,9 +1807,10 @@
return false;
}
- if (!output->starts_with('.')) {
+ if (!output->starts_with('.') || output->length() <= 1) {
AddErrorInfo(
- "property 'accept' file extension ignored, must start with a '.'.");
+ "property 'accept' file extension ignored, must start with a '.' and "
+ "contain at least one extension character.");
... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/shell_integration_linux_unittest.cc b/chrome/browser/shell_integration_linux_unittest.cc
index 9560a3f9..3c9d8db2 100644
--- a/chrome/browser/shell_integration_linux_unittest.cc
+++ b/chrome/browser/shell_integration_linux_unittest.cc
@@ -631,6 +631,64 @@
EXPECT_EQ(file_contents, expected_file_contents);
}
+TEST(ShellIntegrationTest,
+ GetMimeTypesRegistrationFileContentsFiltersSchemeHandlers) {
+ apps::FileHandlers file_handlers;
+ {
+ apps::FileHandler file_handler;
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // "x-scheme-handler/*" is a FreeDesktop pseudo-MIME type for URL scheme
+ // handling, not a valid media type for file associations. It should be
+ // ignored during shared MIME-info file registration.
+ accept_entry.mime_type = "x-scheme-handler/mailto";
+ accept_entry.file_extensions.insert(".eml");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // Bare "x-scheme-handler" without trailing slash must also be ignored.
+ accept_entry.mime_type = "x-scheme-handler";
+ accept_entry.file_extensions.insert(".bad");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // Uppercase pseudo-types must also be ignored case-insensitively.
+ accept_entry.mime_type = "X-Scheme-Handler/custom";
+ accept_entry.file_extensions.insert(".cust");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // Uppercase bare "X-Scheme-Handler" must also be ignored.
+ accept_entry.mime_type = "X-Scheme-Handler";
+ accept_entry.file_extensions.insert(".bad2");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ accept_entry.mime_type = "text/plain";
+ accept_entry.file_extensions.insert(".txt");
+ file_handler.accept.push_back(accept_entry);
+ }
+ file_handlers.push_back(file_handler);
+ }
+
+ const std::string file_contents =
+ GetMimeTypesRegistrationFileContents(file_handlers);
+ const std::string expected_file_contents =
+ "<?xml version=\"1.0\"?>\n"
+ "<mime-info "
+ "xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n"
+ " <mime-type type=\"text/plain\">\n"
+ " <glob pattern=\"*.txt\"/>\n"
+ " </mime-type>\n"
+ "</mime-info>\n";
+
+ EXPECT_EQ(file_contents, expected_file_contents);
+}
+
// The WM class name may be either capitalised or not, depending on the
// platform.
void CheckProgramClassClass(const std::string& class_name) {
diff --git a/chrome/browser/web_applications/os_integration/file_handling_sub_manager_unittest.cc b/chrome/browser/web_applications/os_integration/file_handling_sub_manager_unittest.cc
index c07e4b6e..65b07167 100644
--- a/chrome/browser/web_applications/os_integration/file_handling_sub_manager_unittest.cc
+++ b/chrome/browser/web_applications/os_integration/file_handling_sub_manager_unittest.cc
@@ -189,6 +189,102 @@
".baz");
}
+TEST_F(FileHandlingSubManagerConfigureTest, InvalidMimeTypesIgnored) {
+ apps::FileHandlers file_handlers;
+ {
+ apps::FileHandler file_handler;
+ file_handler.action = GURL("https://app.site/open-foo");
+ file_handler.display_name = u"Mixed opener";
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // "x-scheme-handler/mailto" is invalid file handler data (it represents a
+ // FreeDesktop URL scheme pseudo-MIME type rather than a valid file media
+ // type) and must be filtered out when configuring OS file handling state.
+ accept_entry.mime_type = "x-scheme-handler/mailto";
+ accept_entry.file_extensions.insert(".eml");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // Bare "x-scheme-handler" without trailing slash must also be skipped.
+ accept_entry.mime_type = "x-scheme-handler";
+ accept_entry.file_extensions.insert(".bad");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // Uppercase "X-Scheme-Handler/..." must also be skipped.
+ accept_entry.mime_type = "X-Scheme-Handler/mailto";
+ accept_entry.file_extensions.insert(".eml2");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ // Uppercase bare "X-Scheme-Handler" must also be skipped.
+ accept_entry.mime_type = "X-Scheme-Handler";
+ accept_entry.file_extensions.insert(".bad2");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ accept_entry.mime_type = "application/foo";
+ accept_entry.file_extensions.insert(".foo");
+ file_handler.accept.push_back(accept_entry);
+ }
+ file_handlers.push_back(file_handler);
+ }
+
+ const webapps::AppId& app_id = InstallWebAppWithFileHandlers(file_handlers);
+ auto state =
+ provider().registrar_unsafe().GetAppCurrentOsIntegrationState(app_id);
+ EXPECT_TRUE(state.has_value());
+ const proto::os_state::WebAppOsIntegration& os_integration_state =
+ state.value();
+ EXPECT_TRUE(os_integration_state.has_file_handling());
+ const proto::os_state::FileHandling& file_handling =
+ os_integration_state.file_handling();
+ EXPECT_EQ(file_handling.file_handlers_size(), 1);
+ EXPECT_EQ(file_handling.file_handlers(0).accept_size(), 1);
+ EXPECT_EQ(file_handling.file_handlers(0).accept(0).mimetype(),
+ "application/foo");
+}
+
+TEST_F(FileHandlingSubManagerConfigureTest,
+ AllSchemeHandlersFilteredResultsInNoFileHandling) {
+ apps::FileHandlers file_handlers;
+ {
+ apps::FileHandler file_handler;
+ file_handler.action = GURL("https://app.site/open-foo");
+ file_handler.display_name = u"Scheme opener";
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ accept_entry.mime_type = "x-scheme-handler/mailto";
+ accept_entry.file_extensions.insert(".eml");
+ file_handler.accept.push_back(accept_entry);
+ }
+ {
+ apps::FileHandler::AcceptEntry accept_entry;
+ accept_entry.mime_type = "X-Scheme-Handler";
+ accept_entry.file_extensions.insert(".bad");
+ file_handler.accept.push_back(accept_entry);
+ }
+ file_handlers.push_back(file_handler);
+ }
+
+ const webapps::AppId& app_id = InstallWebAppWithFileHandlers(file_handlers);
+ auto state =
+ provider().registrar_unsafe().GetAppCurrentOsIntegrationState(app_id);
+ EXPECT_TRUE(state.has_value());
+ const proto::os_state::WebAppOsIntegration& os_integration_state =
+ state.value();
+ // File handling is enabled in OS integration state, but because all accept
+ // entries were filtered out as scheme handlers, no file handler entries
+ // are retained. HasFileHandling() in FileHandlingSubManager::Execute avoids
+ // registering with the OS.
+ EXPECT_TRUE(os_integration_state.has_file_handling());
+ EXPECT_EQ(os_integration_state.file_handling().file_handlers_size(), 0);
+}
+
TEST_F(FileHandlingSubManagerConfigureTest, UpdateUserChoiceDisallowed) {
apps::FileHandlers file_handlers;
diff --git a/chrome/browser/web_applications/os_integration/web_app_file_handler_registration_linux_browsertest.cc b/chrome/browser/web_applications/os_integration/web_app_file_handler_registration_linux_browsertest.cc
index 383af22..56edd39 100644
--- a/chrome/browser/web_applications/os_integration/web_app_file_handler_registration_linux_browsertest.cc
+++ b/chrome/browser/web_applications/os_integration/web_app_file_handler_registration_linux_browsertest.cc
@@ -28,6 +28,7 @@
#include "components/webapps/common/web_app_id.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gmock/include/gmock/gmock.h"
#include "url/gurl.h"
namespace web_app {
@@ -142,4 +143,64 @@
GetUserApplicationsDir().value()));
}
+// Verify that scheme handler pseudo-types in manifest file_handlers are
+// ignored and not registered in OS MIME type files.
+IN_PROC_BROWSER_TEST_F(WebAppFileHandlerRegistrationLinuxBrowserTest,
+ SchemeHandlerMimeTypesExcludedFromRegistration) {
+ EXPECT_TRUE(embedded_test_server()->Start());
+ GURL url(embedded_test_server()->GetURL(
+ "/banners/"
+ "manifest_test_page.html?manifest="
+ "manifest_with_x_scheme_handler_in_file_handlers.json"));
+
+ apps::FileHandlers expected_file_handlers;
+ expected_file_handlers.push_back(GetTestFileHandler(
+ "https://site.api/open-foo", {{"application/foo", {".foo"}}}));
+
+ std::string expected_file_contents =
+ shell_integration_linux::GetMimeTypesRegistrationFileContents(
+ expected_file_handlers);
+
+ std::vector<LinuxFileRegistration> xdg_commands_called;
+ base::RunLoop loop;
+ SetUpdateMimeInfoDatabaseOnLinuxCallbackForTesting(base::BindLambdaForTesting(
+ [&](base::FilePath filename_in, std::string xdg_command_in,
+ std::string file_contents_in) {
+ LinuxFileRegistration file_registration = LinuxFileRegistration();
+ file_registration.file_name = filename_in;
+ file_registration.xdg_command = xdg_command_in;
+ file_registration.file_contents = file_contents_in;
+ xdg_commands_called.push_back(file_registration);
+ loop.Quit();
+ return true;
+ }));
+
+ ExternalInstallOptions install_options = CreateInstallOptions(url);
+ install_options.install_source = ExternalInstallSource::kExternalPolicy;
+ InstallApp(install_options);
+
+ loop.Run();
+ std::optional<webapps::AppId> app_id = WebAppProvider::GetForTest(profile())
+ ->registrar_unsafe()
+ .LookupExternalAppId(url);
+ EXPECT_TRUE(app_id.has_value());
+
+ base::FilePath expected_filename =
+ shell_integration_linux::GetMimeTypesRegistrationFilename(
+ profile()->GetPath(), app_id.value());
+
+ // Verify that the file registration command was called with XML contents
+ // matching only the valid "application/foo" file handler. We verify that
+ // "x-scheme-handler/mailto" was filtered out during manifest installation
+ // and is completely absent from the registered file contents.
+ EXPECT_EQ(xdg_commands_called[0].file_contents, expected_file_contents);
+ EXPECT_EQ(xdg_commands_called[0].file_name, expected_filename);
+ EXPECT_THAT(xdg_commands_called[0].file_contents,
+ testing::Not(testing::HasSubstr("x-scheme-handler")));
+ SetUpdateMimeInfoDatabaseOnLinuxCallbackForTesting(
+ UpdateMimeInfoDatabaseOnLinuxCallback());
+ EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall,
+ result_code_.value());
+}
+
} // namespace web_app
diff --git a/chrome/browser/web_applications/os_integration/web_app_shortcut_linux_unittest.cc b/chrome/browser/web_applications/os_integration/web_app_shortcut_linux_unittest.cc
index 25d47f36..616420a 100644
--- a/chrome/browser/web_applications/os_integration/web_app_shortcut_linux_unittest.cc
+++ b/chrome/browser/web_applications/os_integration/web_app_shortcut_linux_unittest.cc
@@ -538,13 +538,94 @@
ASSERT_TRUE(base::PathExists(autostart_shortcut_path));
std::string actual_contents;
- ASSERT_TRUE(
+ EXPECT_TRUE(
base::ReadFileToString(autostart_shortcut_path, &actual_contents));
EXPECT_EQ(expected_contents, actual_contents);
}
}
TEST_F(WebAppShortcutLinuxTest,
+ CreateDesktopShortcutWithFileHandlersAndProtocols) {
+ std::unique_ptr<ShortcutInfo> shortcut_info = GetShortcutInfo();
+ ShortcutLocations locations;
+ locations.on_desktop = true;
+ locations.in_startup = true;
+ locations.applications_menu_location = APP_MENU_LOCATION_SUBDIR_CHROMEAPPS;
+
+ // Add file handler MIME types, including an unexpected x-scheme-handler
+ // entry.
+ shortcut_info->file_handler_mime_types.emplace("image/png");
+ shortcut_info->file_handler_mime_types.emplace("text/plain");
+ shortcut_info->file_handler_mime_types.emplace("x-scheme-handler/mailto");
+ shortcut_info->file_handler_mime_types.emplace("x-scheme-handler");
+ shortcut_info->file_handler_mime_types.emplace("X-Scheme-Handler/custom");
+ shortcut_info->file_handler_mime_types.emplace("X-Scheme-Handler");
+
+ // Add protocol handlers.
+ shortcut_info->protocol_handlers.emplace("web+testing");
+
+ int invoke_count = 0;
+ LaunchXdgUtilityForTesting CreateDesktopShortcutLaunchXdgUtility =
+ base::BindLambdaForTesting([&](const std::vector<std::string>& argv,
+ int* exit_code) -> bool {
+ ValidateCreateDesktopShortcutLaunchXdgUtility(
+ argv, exit_code, GetApplicationsPath(), invoke_count);
+
+ // Validate MIME types were added to contents correctly:
+ // - "image/png" and "text/plain" from file_handler_mime_types are kept.
+ // - "x-scheme-handler/web+testing" is added from protocol_handlers.
+ // - "x-scheme-handler/mailto" from file_handler_mime_types is filtered
+ // out and must NOT appear in the .desktop file.
+ // Each of these expectations is explicitly asserted below.
... (truncated)
Original Bug Report
PWA file_handlers: Potential unvalidated protocol handler registration on Linux
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: PWA manifests on Linux can potentially register as handlers for arbitrary URL schemes by misusing the ‘file_handlers’ field. By providing strings starting with ‘x-scheme-handler/’, an attacker can bypass the security restrictions and prefixes required by the ‘protocol_handlers’ API. This may lead to persistent OS-level interception of protocols like ‘mailto:’ and potential UI spoofing.
Affected files:
chrome/browser/web_applications/os_integration/file_handling_sub_manager.ccchrome/browser/web_applications/web_app_install_utils.ccchrome/browser/web_applications/os_integration/web_app_shortcut_linux.ccchrome/browser/shell_integration_linux.ccnet/base/mime_util.ccthird_party/blink/renderer/modules/manifest/manifest_parser.ccchrome/browser/web_applications/os_integration/web_app_shortcut.ccthird_party/blink/public/mojom/manifest/manifest.mojom
Estimated timestamp from git blame: 2019-12-03
Description
A potential logic vulnerability in the Progressive Web App (PWA) installation process on Linux allows a website to register itself as a handler for arbitrary URL schemes. This bypasses the security restrictions and user permission prompts usually required for protocol handler registration (the protocol_handlers field in the manifest), which enforces safelists and requires custom schemes to be prefixed with web+.
The issue exists because the file_handlers manifest field, intended for MIME type and file extension associations, is not sufficiently validated. On Linux, Chrome integrates these handlers by writing to the MimeType= line of a .desktop file. The browser fails to verify that the keys in the accept map are valid MIME types and do not contain protocol handler registrations using the x-scheme-handler/ convention used by Linux desktop environments.
Technical Details
When a PWA manifest is parsed, the renderer uses net::IsValidTopLevelMimeType() in net/base/mime_util.cc to validate the accept keys in file_handlers. This function accepts any top-level type starting with x- that is at least 3 characters long (e.g., x-scheme-handler).
- Renderer-side Parsing: In
third_party/blink/renderer/modules/manifest/manifest_parser.cc, the parser extracts the MIME type and validates it usingnet::IsValidTopLevelMimeType. A malicious manifest can provide a key like"x-scheme-handler/mailto". - Data Flow: The parsed string is sent to the browser process via Mojo (
blink::mojom::ManifestFileHandler). Inchrome/browser/web_applications/web_app_install_utils.cc, the functionPopulateFileHandlerInfoFromManifestcopies these strings verbatim into theWebAppInstallInfoobject (line 375) without further validation. - OS Integration: During PWA installation on Linux,
web_app_shortcut_linux.cccollects these MIME types and passes them toshell_integration_linux::GetDesktopFileContents. This function writes the attacker-controlled strings into the PWA’s.desktopfile (typically in~/.local/share/applications/) usingg_key_file_set_string(line 613).
Because Linux desktop environments (following freedesktop.org standards) use x-scheme-handler/<scheme> entries in the MimeType field of .desktop files to determine protocol handlers, the PWA is successfully registered as a handler for the specified protocol. This registration occurs during initial installation, bypassing the protocol_handlers security model entirely.
Compromised Renderer Variant
In a scenario where the renderer is compromised, an attacker could potentially bypass the renderer-side checks in manifest_parser.cc and provide arbitrary strings to the browser process. This would allow the registration of sensitive MIME types, such as inode/directory, making the PWA a candidate handler for system locations in the OS “Open With” menus.
Impact
This is a persistent OS-level URL scheme and MIME type association vulnerability. A potential attacker could:
- Intercept protocol requests (e.g.,
mailto:,slack:,zoommtg:). - Spoof system “Open With” dialogs.
- Become the default handler for schemes that do not have a prior association on the user’s system.
Potential Reproduction Steps
- Host a PWA manifest with the following entry:
{"file_handlers":[{"action":"/handle-mailto","accept":{"x-scheme-handler/mailto":[".eml"]}}]}. - On a Linux system (e.g., Ubuntu with GNOME), navigate to the site in Chrome and click the “Install” button in the omnibox.
- After installation, inspect the generated
.desktopfile in~/.local/share/applications/. Observe that theMimeType=line containsx-scheme-handler/mailto. - Attempt to open a
mailto:link (e.g., viaxdg-open mailto:test@example.comin a terminal). - Observe that the OS offers to open the link with the newly installed PWA.
Suggested Fix
The browser process should perform its own validation of MIME types received from the renderer during PWA installation. Specifically, in chrome/browser/web_applications/web_app_install_utils.cc, the PopulateFileHandlerInfoFromManifest function should verify that the MIME type strings are well-formed and do not contain the x-scheme-handler/ prefix or other sensitive system types.
Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049
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.