Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Mobile
DescriptionIncorrect authorization in Mobile
ComponentMobile
Bug ClassLogic Error
Tracker514055709
Fix commitde779b89cea5 (chromium/src) +91/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_F
ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
modified
for
ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
modified
if
url/BUILD.gn
modified

Files Changed

  • ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
  • ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
  • url/BUILD.gn
  • url/url_constants_ios.h
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_
Loading diff…

Regression Test / PoC

shipped with the fix
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.
Loading diff…

Original Bug Report

reported by [email protected]

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.

  1. Host a page on https://victim.example that embeds a cross-origin iframe from https://attacker.example.
  2. In the iframe, include a link such as: <a href="telprompt://+15551234567" target="_top">Call Support</a>.
  3. In Chrome on iOS, click the link in the iframe.
  4. Observe if iOS displays a phone call confirmation prompt while the Chrome address bar still shows https://victim.example.
  5. 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.

View on issue tracker