CVE-2026-79217
Overview
Background
- `AppLauncherTabHelper`
- the iOS Chrome component that decides whether a navigation to an external-app URL scheme should be allowed and whether to launch the corresponding app.
- Call URL schemes
- URI schemes such as
tel:,telprompt:,facetime:,facetime-audio:that iOS routes to native apps to initiate a phone or video call, typically after a system prompt. - Cross-origin frame
- a subframe or window whose origin differs from the top-level page, which should not be able to silently drive privileged navigations on the user’s behalf.
- `ShouldAllowRequest`
- the tab-helper policy hook (inferred name; the containing function is not shown in the hunk) that returns a
PolicyDecisionofCancel()or allow for a candidate external navigation.
Root Cause Analysis
Before the patch, the cross-origin guard in app_launcher_tab_helper.mm only blocked url::kTelScheme (tel:) navigations originating from cross-origin frames, checking request_url.SchemeIs(url::kTelScheme) && request_info.target_frame_is_cross_origin. On iOS, however, several other schemes — telprompt:, facetime:, facetime-audio: and their prompt variants — behave equivalently to tel: in that they trigger a native call flow, yet they were not covered by the guard. A cross-origin frame could therefore navigate to one of these uncovered schemes and reach the app-launch path that tel: was specifically restricted from.
The patch introduces a helper UrlHasCallWithPromptScheme() that returns true for tel:, telprompt:, facetime:, facetime-prompt:, facetime-audio: and facetime-audio-prompt:, and replaces the bare tel: test in the cross-origin guard (and in the user-interaction gate) with this helper, so all call-prompt schemes are treated identically and blocked from cross-origin frames.
tel: scheme while other iOS schemes (telprompt:, facetime:, facetime-audio: and prompt variants) produce the same call behavior; the fix generalizes the check to UrlHasCallWithPromptScheme() so every equivalent scheme is covered.Attack Path
- Embed a cross-origin frame An attacker page loads a cross-origin iframe (or opens a cross-origin window) under their control on iOS Chrome.
- Navigate to an uncovered call scheme
From that cross-origin frame, initiate a navigation to
telprompt:,facetime:,facetime-audio:, or a prompt variant rather thantel:. - Bypass the tel-only guard
The pre-patch check
request_url.SchemeIs(url::kTelScheme) && target_frame_is_cross_origindoes not match the non-tel:scheme, so it does not returnPolicyDecision::Cancel(), and the navigation proceeds. - Reach the call-launch flow The request advances to the app-launch path, surfacing a phone/FaceTime call prompt for an attacker-chosen callee without proper cross-origin authorization.
Impact Assessment
telprompt: call flows to an attacker-chosen number/address that were meant to be blocked from cross-origin contexts, an incorrect-authorization issue in the Mobile (iOS) component. Exploitation requires the user to be on iOS Chrome and interacting with a page hosting the malicious frame; the description states “Incorrect authorization in Mobile” and no exploited-in-the-wild status is provided. The impact is limited to unauthorized initiation of the call prompt; no memory-safety escalation is implied.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
url/BUILD.gnios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mmurl/url_constants_ios.hios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
Audit Directions
- Scheme allow/deny lists keyed on one schemeSearch for security checks that test a single scheme (e.g.
SchemeIs(kTelScheme)) where the underlying OS treats a family of schemes equivalently; enumerate the full family. - Platform-specific URL handlersReview iOS/Android external-launch code paths for schemes the platform auto-routes to native apps (call, message, mail) that may bypass cross-origin or user-gesture gates.
- Duplicated scheme checksWhere the same scheme condition appears in multiple gates (user-interaction and cross-origin), verify both were updated to the shared helper so one does not lag behind.
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/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_ 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) {
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.