CVE-2026-93386
Overview
Background
- Bidi / format control characters
- invisible Unicode code points such as
U+202E(Right-to-Left Override, “RLO”) orU+200E(Left-to-Right Mark, “LRM”), classifiedU_FORMAT_CHAR, that reorder or hide surrounding text when rendered. - C0/C1 control characters
- non-printing code points such as
U+0001(SOH),U+007F(DEL), orU+009F(APC) thatbase::IsUnicodeControlreports as control characters and that can corrupt or spoof rendered UI strings. - PWA file handler `accept` extensions
- the file-type extension strings a web app manifest declares under
file_handlers[].accept, which are surfaced to the user in install and file-handling UI. - `TransformFileExtensionsForDisplay`
- the
web_app_utils.cchelper that converts a set of raw extension strings into upper-casedstd::u16stringvalues for display in PWA UI.
Root Cause Analysis
Before the patch, extension strings from a web app manifest’s file_handlers[].accept map were accepted verbatim through the whole pipeline: the Blink manifest parser stored them without character validation, the browser process trusted them over IPC, and TransformFileExtensionsForDisplay only did base::UTF8ToUTF16(base::ToUpperASCII(extension.substr(1))) before handing them to display code. Because no layer filtered U_FORMAT_CHAR or control characters, an attacker-controlled manifest could embed a bidirectional override (e.g. U+202E) inside an extension so that the string rendered in PWA UI misrepresents the true file type the app handles. The invariant violated is that displayed file-extension text should be a faithful, printable representation of the declared extension.
The fix TIGHTENS enforcement at three tiers: parse-time rejection in manifest_parser.cc, browser-side zero-trust IPC rejection in manifest_manager_host.cc (which reports a bad message and kills a misbehaving renderer), and display-time sanitization in TransformFileExtensionsForDisplay that iterates code points with U16_NEXT and drops any that are control or format characters. The added empty-string guard in TransformFileExtensionsForDisplay also avoids calling extension.substr(1) on an empty string.
Attack Path
- Craft a malicious manifest
An attacker hosts a web app whose manifest declares a
file_handlersentry with anacceptextension containing an embedded format character, e.g.".png". - Extension survives parsing
Pre-patch, the Blink manifest parser accepts the extension without checking code-point categories, so it is stored in the parsed
Manifestand sent to the browser process. - Browser process trusts the string
Pre-patch, the browser-side manifest validation in
manifest_manager_host.ccperforms no control/format-character check, so the tainted extension is retained for the installed PWA. - Spoofed display
At render time
TransformFileExtensionsForDisplayupper-cases the raw string and returns it unchanged for the bidi/control characters, so the install or file-handler UI shows a reordered or misleading extension, misrepresenting what file type the app will handle (inferred: the specific UI surface and the resulting user deception are implied by the “UI misrepresentation” advisory, not shown in the diff).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/web_applications/web_app_utils.cc |
modified | |
forcontent/browser/manifest/manifest_manager_host.cc |
modified | |
forthird_party/blink/renderer/modules/manifest/manifest_parser.cc |
modified | |
TEST_Fchrome/browser/web_applications/web_app_utils_unittest.cc |
modified |
Files Changed
third_party/blink/renderer/modules/manifest/manifest_parser_unittest.ccchrome/browser/web_applications/web_app_utils_unittest.cccontent/browser/manifest/manifest_browsertest.ccthird_party/blink/renderer/modules/manifest/manifest_parser.ccchrome/browser/web_applications/web_app_utils.ccthird_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.pycontent/browser/manifest/manifest_manager_host.cc
Audit Directions
- Renderer-to-display string passthroughAudit other manifest- or renderer-supplied strings (app name, shortcut names, protocol/handler titles,
description) that reach browser UI to confirm they are filtered for bidi/format/control characters before display. - Duplicated ad-hoc character validationThe patch’s own
TODO(crbug.com/530303003)notes the control/format check is duplicated across parsing, IPC, and display; hunt for other sites doing partial or missing versions of this filter that should share a common helper. - Unsanitized `substr`/index math on stringsLook for other display transforms like
extension.substr(1)that assume a non-empty or well-formed input and can throw or misbehave on empty/edge-case strings from untrusted manifests.
Patch
From f89efb408bbd1329e7f06bbac2e78293896b4604 Mon Sep 17 00:00:00 2001 From: Vincent Scheib <[email protected]> Date: Wed, 01 Jul 2026 16:52:15 -0700 Subject: [PATCH] Sanitize bidi and control characters in PWA file extensions PWA file extension strings in the manifest can contain unsanitized bidirectional control characters or other control characters. This CL implements a three-tier hybrid enforcement strategy to prevent UI spoofing: 1. Parse-time DevTools Gate: Rejects manifest file handler extensions containing U_FORMAT_CHAR or U_CONTROL_CHAR during Blink manifest parsing, logging a DevTools console error. 2. Browser-side Zero-Trust IPC Validation: Hardens the trusted browser process to reject manifests containing these characters in file handler extensions, preventing exploitation even by compromised renderers. 3. Display-time Hardening: Sanitizes extensions before UI rendering in web_app_utils.cc by actively filtering out U_FORMAT_CHAR and U_CONTROL_CHAR. Tests: - WebAppUtilsTest. TransformFileExtensionsForDisplay_StripsBidiControls (covers LRM, RLO, SOH, DEL, APC, and empty string) - ManifestParserTest.FileHandlerParseRules (covers format and control characters) - ManifestBrowserTest. BadMessage_FileHandlersAcceptExtensionInvalidFormatCharacter - ManifestBrowserTest. BadMessage_FileHandlersAcceptExtensionInvalidControlCharacter BUG=520490618 BUG=513996595 TAG=agy CONV=d87700cf-7987-4cf2-bb6b-56ac7798e179 Change-Id: I77f874f6dbac094c522eed822486978cdff90a0d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8012267 Reviewed-by: Daniel Murphy <[email protected]> Reviewed-by: Daniel Cheng <[email protected]> Reviewed-by: Luke Deng <[email protected]> Commit-Queue: Vincent Scheib <[email protected]> Cr-Commit-Position: refs/heads/main@{#1655824} --- diff --git a/chrome/browser/web_applications/web_app_utils.cc b/chrome/browser/web_applications/web_app_utils.cc index f2ccb0ce..5a8bf2c8 100644 --- a/chrome/browser/web_applications/web_app_utils.cc +++ b/chrome/browser/web_applications/web_app_utils.cc @@ -68,6 +68,8 @@ #include "third_party/blink/public/common/safe_url_pattern.h" #include "third_party/blink/public/mojom/manifest/display_mode.mojom-shared.h" #include "third_party/blink/public/mojom/manifest/manifest.mojom-shared.h" +#include "third_party/icu/source/common/unicode/uchar.h" +#include "third_party/icu/source/common/unicode/utf16.h" #include "third_party/liburlpattern/options.h" #include "third_party/liburlpattern/part.h" #include "third_party/liburlpattern/pattern.h" @@ -426,7 +428,33 @@ std::ranges::transform( extensions, std::back_inserter(extensions_for_display), [](const std::string& extension) { - return base::UTF8ToUTF16(base::ToUpperASCII(extension.substr(1))); + if (extension.empty()) { + return std::u16string(); + } + std::u16string ext_u16 = + base::UTF8ToUTF16(base::ToUpperASCII(extension.substr(1))); + std::u16string sanitized; + sanitized.reserve(ext_u16.size()); + for (size_t i = 0; i < ext_u16.length();) { + UChar32 c; + U16_NEXT(ext_u16, i, ext_u16.length(), c); + // TODO(crbug.com/530303003): This check for control and format + // characters is duplicated across manifest parsing, IPC validation, + // and PWA display. Consider consolidating it into a shared helper in + // //base/strings/string_util.h. + if (!base::IsUnicodeControl(c) && u_charType(c) != U_FORMAT_CHAR) { + if (c <= 0xFFFF) { + // Safe to cast UChar32 to char16_t here because the guard ensures + // c is within the BMP (<= 0xFFFF), and Unicode code points are + // non-negative. + sanitized.push_back(static_cast<char16_t>(c)); + } else { + sanitized.push_back(U16_LEAD(c)); + sanitized.push_back(U16_TRAIL(c)); + } + } + } + return sanitized; }); return extensions_for_display; } diff --git a/content/browser/manifest/manifest_manager_host.cc b/content/browser/manifest/manifest_manager_host.cc index ad89aab3..0bf0cd58 100644 --- a/content/browser/manifest/manifest_manager_host.cc +++ b/content/browser/manifest/manifest_manager_host.cc @@ -26,6 +26,8 @@ #include "third_party/blink/public/common/security/protocol_handler_security_level.h" #include "third_party/blink/public/mojom/manifest/manifest.mojom.h" #include "third_party/blink/public/mojom/manifest/manifest_manager.mojom.h" +#include "third_party/icu/source/common/unicode/uchar.h" +#include "third_party/icu/source/common/unicode/utf16.h" #include "url/gurl.h" #include "url/origin.h" @@ -93,6 +95,22 @@ if (!document_origin.IsSameOriginWith(file_handler->action)) { return "Manifest file_handlers must be same-origin with the document."; } + for (const auto& [mime_type, extensions] : file_handler->accept) { + for (const auto& extension : extensions) { + for (size_t i = 0; i < extension.length();) { + UChar32 c; + U16_NEXT(extension, i, extension.length(), c); + // TODO(crbug.com/530303003): This check for control and format + // characters is duplicated across manifest parsing, IPC validation, + // and PWA display. Consider consolidating it into a shared helper + // in //base/strings/string_util.h. + if (base::IsUnicodeControl(c) || u_charType(c) == U_FORMAT_CHAR) { + return "Manifest file_handlers accept extension contains invalid " + "control or format characters."; + } + } + } + } } for (const auto& protocol_handler : manifest.protocol_handlers) { diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser.cc b/third_party/blink/renderer/modules/manifest/manifest_parser.cc index 870efde..893a0b2 100644 --- a/third_party/blink/renderer/modules/manifest/manifest_parser.cc +++ b/third_party/blink/renderer/modules/manifest/manifest_parser.cc @@ -4,6 +4,8 @@ #include "third_party/blink/renderer/modules/manifest/manifest_parser.h" +#include <unicode/uchar.h> + #include <cstddef> #include <memory> #include <optional> @@ -17,6 +19,7 @@ #include "base/feature_list.h" #include "base/metrics/histogram_functions.h" #include "base/notreached.h" +#include "base/strings/string_util.h" #include "net/base/mime_util.h" #include "net/base/registry_controlled_domains/registry_controlled_domain.h" #include "services/device/public/mojom/screen_orientation_lock_types.mojom-blink.h" @@ -48,6 +51,7 @@ #include "third_party/blink/renderer/platform/weborigin/kurl.h" #include "third_party/blink/renderer/platform/weborigin/security_origin.h" #include "third_party/blink/renderer/platform/wtf/hash_map.h" +#include "third_party/blink/renderer/platform/wtf/text/code_point_iterator.h" #include "third_party/blink/renderer/platform/wtf/text/strcat.h" #include "third_party/blink/renderer/platform/wtf/text/string_impl.h" #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" @@ -1821,6 +1825,19 @@ return false; } + // TODO(crbug.com/530303003): This check for control and format characters is + // duplicated across manifest parsing, IPC validation, and PWA display. + // Consider consolidating it into a shared helper in + // //base/strings/string_util.h. + for (UChar32 c : *output) { + if (base::IsUnicodeControl(c) || u_charType(c) == U_FORMAT_CHAR) { + AddErrorInfo( + "property 'accept' file extension ignored, contains invalid " + "control or format characters."); + return false; + } + } + return true; } diff --git a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py index 20a6cb7..1a279700 100755 --- a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py +++ b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py @@ -278,6 +278,7 @@ 'base::ClampFloor', 'base::ClampRound', 'base::IsTypeInRangeForNumericType', + 'base::IsUnicodeControl', 'base::IsValueInRangeForNumericType', 'base::IsValueNegative', 'base::MakeStrictNum', diff --git a/chrome/browser/web_applications/web_app_utils_unittest.cc b/chrome/browser/web_applications/web_app_utils_unittest.cc index 7cae1bd..50659495 100644 --- a/chrome/browser/web_applications/web_app_utils_unittest.cc +++ b/chrome/browser/web_applications/web_app_utils_unittest.cc @@ -115,6 +115,23 @@ #endif } +TEST_F(WebAppUtilsTest, TransformFileExtensionsForDisplay_StripsBidiControls) { + std::set<std::string> extensions = { + ".aa\xE2\x80\x8E", // LRM (Format) + ".bb\xE2\x80\xAE", // RLO (Format) + ".cc\x01", // SOH (Control) + ".dd\x7F", // DEL (Control) + ".ee\xC2\x9F", // APC (Control) + ".bat", // Safe + "" // Empty + }; + std::vector<std::u16string> transformed = + TransformFileExtensionsForDisplay(extensions); + + EXPECT_THAT(transformed, ::testing::UnorderedElementsAre( + u"", u"AA", u"BB", u"CC", u"DD", u"EE", u"BAT")); +} + TEST_F(WebAppUtilsTest, AreWebAppsUserInstallable) { Profile* regular_profile = profile(); diff --git a/content/browser/manifest/manifest_browsertest.cc b/content/browser/manifest/manifest_browsertest.cc index 2022229..a4485c1 100644 --- a/content/browser/manifest/manifest_browsertest.cc +++ b/content/browser/manifest/manifest_browsertest.cc @@ -1245,6 +1245,76 @@ "Manifest file_handlers must be same-origin with the document.")); } +IN_PROC_BROWSER_TEST_F( + ManifestBrowserTest, + BadMessage_FileHandlersAcceptExtensionInvalidFormatCharacter) { + const GURL test_url = + embedded_test_server()->GetURL("/manifest/empty-manifest.html"); + ASSERT_TRUE(NavigateToURL(shell(), test_url)); + + ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage( + shell()->web_contents()->GetPrimaryPage()); + + mojo::FakeMessageDispatchContext fake_dispatch_context; + auto bad_manifest = blink::mojom::Manifest::New(); + bad_manifest->start_url = test_url; + bad_manifest->id = test_url; + bad_manifest->scope = embedded_test_server()->GetURL("/manifest/"); + + blink::mojom::ManifestFileHandlerPtr file_handler = + blink::mojom::ManifestFileHandler::New(); + file_handler->action = test_url; + + std::vector<std::u16string> extensions; + extensions.push_back(u".png\u202E"); + file_handler->accept[u"image/png"] = std::move(extensions); + + bad_manifest->file_handlers.push_back(std::move(file_handler)); + + mojo::test::BadMessageObserver bad_message_observer; + host->ValidateAndMaybeOverrideManifestForTesting( + blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest)); + EXPECT_THAT(bad_message_observer.WaitForBadMessage(), + ::testing::StartsWith( + "Manifest file_handlers accept extension contains invalid " + "control or format characters.")); +} + +IN_PROC_BROWSER_TEST_F( + ManifestBrowserTest, + BadMessage_FileHandlersAcceptExtensionInvalidControlCharacter) { + const GURL test_url = + embedded_test_server()->GetURL("/manifest/empty-manifest.html"); + ASSERT_TRUE(NavigateToURL(shell(), test_url)); + + ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage( + shell()->web_contents()->GetPrimaryPage()); + + mojo::FakeMessageDispatchContext fake_dispatch_context; + auto bad_manifest = blink::mojom::Manifest::New(); + bad_manifest->start_url = test_url; + bad_manifest->id = test_url; + bad_manifest->scope = embedded_test_server()->GetURL("/manifest/"); + + blink::mojom::ManifestFileHandlerPtr file_handler = + blink::mojom::ManifestFileHandler::New(); + file_handler->action = test_url; + + std::vector<std::u16string> extensions; + extensions.push_back(u".png\u0001"); + file_handler->accept[u"image/png"] = std::move(extensions); + + bad_manifest->file_handlers.push_back(std::move(file_handler)); + + mojo::test::BadMessageObserver bad_message_observer; + host->ValidateAndMaybeOverrideManifestForTesting( + blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest)); + EXPECT_THAT(bad_message_observer.WaitForBadMessage(), + ::testing::StartsWith( + "Manifest file_handlers accept extension contains invalid " + "control or format characters.")); +} + IN_PROC_BROWSER_TEST_F(ManifestBrowserTest, BadMessage_ProtocolHandlersActionCrossOrigin) { const GURL test_url = diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc b/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc index 488fbdcf..3e0076d 100644 --- a/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc +++ b/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc @@ -3040,6 +3040,62 @@ ASSERT_EQ(0u, file_handlers.size()); } + // Extensions that contain format characters are invalid. + { + auto& manifest = ParseManifest( + R"({ + "file_handlers": [ + { + "name": "name", + "action": "/files", + "accept": { + "image/png": [ + ".png\u202E" + ] + } + } + ] + })"); + auto& file_handlers = manifest->file_handlers; + + ASSERT_EQ(2u, GetErrorCount()); + EXPECT_EQ( + "property 'accept' file extension ignored, contains invalid " + "control or format characters.", + errors()[0]); + EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.", + errors()[1]); + ASSERT_EQ(0u, file_handlers.size()); + } + + // Extensions that contain control characters are invalid. + { + auto& manifest = ParseManifest( + R"({ + "file_handlers": [ + { + "name": "name", + "action": "/files", + "accept": { + "image/png": [ + ".png\u0001" + ] + } + } + ] + })"); + auto& file_handlers = manifest->file_handlers; + + ASSERT_EQ(2u, GetErrorCount()); + EXPECT_EQ( + "property 'accept' file extension ignored, contains invalid " + "control or format characters.", + errors()[0]); + EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.", + errors()[1]); + ASSERT_EQ(0u, file_handlers.size()); + } + // Invalid MIME types and those with parameters are stripped. { auto& manifest = ParseManifest(
Regression Test / PoC
diff --git a/chrome/browser/web_applications/web_app_utils_unittest.cc b/chrome/browser/web_applications/web_app_utils_unittest.cc
index 7cae1bd..50659495 100644
--- a/chrome/browser/web_applications/web_app_utils_unittest.cc
+++ b/chrome/browser/web_applications/web_app_utils_unittest.cc
@@ -115,6 +115,23 @@
#endif
}
+TEST_F(WebAppUtilsTest, TransformFileExtensionsForDisplay_StripsBidiControls) {
+ std::set<std::string> extensions = {
+ ".aa\xE2\x80\x8E", // LRM (Format)
+ ".bb\xE2\x80\xAE", // RLO (Format)
+ ".cc\x01", // SOH (Control)
+ ".dd\x7F", // DEL (Control)
+ ".ee\xC2\x9F", // APC (Control)
+ ".bat", // Safe
+ "" // Empty
+ };
+ std::vector<std::u16string> transformed =
+ TransformFileExtensionsForDisplay(extensions);
+
+ EXPECT_THAT(transformed, ::testing::UnorderedElementsAre(
+ u"", u"AA", u"BB", u"CC", u"DD", u"EE", u"BAT"));
+}
+
TEST_F(WebAppUtilsTest, AreWebAppsUserInstallable) {
Profile* regular_profile = profile();
diff --git a/content/browser/manifest/manifest_browsertest.cc b/content/browser/manifest/manifest_browsertest.cc
index 2022229..a4485c1 100644
--- a/content/browser/manifest/manifest_browsertest.cc
+++ b/content/browser/manifest/manifest_browsertest.cc
@@ -1245,6 +1245,76 @@
"Manifest file_handlers must be same-origin with the document."));
}
+IN_PROC_BROWSER_TEST_F(
+ ManifestBrowserTest,
+ BadMessage_FileHandlersAcceptExtensionInvalidFormatCharacter) {
+ const GURL test_url =
+ embedded_test_server()->GetURL("/manifest/empty-manifest.html");
+ ASSERT_TRUE(NavigateToURL(shell(), test_url));
+
+ ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
+ shell()->web_contents()->GetPrimaryPage());
+
+ mojo::FakeMessageDispatchContext fake_dispatch_context;
+ auto bad_manifest = blink::mojom::Manifest::New();
+ bad_manifest->start_url = test_url;
+ bad_manifest->id = test_url;
+ bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");
+
+ blink::mojom::ManifestFileHandlerPtr file_handler =
+ blink::mojom::ManifestFileHandler::New();
+ file_handler->action = test_url;
+
+ std::vector<std::u16string> extensions;
+ extensions.push_back(u".png\u202E");
+ file_handler->accept[u"image/png"] = std::move(extensions);
+
+ bad_manifest->file_handlers.push_back(std::move(file_handler));
+
+ mojo::test::BadMessageObserver bad_message_observer;
+ host->ValidateAndMaybeOverrideManifestForTesting(
+ blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
+ EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
+ ::testing::StartsWith(
+ "Manifest file_handlers accept extension contains invalid "
+ "control or format characters."));
+}
+
+IN_PROC_BROWSER_TEST_F(
+ ManifestBrowserTest,
+ BadMessage_FileHandlersAcceptExtensionInvalidControlCharacter) {
+ const GURL test_url =
+ embedded_test_server()->GetURL("/manifest/empty-manifest.html");
+ ASSERT_TRUE(NavigateToURL(shell(), test_url));
+
+ ManifestManagerHost* host = ManifestManagerHost::GetOrCreateForPage(
+ shell()->web_contents()->GetPrimaryPage());
+
+ mojo::FakeMessageDispatchContext fake_dispatch_context;
+ auto bad_manifest = blink::mojom::Manifest::New();
+ bad_manifest->start_url = test_url;
+ bad_manifest->id = test_url;
+ bad_manifest->scope = embedded_test_server()->GetURL("/manifest/");
+
+ blink::mojom::ManifestFileHandlerPtr file_handler =
+ blink::mojom::ManifestFileHandler::New();
+ file_handler->action = test_url;
+
+ std::vector<std::u16string> extensions;
+ extensions.push_back(u".png\u0001");
+ file_handler->accept[u"image/png"] = std::move(extensions);
+
+ bad_manifest->file_handlers.push_back(std::move(file_handler));
+
+ mojo::test::BadMessageObserver bad_message_observer;
+ host->ValidateAndMaybeOverrideManifestForTesting(
+ blink::mojom::ManifestRequestResult::kSuccess, std::move(bad_manifest));
+ EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
+ ::testing::StartsWith(
+ "Manifest file_handlers accept extension contains invalid "
+ "control or format characters."));
+}
+
IN_PROC_BROWSER_TEST_F(ManifestBrowserTest,
BadMessage_ProtocolHandlersActionCrossOrigin) {
const GURL test_url =
diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc b/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc
index 488fbdcf..3e0076d 100644
--- a/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc
+++ b/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc
@@ -3040,6 +3040,62 @@
ASSERT_EQ(0u, file_handlers.size());
}
+ // Extensions that contain format characters are invalid.
+ {
+ auto& manifest = ParseManifest(
+ R"({
+ "file_handlers": [
+ {
+ "name": "name",
+ "action": "/files",
+ "accept": {
+ "image/png": [
+ ".png\u202E"
+ ]
+ }
+ }
+ ]
+ })");
+ auto& file_handlers = manifest->file_handlers;
+
+ ASSERT_EQ(2u, GetErrorCount());
+ EXPECT_EQ(
+ "property 'accept' file extension ignored, contains invalid "
+ "control or format characters.",
+ errors()[0]);
+ EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
+ errors()[1]);
+ ASSERT_EQ(0u, file_handlers.size());
+ }
+
+ // Extensions that contain control characters are invalid.
+ {
+ auto& manifest = ParseManifest(
+ R"({
+ "file_handlers": [
+ {
+ "name": "name",
+ "action": "/files",
+ "accept": {
+ "image/png": [
+ ".png\u0001"
+ ]
+ }
+ }
+ ]
+ })");
+ auto& file_handlers = manifest->file_handlers;
+
+ ASSERT_EQ(2u, GetErrorCount());
+ EXPECT_EQ(
+ "property 'accept' file extension ignored, contains invalid "
+ "control or format characters.",
+ errors()[0]);
+ EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
+ errors()[1]);
+ ASSERT_EQ(0u, file_handlers.size());
+ }
+
// Invalid MIME types and those with parameters are stripped.
{
auto& manifest = ParseManifest(