Medium watch CVSS 4.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Mobile
DescriptionIncorrect authorization in Mobile
ComponentMobile
Sub-componentiOS
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker514055709
Fix commitde779b89cea5 (chromium/src) +91/-3
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-07-02 (public git)
Disclosed2026-08-25
Patch gap54 days — fix public in source before the advisory

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 PolicyDecision of Cancel() 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.

Key insight
The single mistake was hard-coding the cross-origin restriction to the one 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

  1. 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.
  2. Navigate to an uncovered call scheme From that cross-origin frame, initiate a navigation to telprompt:, facetime:, facetime-audio:, or a prompt variant rather than tel:.
  3. Bypass the tel-only guard The pre-patch check request_url.SchemeIs(url::kTelScheme) && target_frame_is_cross_origin does not match the non-tel: scheme, so it does not return PolicyDecision::Cancel(), and the navigation proceeds.
  4. 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

A malicious cross-origin frame on iOS Chrome could initiate FaceTime or 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

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

  • url/BUILD.gn
  • ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
  • url/url_constants_ios.h
  • ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm

Audit Directions

  • Scheme allow/deny lists keyed on one scheme
    Search 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 handlers
    Review 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 checks
    Where 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.
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) {
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