CVE-2026-11215
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifurl/BUILD.gn |
modified | |
ifurl/android/java/src/org/chromium/url/IDNStringUtil.java |
modified |
Files Changed
components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.javacomponents/cronet/android/test/javatests/src/org/chromium/net/PkpTest.javacomponents/cronet/android/test/res/xml/network_security_config.xmlurl/BUILD.gnurl/android/java/src/org/chromium/url/IDNStringUtil.java
Patch
From 0dda54862dc6cb6269417d5a46ca86c3468e8b7f Mon Sep 17 00:00:00 2001
From: Etienne Dechamps <edechamps@google.com>
Date: Wed, 27 May 2026 00:38:10 -0700
Subject: [PATCH] Reland "Use IDNA2008 on Android API 24+"
This reverts commit 48b44d2d40de8b245129a42dda1ac37dc7be083e.
Reason for revert: re-landing with fix for ObsoleteSdkInt warning on
some build configs that have min SDK 24+ (e.g. android-cast-arm-rel,
apparently)
Original change's description:
> Revert "Use IDNA2008 on Android API 24+"
>
> This reverts commit 2c4aacb85e2756af5d9b917669f6bf8387a22e83.
>
> Reason for revert:
> LUCI Bisection has identified this change as the culprit of a build failure. See the analysis: https://ci.chromium.org/ui/p/chromium/bisection/compile-analysis/b/8680744491180794513
>
> Sample failed build: https://ci.chromium.org/b/8680744491180794513
>
> If this is a false positive, please report it at http://b.corp.google.com/createIssue?component=1199205&description=Analysis%3A+https%3A%2F%2Fchromium-review.googlesource.com%2Fc%2Fchromium%2Fsrc%2F%2B%2F7868313&format=PLAIN&priority=P3&title=Wrongly+blamed+https%3A%2F%2Fci.chromium.org%2Fui%2Fp%2Fchromium%2Fbisection%2Fcompile-analysis%2Fb%2F8680744491180794513&type=BUG
>
> Original change's description:
> > Use IDNA2008 on Android API 24+
> >
> > Bug: 513446116
> > Change-Id: Ib466aabeff577114b3c6cb77a11be35a63b75b55
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7868313
> > Reviewed-by: Stefano Duo <stefanoduo@google.com>
> > Commit-Queue: Etienne Dechamps <edechamps@google.com>
> > Reviewed-by: Mike West <mkwst@chromium.org>
> > Cr-Commit-Position: refs/heads/main@{#1636053}
> >
>
> Bug: 513446116
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Change-Id: I30ad3ec5c98e6572042f46f70e7e8d2cb8eccf25
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7874650
> Commit-Queue: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
> Bot-Commit: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
> Owners-Override: luci-bisection@appspot.gserviceaccount.com <luci-bisection@appspot.gserviceaccount.com>
> Cr-Commit-Position: refs/heads/main@{#1636057}
Bug: 513446116
Cq-Include-Trybots: luci.chromium.try:android-cast-arm-rel
Change-Id: Ic27540f5c30b9f52887ca889fc2a5063f0ed7684
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7874801
Reviewed-by: Mike West <mkwst@chromium.org>
Auto-Submit: Etienne Dechamps <edechamps@google.com>
Commit-Queue: Mike West <mkwst@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1636742}
---
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java
index 35c86d1..e5e12c51 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java
@@ -6,6 +6,8 @@
import static com.google.common.truth.Truth.assertThat;
+import android.os.Build;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -44,10 +46,14 @@
// Note the strategic use of ß as our test character, which is handled differently based on
// which version of IDNA is used - see Unicode Technical Standard #46.
final var IDN_UNICODE = "example-idn-begin-ß-end";
- // Cronet currently uses IDNA2003, under which "ß" is mapped to "ss".
- // TODO(https://crbug.com/513446116): ideally, Cronet should use IDNA2008, which is the
- // recommended version and the one used by modern browsers.
- final var EXPECTED_IDN_ASCII = "example-idn-begin-ss-end";
+ // On Android API <24 Cronet uses IDNA2003, under which "ß" is mapped to "ss".
+ // On Android API 24+ Cronet uses IDNA2008, under which "ß" is preserved and triggers
+ // punycode conversion.
+ // See also https://crbug.com/513446116.
+ final var EXPECTED_IDN_ASCII =
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ ? "example-idn-begin-ss-end"
+ : "xn--example-idn-begin--end-71b";
var testFramework = mTestRule.getTestFramework();
testFramework.applyEngineBuilderPatch(
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
index 48e5ad5..04f6bcf 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
@@ -483,10 +483,14 @@
// Note the strategic use of ß as our test character, which is handled differently based on
// which version of IDNA is used - see Unicode Technical Standard #46.
final var IDN_UNICODE = "example-idn-begin-ß-end";
- // Cronet currently uses IDNA2003, under which "ß" is mapped to "ss".
- // TODO(https://crbug.com/513446116): ideally, Cronet should use IDNA2008, which is the
- // recommended version and the one used by modern browsers.
- final var EXPECTED_IDN_ASCII = "example-idn-begin-ss-end";
+ // On Android API <24 Cronet uses IDNA2003, under which "ß" is mapped to "ss".
+ // On Android API 24+ Cronet uses IDNA2008, under which "ß" is preserved and triggers
+ // punycode conversion.
+ // See also https://crbug.com/513446116.
+ final var EXPECTED_IDN_ASCII =
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ ? "example-idn-begin-ss-end"
+ : "xn--example-idn-begin--end-71b";
final var testFramework = mTestRule.getTestFramework();
applyCronetEngineBuilderConfigurationPatchWithMockCertVerifier(
diff --git a/components/cronet/android/test/res/xml/network_security_config.xml b/components/cronet/android/test/res/xml/network_security_config.xml
index 6f8241f..d8e8c5154 100644
--- a/components/cronet/android/test/res/xml/network_security_config.xml
+++ b/components/cronet/android/test/res/xml/network_security_config.xml
@@ -64,5 +64,6 @@
<domain includeSubdomains="true">test-hostname</domain>
<!-- Used by HostnameTest -->
<domain includeSubdomains="true">example-idn-begin-ss-end</domain>
+ <domain includeSubdomains="true">xn--example-idn-begin--end-71b</domain>
</domain-config>
</network-security-config>
diff --git a/url/BUILD.gn b/url/BUILD.gn
index 8ecaa19..e231b8f 100644
--- a/url/BUILD.gn
+++ b/url/BUILD.gn
@@ -214,6 +214,9 @@
if (use_platform_icu_alternatives) {
# Unit tests that are not supported by the current ICU alternatives on
# Android.
+ # TODO(https://crbug.com/513446116): revisit this - in Android >=24 we now
+ # use android.icu.text.IDNA which implements IDNA2008 and should therefore
+ # be consistent with ICU.
if (is_android) {
sources -= [
"url_canon_icu_unittest.cc",
diff --git a/url/android/java/src/org/chromium/url/IDNStringUtil.java b/url/android/java/src/org/chromium/url/IDNStringUtil.java
index eb458fb..d9406c8 100644
--- a/url/android/java/src/org/chromium/url/IDNStringUtil.java
+++ b/url/android/java/src/org/chromium/url/IDNStringUtil.java
@@ -4,6 +4,9 @@
package org.chromium.url;
+import android.icu.text.IDNA;
+import android.os.Build;
+
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
@@ -11,17 +14,60 @@
import org.chromium.build.annotations.Nullable;
import java.net.IDN;
+import java.util.StringJoiner;
/** This class is used to convert unicode IDN domain names to ASCII, when not building with ICU. */
@JNINamespace("url::android")
@NullMarked
public class IDNStringUtil {
+ private static final @Nullable IDNA sIDNA = getIDNA();
+
+ // We suppress "Unnecessary; SDK_INT is never < 24 [ObsoleteSdkInt]" warnings that occur in
+ // some, but not all, build configs because these build configs set their min SDK version to
+ // 24+. See https://crrev.com/c/7874650.
+ @SuppressWarnings("ObsoleteSdkInt")
+ private static @Nullable IDNA getIDNA() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return null;
+ }
+
+ var sIDNA =
+ IDNA.getUTS46Instance(
+ IDNA.NONTRANSITIONAL_TO_ASCII | IDNA.NONTRANSITIONAL_TO_UNICODE);
+ if (sIDNA == null) {
+ throw new IllegalStateException("Failed to create IDNA instance");
+ }
+ return sIDNA;
+ }
+
/**
- * Attempts to convert a Unicode string to an ASCII string using IDN rules. As of May 2014, the
- * underlying Java function IDNA2003.
+ * Attempts to convert a Unicode hostname to an ASCII hostname using IDN rules. Uses IDNA2008 on
+ * Android API 24+, IDNA2003 otherwise. See also https://crbug.com/513446116. Additionally, the
+ * resulting ASCII hostname is validated against RFC 1122 and RFC 1123 rules.
*/
- public static String idnToASCII(String src) {
- return IDN.toASCII(src, IDN.USE_STD3_ASCII_RULES);
+ public static String idnToASCII(String unicodeHostname) {
+ if (sIDNA == null) {
+ return IDN.toASCII(unicodeHostname, IDN.USE_STD3_ASCII_RULES);
+ }
+
+ var asciiHostnameBuilder = new StringBuilder();
+ var info = new IDNA.Info();
+ sIDNA.nameToASCII(unicodeHostname, asciiHostnameBuilder, info);
+ if (info.hasErrors()) {
+ var errors = new StringJoiner(", ");
+ for (var error : info.getErrors()) {
Regression Test / PoC
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java
index 35c86d1..e5e12c51 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/HostnameTest.java
@@ -6,6 +6,8 @@
import static com.google.common.truth.Truth.assertThat;
+import android.os.Build;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -44,10 +46,14 @@
// Note the strategic use of ß as our test character, which is handled differently based on
// which version of IDNA is used - see Unicode Technical Standard #46.
final var IDN_UNICODE = "example-idn-begin-ß-end";
- // Cronet currently uses IDNA2003, under which "ß" is mapped to "ss".
- // TODO(https://crbug.com/513446116): ideally, Cronet should use IDNA2008, which is the
- // recommended version and the one used by modern browsers.
- final var EXPECTED_IDN_ASCII = "example-idn-begin-ss-end";
+ // On Android API <24 Cronet uses IDNA2003, under which "ß" is mapped to "ss".
+ // On Android API 24+ Cronet uses IDNA2008, under which "ß" is preserved and triggers
+ // punycode conversion.
+ // See also https://crbug.com/513446116.
+ final var EXPECTED_IDN_ASCII =
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ ? "example-idn-begin-ss-end"
+ : "xn--example-idn-begin--end-71b";
var testFramework = mTestRule.getTestFramework();
testFramework.applyEngineBuilderPatch(
diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
index 48e5ad5..04f6bcf 100644
--- a/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
+++ b/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java
@@ -483,10 +483,14 @@
// Note the strategic use of ß as our test character, which is handled differently based on
// which version of IDNA is used - see Unicode Technical Standard #46.
final var IDN_UNICODE = "example-idn-begin-ß-end";
- // Cronet currently uses IDNA2003, under which "ß" is mapped to "ss".
- // TODO(https://crbug.com/513446116): ideally, Cronet should use IDNA2008, which is the
- // recommended version and the one used by modern browsers.
- final var EXPECTED_IDN_ASCII = "example-idn-begin-ss-end";
+ // On Android API <24 Cronet uses IDNA2003, under which "ß" is mapped to "ss".
+ // On Android API 24+ Cronet uses IDNA2008, under which "ß" is preserved and triggers
+ // punycode conversion.
+ // See also https://crbug.com/513446116.
+ final var EXPECTED_IDN_ASCII =
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ ? "example-idn-begin-ss-end"
+ : "xn--example-idn-begin--end-71b";
final var testFramework = mTestRule.getTestFramework();
applyCronetEngineBuilderConfigurationPatchWithMockCertVerifier(
diff --git a/components/cronet/android/test/res/xml/network_security_config.xml b/components/cronet/android/test/res/xml/network_security_config.xml
index 6f8241f..d8e8c5154 100644
--- a/components/cronet/android/test/res/xml/network_security_config.xml
+++ b/components/cronet/android/test/res/xml/network_security_config.xml
@@ -64,5 +64,6 @@
<domain includeSubdomains="true">test-hostname</domain>
<!-- Used by HostnameTest -->
<domain includeSubdomains="true">example-idn-begin-ss-end</domain>
+ <domain includeSubdomains="true">xn--example-idn-begin--end-71b</domain>
</domain-config>
</network-security-config>
Original Bug Report
Potential host confusion and PKP bypass in Cronet-Android due to transitional IDNA processing
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: Cronet for Android uses a platform-specific IDNA implementation that defaults to transitional processing, deviating from the WHATWG URL standard. This discrepancy leads to host confusion and allows for the bypass of Public Key Pinning (PKP) protections for domains containing certain ‘deviation characters’.
Affected files:
url/url_idna_icu_alternatives_android.ccurl/android/java/src/org/chromium/url/IDNStringUtil.javacomponents/cronet/android/java/src/org/chromium/net/impl/CronetEngineBuilderImpl.javacomponents/cronet/android/cronet_url_request_adapter.ccnet/url_request/url_request_job.cc
Estimated timestamp from git blame: 2022-11-17
Summary
When built for Android with is_cronet_build = true, Cronet utilizes platform-specific ICU alternatives to minimize binary size. This causes host canonicalization to delegate to the Android platform’s java.net.IDN API. Unlike standard Chromium, which uses UTS #46 non-transitional processing (as required by the WHATWG URL standard), the Android platform implementation defaults to transitional processing. This discrepancy causes certain Unicode characters to resolve to different registrable domains than intended.
Technical Details
In Cronet-Android, IDN conversion is handled in url/url_idna_icu_alternatives_android.cc, which calls org.chromium.url.IDNStringUtil.idnToASCII via JNI. This Java method invokes java.net.IDN.toASCII(src, IDN.USE_STD3_ASCII_RULES).
The mapping divergence for UTS #46 ‘deviation characters’ results in different canonical hosts:
| Input Host | Standard Chrome (Non-transitional) | Cronet-Android (Transitional) |
|---|---|---|
straße.de |
xn--strae-oqa.de |
strasse.de |
βόλος.gr |
xn--nxasmq6b.gr |
xn--nxasmm1c.gr |
a\u200Db.com (ZWJ) |
Rejected/Correctly Encoded | ab.com (ZWJ silently deleted) |
Security Impact
- Host Confusion / Parser Differential: A URL containing these characters resolves to a different TLS endpoint in Cronet compared to a WHATWG-conforming component. An attacker who registers the transitional variant (e.g.,
strasse.de) can intercept traffic intended for the non-transitional domain (xn--strae-oqa.de). - PKP Bypass: If an application developer pins the correct A-label
xn--strae-oqa.deforstraße.de, Cronet’s canonicalization maps the request host tostrasse.de. During theTransportSecurityStatelookup, the request host (strasse.de) fails to match the registered pin (xn--strae-oqa.de), allowing the connection to proceed without enforcement to a potentially malicious domain. - Origin Isolation Bypass: Characters like Zero-Width Joiners (ZWJ) are silently stripped, potentially allowing different origins to be treated as identical by the Cronet engine.
Potential Attack Scenario
- An attacker registers the domain
strasse.de(the transitional variant ofstraße.de). - A user attempts to visit
https://straße.de/using an application embedded with Cronet. - Cronet canonicalizes the host to
strasse.deand resolves it to the attacker’s server. - If the developer has configured PKP for
straße.deusing the non-transitional A-labelxn--strae-oqa.de, the check fails to find a matching pin forstrasse.de, and the connection proceeds.
Suggested Fix
Cronet-Android should avoid delegating IDNA processing to java.net.IDN if it cannot be configured for non-transitional UTS #46 processing. Ideally, Cronet should use a consistent UTS #46 non-transitional implementation across all platforms, even if it requires a small increase in binary size, to ensure URL standard compliance and security invariant consistency.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.