CVE-2026-79217
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm |
modified | |
forios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm |
modified | |
ifurl/BUILD.gn |
modified |
Files Changed
ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mmios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mmurl/BUILD.gnurl/url_constants_ios.h
Patch
From de779b89cea52a8979cac5e1ec88c8153d38ddd3 Mon Sep 17 00:00:00 2001 From: Quentin Pubert <[email protected]> Date: Thu, 02 Jul 2026 06:30:47 -0700 Subject: [PATCH] [iOS] Disallow cross-origin navigations to all call URL schemes This CL updates AppLauncherTabHelper to also disallow navigations to call URLs other than tel: e.g. telprompt:, facetime:, facetime-audio: from cross-origin frames. These schemes appear to behave the same as tel: on iOS so they should be treated the same. Fixed: 514055709 Change-Id: I673f00715aaa5f2b20892fa886742dc7166cf47b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8024832 Reviewed-by: Olivier Robin <[email protected]> Reviewed-by: Mike West <[email protected]> Commit-Queue: Quentin Pubert <[email protected]> Reviewed-by: Sylvain Defresne <[email protected]> Auto-Submit: Quentin Pubert <[email protected]> Cr-Commit-Position: refs/heads/main@{#1656056} --- diff --git a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm index 507aa5e..d01f8bb7 100644 --- a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm +++ b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm @@ -28,6 +28,7 @@ #import "ios/web/public/web_client.h" #import "net/base/apple/url_conversions.h" #import "url/gurl.h" +#import "url/url_constants_ios.h" namespace { @@ -48,6 +49,16 @@ return true; } +// Returns true if `url` has a scheme that shows a prompt to initiate a phone or +// video call. +bool UrlHasCallWithPromptScheme(const GURL& url) { + return url.SchemeIs(url::kTelScheme) || url.SchemeIs(url::kTelPromptScheme) || + url.SchemeIs(url::kFaceTimeScheme) || + url.SchemeIs(url::kFaceTimePromptScheme) || + url.SchemeIs(url::kFaceTimeAudioScheme) || + url.SchemeIs(url::kFaceTimeAudioPromptScheme); +} + // Returns True if `app_url` has a Chrome bundle URL scheme. bool HasChromeAppScheme(const GURL& app_url) { NSArray* chrome_schemes = @@ -134,7 +145,7 @@ } if (!(is_user_initiated || - (url.SchemeIs(url::kTelScheme) && user_tapped_recently))) { + (UrlHasCallWithPromptScheme(url) && user_tapped_recently))) { ShowAppLaunchAlert(AppLauncherAlertCause::kNoUserInteraction, url); return; } @@ -321,8 +332,9 @@ kNoAppLaunchRequest}; } - // Disallow navigations to tel: URLs from cross-origin frames. - if (request_url.SchemeIs(url::kTelScheme) && + // Disallow navigations to call or messaging URLs (tel:, telprompt:, + // facetime:, facetime-audio:, sms:) from cross-origin frames. + if (UrlHasCallWithPromptScheme(request_url) && request_info.target_frame_is_cross_origin) { return {PolicyDecision::Cancel(), kNoAppLaunchRequest}; } diff --git a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm index 0ccb9a65..e2b1e14 100644 --- a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm +++ b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm @@ -13,6 +13,7 @@ #import "base/location.h" #import "base/memory/raw_ptr.h" #import "base/memory/scoped_refptr.h" +#import "base/strings/sys_string_conversions.h" #import "base/test/scoped_feature_list.h" #import "base/time/default_clock.h" #import "components/policy/core/browser/url_list/policy_blocklist_service.h" @@ -876,6 +877,59 @@ EXPECT_EQ(2U, delegate_.GetAppLaunchCount()); } +// Tests that tel:, facetime:, facetime-prompt:, facetime-audio:, +// facetime-audio-prompt: and telprompt: URLs are blocked when the target frame +// is cross-origin with respect to the source origin, and allowed when +// same-origin. +// TODO(crbug.com/40166678): The test fails on device. +#if TARGET_OS_SIMULATOR +#define MAYBE_CallWithPromptUrls CallWithPromptUrls +#else +#define MAYBE_CallWithPromptUrls DISABLED_CallWithPromptUrls +#endif +TEST_F(AppLauncherTabHelperTest, MAYBE_CallWithPromptUrls) { + NSArray<NSString*>* url_strings = @[ + @"tel:+12345551212", + @"facetime://+12345551212", + @"facetime-prompt://+12345551212", + @"facetime-audio://+12345551212", + @"telprompt:+12345551212", + @"facetime-audio-prompt://+12345551212", + ]; + for (NSString* url_string in url_strings) { + EXPECT_FALSE(TestShouldAllowRequest(url_string, + /*target_frame_is_main=*/true, + /*target_frame_is_cross_origin=*/true, + /*target_window_is_cross_origin=*/false, + /*is_user_initiated=*/true, + /*user_tapped_recently=*/true)); + EXPECT_EQ(0U, delegate_.GetAppLaunchCount()) + << base::SysNSStringToUTF8(url_string); + + EXPECT_FALSE(TestShouldAllowRequest(url_string, + /*target_frame_is_main=*/false, + /*target_frame_is_cross_origin=*/true, + /*target_window_is_cross_origin=*/false, + /*is_user_initiated=*/true, + /*user_tapped_recently=*/true)); + EXPECT_EQ(0U, delegate_.GetAppLaunchCount()) + << base::SysNSStringToUTF8(url_string); + } + + size_t expected_launch_count = 0U; + for (NSString* url_string in url_strings) { + EXPECT_FALSE(TestShouldAllowRequest(url_string, + /*target_frame_is_main=*/true, + /*target_frame_is_cross_origin=*/false, + /*target_window_is_cross_origin=*/false, + /*is_user_initiated=*/true, + /*user_tapped_recently=*/true)); + ++expected_launch_count; + EXPECT_EQ(expected_launch_count, delegate_.GetAppLaunchCount()) + << base::SysNSStringToUTF8(url_string); + } +} + // Tests that URLs with Chrome Bundle schemes are blocked on main frames and // iframes. // TODO(crbug.com/40166678): The test fails on device. diff --git a/url/BUILD.gn b/url/BUILD.gn index 6951ca62..f39d8aa 100644 --- a/url/BUILD.gn +++ b/url/BUILD.gn @@ -98,6 +98,10 @@ output_name = "url_lib" } + if (is_ios) { + sources += [ "url_constants_ios.h" ] + } + # ICU support. if (use_platform_icu_alternatives) { if (is_android) { diff --git a/url/url_constants_ios.h b/url/url_constants_ios.h new file mode 100644 index 0000000..ec920f6 --- /dev/null +++ b/url/url_constants_ios.h @@ -0,0 +1,18 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef URL_URL_CONSTANTS_IOS_H_ +#define URL_URL_CONSTANTS_IOS_H_ + +namespace url { + +inline constexpr char kFaceTimeScheme[] = "facetime"; +inline constexpr char kFaceTimePromptScheme[] = "facetime-prompt"; +inline constexpr char kFaceTimeAudioScheme[] = "facetime-audio"; +inline constexpr char kFaceTimeAudioPromptScheme[] = "facetime-audio-prompt"; +inline constexpr char kTelPromptScheme[] = "telprompt"; + +} // namespace url + +#endif // URL_URL_CONSTANTS_IOS_H_
Regression Test / PoC
diff --git a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
index 0ccb9a65..e2b1e14 100644
--- a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
+++ b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
@@ -13,6 +13,7 @@
#import "base/location.h"
#import "base/memory/raw_ptr.h"
#import "base/memory/scoped_refptr.h"
+#import "base/strings/sys_string_conversions.h"
#import "base/test/scoped_feature_list.h"
#import "base/time/default_clock.h"
#import "components/policy/core/browser/url_list/policy_blocklist_service.h"
@@ -876,6 +877,59 @@
EXPECT_EQ(2U, delegate_.GetAppLaunchCount());
}
+// Tests that tel:, facetime:, facetime-prompt:, facetime-audio:,
+// facetime-audio-prompt: and telprompt: URLs are blocked when the target frame
+// is cross-origin with respect to the source origin, and allowed when
+// same-origin.
+// TODO(crbug.com/40166678): The test fails on device.
+#if TARGET_OS_SIMULATOR
+#define MAYBE_CallWithPromptUrls CallWithPromptUrls
+#else
+#define MAYBE_CallWithPromptUrls DISABLED_CallWithPromptUrls
+#endif
+TEST_F(AppLauncherTabHelperTest, MAYBE_CallWithPromptUrls) {
+ NSArray<NSString*>* url_strings = @[
+ @"tel:+12345551212",
+ @"facetime://+12345551212",
+ @"facetime-prompt://+12345551212",
+ @"facetime-audio://+12345551212",
+ @"telprompt:+12345551212",
+ @"facetime-audio-prompt://+12345551212",
+ ];
+ for (NSString* url_string in url_strings) {
+ EXPECT_FALSE(TestShouldAllowRequest(url_string,
+ /*target_frame_is_main=*/true,
+ /*target_frame_is_cross_origin=*/true,
+ /*target_window_is_cross_origin=*/false,
+ /*is_user_initiated=*/true,
+ /*user_tapped_recently=*/true));
+ EXPECT_EQ(0U, delegate_.GetAppLaunchCount())
+ << base::SysNSStringToUTF8(url_string);
+
+ EXPECT_FALSE(TestShouldAllowRequest(url_string,
+ /*target_frame_is_main=*/false,
+ /*target_frame_is_cross_origin=*/true,
+ /*target_window_is_cross_origin=*/false,
+ /*is_user_initiated=*/true,
+ /*user_tapped_recently=*/true));
+ EXPECT_EQ(0U, delegate_.GetAppLaunchCount())
+ << base::SysNSStringToUTF8(url_string);
+ }
+
+ size_t expected_launch_count = 0U;
+ for (NSString* url_string in url_strings) {
+ EXPECT_FALSE(TestShouldAllowRequest(url_string,
+ /*target_frame_is_main=*/true,
+ /*target_frame_is_cross_origin=*/false,
+ /*target_window_is_cross_origin=*/false,
+ /*is_user_initiated=*/true,
+ /*user_tapped_recently=*/true));
+ ++expected_launch_count;
+ EXPECT_EQ(expected_launch_count, delegate_.GetAppLaunchCount())
+ << base::SysNSStringToUTF8(url_string);
+ }
+}
+
// Tests that URLs with Chrome Bundle schemes are blocked on main frames and
// iframes.
// TODO(crbug.com/40166678): The test fails on device.
Original Bug Report
Potential origin attribution spoofing via incomplete scheme denylist in AppLauncherTabHelper
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: AppLauncherTabHelper for iOS fails to block several communication schemes (facetime, sms, telprompt) when initiated from cross-origin frames targeting the main frame. This potentially allows an attacker to trigger OS-level call/message prompts while spoofing the top-level origin in the omnibox.
Affected files:
ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
Estimated timestamp from git blame: 2021-01-15
Description
In ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm, the method AppLauncherTabHelper::GetPolicyDecisionAndOptionalAppLaunchRequest contains logic designed to prevent cross-origin frames from initiating phone calls via the tel: scheme. This is a security measure to prevent origin attribution spoofing, where a malicious iframe triggers a call prompt while the user is viewing a different, legitimate top-level site.
However, this denylist is incomplete. While tel: is explicitly blocked if initiated from a cross-origin frame, other equivalent communication schemes that trigger similar OS-level prompts are not checked. These include:
facetime:/facetime-audio:sms:telprompt:
Vulnerability Details
The problematic logic in app_launcher_tab_helper.mm is as follows:
// Disallow navigations to tel: URLs from cross-origin frames.
if (request_url.SchemeIs(url::kTelScheme) &&
request_info.target_frame_is_cross_origin) {
return {PolicyDecision::Cancel(), kNoAppLaunchRequest};
}
Because url::kTelScheme only matches the exact string "tel", equivalent or similar communication schemes bypass this check. A cross-origin iframe can navigate the top frame (target="_top") to a URL using one of the missing schemes. Because the navigation targets the main frame, target_frame_is_main is true, and it bypasses subsequent subframe-specific app launch restrictions.
If the navigation is accompanied by a user gesture, Chrome will proceed to call [[UIApplication sharedApplication] openURL:...] via its delegate without displaying a Chrome-side confirmation alert. This allows an attacker-controlled iframe to surface an iOS FaceTime call sheet, Messages UI, or a phone call prompt (via telprompt:) while the Chrome omnibox still displays the victim top-level origin.
Potential Reproduction Steps
Note: These are suggested steps based on code analysis; our current environment does not support executing this on iOS hardware.
- Host a page on
https://victim.examplethat embeds a cross-origin iframe fromhttps://attacker.example. - In the iframe, include a link such as:
<a href="telprompt://+15551234567" target="_top">Call Support</a>. - In Chrome on iOS, click the link in the iframe.
- Observe if iOS displays a phone call confirmation prompt while the Chrome address bar still shows
https://victim.example. - Compare this behavior to a
tel:URL, which should be correctly blocked by the existing check.
Suggested Fix
Expand the scheme check in AppLauncherTabHelper::GetPolicyDecisionAndOptionalAppLaunchRequest to include all communication schemes that trigger OS-level prompts. It is recommended to use a set or a helper function to identify these schemes (e.g., facetime, facetime-audio, sms, telprompt, and tel).
// Disallow navigations to communication URLs from cross-origin frames.
bool is_communication_scheme = request_url.SchemeIs(url::kTelScheme) ||
request_url.SchemeIs("telprompt") ||
request_url.SchemeIs("facetime") ||
request_url.SchemeIs("facetime-audio") ||
request_url.SchemeIs("sms");
if (is_communication_scheme && request_info.target_frame_is_cross_origin) {
return {PolicyDecision::Cancel(), kNoAppLaunchRequest};
}
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.