Prevent USSD codes via Click to Call

Click to Call allows users to send a phone number from their Chrome
desktop instance to their Android phone. This number either comes from a
user's selection and sent via the context menu, or by clicking on a link
with a "tel:" href.
Sending from the context menu is gated by a regular expression and will
not allow any special characters like '#' or '*' to be contained in the
phone number.
Sending link hrefs does not go through that check as we assume the link
is a valid phone number. We do call GURL::GetContent() to get the number
which should discard anything after a (and including the) '#' character.
However, we also URL-decoded the resulting string before then sending it
over to Android, where we URL-decoded it again when constructing the
Dialer intent. This allows sending double-URL-encoded USSD tel links
which will be sent straight to the Dialer on certain Android versions
and device states.

The fix here is on both desktop and Android side:
Desktop:
 - URL-decode the number and ignore if it contains '#', '*' or '%'.
 - Send the raw number (URL-encoded) to Android
Android:
 - Verify that URL-decoding the received raw number is valid as above
 - Show the decoded number in the notification
 - Parse the raw number in Java into a Uri object for the Dialer

Together this makes sure that we only URL-decode tel: links once and
verify it on both sender and receiver side before passing it on to the
Android Dialer.

Bug: 1180510
Test: updated unit_tests and browser_tests to check for conversion
Change-Id: Idf380b629cdf00155ecab054398af69f37ec2ef9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2825704
Reviewed-by: Robert Kaplow <rkaplow@chromium.org>
Reviewed-by: David Jacobo <djacobo@chromium.org>
Reviewed-by: Gayane Petrosyan <gayane@chromium.org>
Reviewed-by: Istiaque Ahmed <lazyboy@chromium.org>
Reviewed-by: Peter Beverloo <peter@chromium.org>
Commit-Queue: Richard Knoll <knollr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#875572}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandler.java
index 5bec93b..ef5b10f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandler.java
@@ -103,11 +103,12 @@
      */
     private static void displayNotification(String phoneNumber) {
         Context context = ContextUtils.getApplicationContext();
+        String contentTitle = Uri.decode(phoneNumber);
         SharingNotificationUtil.showNotification(
                 NotificationUmaTracker.SystemNotificationType.CLICK_TO_CALL,
                 NotificationConstants.GROUP_CLICK_TO_CALL,
                 NotificationConstants.NOTIFICATION_ID_CLICK_TO_CALL,
-                getContentIntentProvider(phoneNumber), /*deleteIntent=*/null, phoneNumber,
+                getContentIntentProvider(phoneNumber), /*deleteIntent=*/null, contentTitle,
                 context.getResources().getString(R.string.click_to_call_notification_text),
                 R.drawable.ic_devices_16dp, R.drawable.ic_dialer_icon_blue_40dp,
                 R.color.default_icon_color_blue, /*startsActivity=*/true);
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandlerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandlerTest.java
index 7753e3c7a8..2c7a059 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandlerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/sharing/click_to_call/ClickToCallMessageHandlerTest.java
@@ -5,22 +5,33 @@
 package org.chromium.chrome.browser.sharing.click_to_call;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.robolectric.Shadows.shadowOf;
 
+import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Context;
+import android.content.Intent;
 import android.os.Build;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowNotification;
 import org.robolectric.shadows.ShadowNotificationManager;
 
+import org.chromium.base.ContextUtils;
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.base.test.util.Feature;
 import org.chromium.chrome.browser.device.DeviceConditions;
 import org.chromium.chrome.browser.device.ShadowDeviceConditions;
+import org.chromium.chrome.browser.notifications.NotificationConstants;
 import org.chromium.net.ConnectionType;
 
 /**
@@ -30,6 +41,15 @@
 @RunWith(BaseRobolectricTestRunner.class)
 @Config(manifest = Config.NONE, shadows = {ShadowDeviceConditions.class})
 public class ClickToCallMessageHandlerTest {
+    @Spy
+    private Context mContext = RuntimeEnvironment.application.getApplicationContext();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ContextUtils.initApplicationContextForTests(mContext);
+    }
+
     /**
      * Android Q+ should always display a notification to open the dialer.
      */
@@ -71,6 +91,25 @@
         ClickToCallMessageHandler.handleMessage("18004444444");
 
         assertEquals(0, getShadowNotificationManager().size());
+        verify(mContext, times(1)).startActivity(any(Intent.class));
+    }
+
+    @Test
+    @Feature({"Browser", "Sharing", "ClickToCall"})
+    @Config(sdk = Build.VERSION_CODES.Q)
+    public void testHandleMessage_decodesUrlForNotification() {
+        setIsScreenOnAndUnlocked(true);
+
+        ClickToCallMessageHandler.handleMessage("%2B44%201234");
+
+        ShadowNotificationManager manager = getShadowNotificationManager();
+        assertEquals(1, manager.size());
+
+        Notification notification =
+                manager.getNotification(NotificationConstants.GROUP_CLICK_TO_CALL,
+                        NotificationConstants.NOTIFICATION_ID_CLICK_TO_CALL);
+        ShadowNotification shadowNotification = shadowOf(notification);
+        assertEquals("+44 1234", shadowNotification.getContentTitle());
     }
 
     private void setIsScreenOnAndUnlocked(boolean isScreenOnAndUnlocked) {
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 066d87c..8f4b4ec 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1536,6 +1536,10 @@
     "sharesheet/sharesheet_types.h",
     "sharing/ack_message_handler.cc",
     "sharing/ack_message_handler.h",
+    "sharing/click_to_call/click_to_call_utils.cc",
+    "sharing/click_to_call/click_to_call_utils.h",
+    "sharing/click_to_call/phone_number_regex.cc",
+    "sharing/click_to_call/phone_number_regex.h",
     "sharing/features.cc",
     "sharing/features.h",
     "sharing/ping_message_handler.cc",
@@ -3970,10 +3974,6 @@
       "sharing/click_to_call/click_to_call_metrics.h",
       "sharing/click_to_call/click_to_call_ui_controller.cc",
       "sharing/click_to_call/click_to_call_ui_controller.h",
-      "sharing/click_to_call/click_to_call_utils.cc",
-      "sharing/click_to_call/click_to_call_utils.h",
-      "sharing/click_to_call/phone_number_regex.cc",
-      "sharing/click_to_call/phone_number_regex.h",
       "sharing/shared_clipboard/remote_copy_message_handler.cc",
       "sharing/shared_clipboard/remote_copy_message_handler.h",
       "sharing/shared_clipboard/shared_clipboard_context_menu_observer.cc",
diff --git a/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog.cc b/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog.cc
index 7f4b9af..e87e9cf1 100644
--- a/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog.cc
+++ b/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog.cc
@@ -380,7 +380,7 @@
   auto* device = it->get();
 
   ClickToCallUiController::GetOrCreateFromWebContents(web_contents)
-      ->OnDeviceSelected(GetUnescapedURLContent(url), *device,
+      ->OnDeviceSelected(url.GetContent(), *device,
                          SharingClickToCallEntryPoint::kLeftClickLink);
 }
 
diff --git a/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog_unittest.cc b/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog_unittest.cc
index c137956..73437cd 100644
--- a/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog_unittest.cc
+++ b/chrome/browser/ash/arc/intent_helper/arc_external_protocol_dialog_unittest.cc
@@ -9,6 +9,8 @@
 #include "chrome/browser/sharing/click_to_call/click_to_call_ui_controller.h"
 #include "chrome/browser/sharing/fake_device_info.h"
 #include "chrome/browser/sharing/mock_sharing_service.h"
+#include "chrome/browser/sharing/proto/click_to_call_message.pb.h"
+#include "chrome/browser/sharing/proto/sharing_message.pb.h"
 #include "chrome/browser/sharing/sharing_service_factory.h"
 #include "chrome/browser/ui/app_list/arc/arc_app_test.h"
 #include "chrome/test/base/browser_with_test_window_test.h"
@@ -993,6 +995,13 @@
   EXPECT_TRUE(in_out_safe_to_bypass_ui);
 }
 
+MATCHER_P(ProtoEquals, message, "") {
+  std::string expected_serialized, actual_serialized;
+  message.SerializeToString(&expected_serialized);
+  arg.SerializeToString(&actual_serialized);
+  return expected_serialized == actual_serialized;
+}
+
 // Tests that clicking on a device calls through to SharingService.
 TEST_F(ArcExternalProtocolDialogTestUtils, TestSelectDeviceForTelLink) {
   CreateTab(/*started_from_arc=*/false);
@@ -1004,13 +1013,18 @@
   std::vector<std::unique_ptr<syncer::DeviceInfo>> devices;
   devices.push_back(CreateFakeDeviceInfo(device_guid));
 
-  EXPECT_CALL(
-      *sharing_service,
-      SendMessageToDevice(Property(&syncer::DeviceInfo::guid, device_guid),
-                          testing::_, testing::_, testing::_));
+  GURL phone_number("tel:073%2099%209999%2099");
+
+  chrome_browser_sharing::SharingMessage sharing_message;
+  sharing_message.mutable_click_to_call_message()->set_phone_number(
+      phone_number.GetContent());
+  EXPECT_CALL(*sharing_service,
+              SendMessageToDevice(
+                  Property(&syncer::DeviceInfo::guid, device_guid), testing::_,
+                  ProtoEquals(sharing_message), testing::_));
 
   OnIntentPickerClosedForTesting(
-      rvh->GetProcess()->GetID(), rvh->GetRoutingID(), GURL("tel:0123456789"),
+      rvh->GetProcess()->GetID(), rvh->GetRoutingID(), phone_number,
       /*safe_to_bypass_ui=*/true, std::move(handlers), std::move(devices),
       /*selected_app_package=*/device_guid, apps::PickerEntryType::kDevice,
       apps::IntentPickerCloseReason::OPEN_APP, /*should_persist=*/false);
diff --git a/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
index 9c2d8b04..1928e8d 100644
--- a/chrome/browser/renderer_context_menu/render_view_context_menu.cc
+++ b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
@@ -1927,7 +1927,7 @@
   std::string selection_text;
   if (ShouldOfferClickToCallForURL(browser_context_, params_.link_url)) {
     entry_point = SharingClickToCallEntryPoint::kRightClickLink;
-    phone_number = GetUnescapedURLContent(params_.link_url);
+    phone_number = params_.link_url.GetContent();
   } else if (!params_.selection_text.empty()) {
     entry_point = SharingClickToCallEntryPoint::kRightClickSelection;
     selection_text = base::UTF16ToUTF8(params_.selection_text);
diff --git a/chrome/browser/send_tab_to_self/send_tab_to_self_util_unittest.cc b/chrome/browser/send_tab_to_self/send_tab_to_self_util_unittest.cc
index a3e9d36..c53623e 100644
--- a/chrome/browser/send_tab_to_self/send_tab_to_self_util_unittest.cc
+++ b/chrome/browser/send_tab_to_self/send_tab_to_self_util_unittest.cc
@@ -110,7 +110,7 @@
 }
 
 TEST_F(SendTabToSelfUtilTest, ShouldNotOfferFeatureForTelephoneLink) {
-  url_ = GURL("tel:07387252578");
+  url_ = GURL("tel:07399999999");
 
   AddTab(browser(), url_);
   SendTabToSelfSyncServiceFactory::GetInstance()->SetTestingFactory(
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.cc b/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.cc
index 74a2a38..1949120 100644
--- a/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.cc
+++ b/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.cc
@@ -5,11 +5,16 @@
 #include "chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.h"
 
 #include "base/android/jni_string.h"
+#include "base/callback_helpers.h"
 #include "base/check.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
 #include "base/trace_event/trace_event.h"
 #include "chrome/android/chrome_jni_headers/ClickToCallMessageHandler_jni.h"
+#include "chrome/browser/sharing/click_to_call/click_to_call_utils.h"
 #include "chrome/browser/sharing/proto/click_to_call_message.pb.h"
 #include "chrome/browser/sharing/proto/sharing_message.pb.h"
+#include "url/gurl.h"
 
 ClickToCallMessageHandler::ClickToCallMessageHandler() = default;
 
@@ -20,12 +25,32 @@
     SharingMessageHandler::DoneCallback done_callback) {
   DCHECK(message.has_click_to_call_message());
   TRACE_EVENT0("sharing", "ClickToCallMessageHandler::OnMessage");
+  base::ScopedClosureRunner runner(
+      base::BindOnce(std::move(done_callback), /*response=*/nullptr));
 
   std::string phone_number = message.click_to_call_message().phone_number();
-  JNIEnv* env = base::android::AttachCurrentThread();
+  GURL phone_url(base::StrCat({"tel:", phone_number}));
+  bool is_valid_phone_number = IsUrlSafeForClickToCall(phone_url) &&
+                               phone_url.GetContent() == phone_number;
 
+  base::UmaHistogramBoolean("Sharing.ClickToCallPhoneNumberValid",
+                            is_valid_phone_number);
+
+  // This can happen if a user on an older version of Chrome on their desktop
+  // clicks on a tel link that contains url-escaped unsafe characters like #.
+  // Another reason might be if the remote sender is using a custom or
+  // compromised version of Chrome. In either case, ignoring the message is the
+  // safest option as we don't want to pass along this number to the Android
+  // Dialer.
+  if (!is_valid_phone_number)
+    return;
+
+  HandlePhoneNumber(phone_number);
+}
+
+void ClickToCallMessageHandler::HandlePhoneNumber(
+    const std::string& phone_number) {
+  JNIEnv* env = base::android::AttachCurrentThread();
   Java_ClickToCallMessageHandler_handleMessage(
       env, base::android::ConvertUTF8ToJavaString(env, phone_number));
-
-  std::move(done_callback).Run(/*response=*/nullptr);
 }
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.h b/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.h
index 6b9f0af..577f258 100644
--- a/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.h
+++ b/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.h
@@ -18,6 +18,10 @@
   void OnMessage(chrome_browser_sharing::SharingMessage message,
                  SharingMessageHandler::DoneCallback done_callback) override;
 
+ protected:
+  // Calls into Java to handle a |phone_number|. Virtual for testing.
+  virtual void HandlePhoneNumber(const std::string& phone_number);
+
  private:
   DISALLOW_COPY_AND_ASSIGN(ClickToCallMessageHandler);
 };
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android_unittest.cc b/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android_unittest.cc
new file mode 100644
index 0000000..fc4e62a
--- /dev/null
+++ b/chrome/browser/sharing/click_to_call/click_to_call_message_handler_android_unittest.cc
@@ -0,0 +1,63 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/sharing/click_to_call/click_to_call_message_handler_android.h"
+
+#include <memory>
+#include <string>
+
+#include "base/callback_helpers.h"
+#include "base/optional.h"
+#include "chrome/browser/sharing/proto/click_to_call_message.pb.h"
+#include "chrome/browser/sharing/proto/sharing_message.pb.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+// Test implementation of ClickToCallMessageHandler that does not call out to
+// Java via JNI but records the last phone number it would have sent over.
+class TestClickToCallMessageHandler : public ClickToCallMessageHandler {
+ public:
+  TestClickToCallMessageHandler() = default;
+  ~TestClickToCallMessageHandler() override = default;
+
+  base::Optional<std::string> last_phone_number() { return last_phone_number_; }
+
+ protected:
+  void HandlePhoneNumber(const std::string& phone_number) override {
+    last_phone_number_ = phone_number;
+  }
+
+ private:
+  base::Optional<std::string> last_phone_number_;
+};
+
+}  // namespace
+
+TEST(ClickToCallMessageHandlerTest, HandlesValidPhoneNumber) {
+  TestClickToCallMessageHandler handler;
+  chrome_browser_sharing::SharingMessage message;
+
+  message.mutable_click_to_call_message()->set_phone_number("12345678");
+  handler.OnMessage(std::move(message), base::DoNothing());
+  EXPECT_EQ("12345678", handler.last_phone_number());
+}
+
+TEST(ClickToCallMessageHandlerTest, IgnoresInvalidPhoneNumbers) {
+  TestClickToCallMessageHandler handler;
+  chrome_browser_sharing::SharingMessage message;
+
+  message.mutable_click_to_call_message()->set_phone_number("*#06#");
+  handler.OnMessage(std::move(message), base::DoNothing());
+  EXPECT_FALSE(handler.last_phone_number().has_value());
+
+  message.mutable_click_to_call_message()->set_phone_number("%2A%2306%23");
+  handler.OnMessage(std::move(message), base::DoNothing());
+  EXPECT_FALSE(handler.last_phone_number().has_value());
+
+  message.mutable_click_to_call_message()->set_phone_number(
+      "%252A%252306%2523");
+  handler.OnMessage(std::move(message), base::DoNothing());
+  EXPECT_FALSE(handler.last_phone_number().has_value());
+}
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_ui_controller.cc b/chrome/browser/sharing/click_to_call/click_to_call_ui_controller.cc
index 9e7b653..b8b9469 100644
--- a/chrome/browser/sharing/click_to_call/click_to_call_ui_controller.cc
+++ b/chrome/browser/sharing/click_to_call/click_to_call_ui_controller.cc
@@ -119,7 +119,7 @@
   if (ukm_recorder_)
     std::move(ukm_recorder_).Run(SharingClickToCallSelection::kDevice);
 
-  SendNumberToDevice(device, GetUnescapedURLContent(phone_url_),
+  SendNumberToDevice(device, phone_url_.GetContent(),
                      SharingClickToCallEntryPoint::kLeftClickLink);
 }
 
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_ui_controller_unittest.cc b/chrome/browser/sharing/click_to_call/click_to_call_ui_controller_unittest.cc
index 84dc0e8..e881aba 100644
--- a/chrome/browser/sharing/click_to_call/click_to_call_ui_controller_unittest.cc
+++ b/chrome/browser/sharing/click_to_call/click_to_call_ui_controller_unittest.cc
@@ -33,8 +33,7 @@
 
 namespace {
 
-const char kPhoneNumber[] = "073%2087%202525%2078";
-const char kExpectedPhoneNumber[] = "073 87 2525 78";
+const char kPhoneNumber[] = "073%2099%209999%2099";
 const char kReceiverGuid[] = "test_receiver_guid";
 const char kReceiverName[] = "test_receiver_name";
 
@@ -85,7 +84,7 @@
 
   chrome_browser_sharing::SharingMessage sharing_message;
   sharing_message.mutable_click_to_call_message()->set_phone_number(
-      kExpectedPhoneNumber);
+      kPhoneNumber);
   EXPECT_CALL(
       *service(),
       SendMessageToDevice(
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_utils.cc b/chrome/browser/sharing/click_to_call/click_to_call_utils.cc
index f9709ab..a52c792 100644
--- a/chrome/browser/sharing/click_to_call/click_to_call_utils.cc
+++ b/chrome/browser/sharing/click_to_call/click_to_call_utils.cc
@@ -10,6 +10,7 @@
 #include "base/optional.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/sharing/click_to_call/phone_number_regex.h"
 #include "chrome/browser/sharing/sharing_service.h"
@@ -34,6 +35,10 @@
 constexpr int kSelectionTextMaxDigits = 15;
 
 bool IsClickToCallEnabled(content::BrowserContext* browser_context) {
+#if defined(OS_ANDROID)
+  // We don't support sending phone numbers from Android.
+  return false;
+#else   // defined(OS_ANDROID)
   // Check Chrome enterprise policy for Click to Call.
   Profile* profile = Profile::FromBrowserContext(browser_context);
   if (profile && !profile->GetPrefs()->GetBoolean(prefs::kClickToCallEnabled))
@@ -42,6 +47,32 @@
   SharingService* sharing_service =
       SharingServiceFactory::GetForBrowserContext(browser_context);
   return sharing_service != nullptr;
+#endif  // defined(OS_ANDROID)
+}
+
+// Returns the first possible phone number in |selection_text| given the
+// |regex_variant| to be used or base::nullopt if the regex did not match.
+base::Optional<std::string> ExtractPhoneNumber(
+    const std::string& selection_text) {
+  std::string parsed_number;
+
+  const re2::RE2& regex = GetPhoneNumberRegex();
+  if (!re2::RE2::PartialMatch(selection_text, regex, &parsed_number))
+    return base::nullopt;
+
+  return base::UTF16ToUTF8(
+      base::TrimWhitespace(base::UTF8ToUTF16(parsed_number), base::TRIM_ALL));
+}
+
+// Unescapes and returns the URL contents.
+std::string GetUnescapedURLContent(const GURL& url) {
+  std::string content_string(url.GetContent());
+  url::RawCanonOutputT<char16_t> unescaped_content;
+  url::DecodeURLEscapeSequences(content_string.data(), content_string.size(),
+                                url::DecodeURLMode::kUTF8OrIsomorphic,
+                                &unescaped_content);
+  return base::UTF16ToUTF8(
+      std::u16string(unescaped_content.data(), unescaped_content.length()));
 }
 
 }  // namespace
@@ -49,7 +80,7 @@
 bool ShouldOfferClickToCallForURL(content::BrowserContext* browser_context,
                                   const GURL& url) {
   return !url.is_empty() && url.SchemeIs(url::kTelScheme) &&
-         !url.GetContent().empty() && IsClickToCallEnabled(browser_context);
+         IsUrlSafeForClickToCall(url) && IsClickToCallEnabled(browser_context);
 }
 
 base::Optional<std::string> ExtractPhoneNumberForClickToCall(
@@ -71,24 +102,13 @@
   return ExtractPhoneNumber(selection_text);
 }
 
-base::Optional<std::string> ExtractPhoneNumber(
-    const std::string& selection_text) {
-  std::string parsed_number;
-
-  const re2::RE2& regex = GetPhoneNumberRegex();
-  if (!re2::RE2::PartialMatch(selection_text, regex, &parsed_number))
-    return base::nullopt;
-
-  return base::UTF16ToUTF8(
-      base::TrimWhitespace(base::UTF8ToUTF16(parsed_number), base::TRIM_ALL));
-}
-
-std::string GetUnescapedURLContent(const GURL& url) {
-  std::string content_string(url.GetContent());
-  url::RawCanonOutputT<char16_t> unescaped_content;
-  url::DecodeURLEscapeSequences(content_string.data(), content_string.size(),
-                                url::DecodeURLMode::kUTF8OrIsomorphic,
-                                &unescaped_content);
-  return base::UTF16ToUTF8(
-      std::u16string(unescaped_content.data(), unescaped_content.length()));
+bool IsUrlSafeForClickToCall(const GURL& url) {
+  // Get the unescaped content as this is what we'll end up sending to the
+  // Android dialer.
+  std::string unescaped = GetUnescapedURLContent(url);
+  // We don't allow any number that contains any of these characters as they
+  // might be used to create USSD codes.
+  return !unescaped.empty() &&
+         std::none_of(unescaped.begin(), unescaped.end(),
+                      [](char c) { return c == '#' || c == '*' || c == '%'; });
 }
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_utils.h b/chrome/browser/sharing/click_to_call/click_to_call_utils.h
index 194b00f..2edaadf 100644
--- a/chrome/browser/sharing/click_to_call/click_to_call_utils.h
+++ b/chrome/browser/sharing/click_to_call/click_to_call_utils.h
@@ -24,12 +24,9 @@
     content::BrowserContext* browser_context,
     const std::string& selection_text);
 
-// Returns the first possible phone number in |selection_text| given the
-// |regex_variant| to be used or base::nullopt if the regex did not match.
-base::Optional<std::string> ExtractPhoneNumber(
-    const std::string& selection_text);
-
-// Unescapes and returns the URL contents.
-std::string GetUnescapedURLContent(const GURL& url);
+// Checks if the given |url| is safe to be used by Click to Call to be sent to
+// remote Android devices. Note that the remote device might open the dialer
+// immediately with the given |url| so any USSD codes should return false here.
+bool IsUrlSafeForClickToCall(const GURL& url);
 
 #endif  // CHROME_BROWSER_SHARING_CLICK_TO_CALL_CLICK_TO_CALL_UTILS_H_
diff --git a/chrome/browser/sharing/click_to_call/click_to_call_utils_unittest.cc b/chrome/browser/sharing/click_to_call/click_to_call_utils_unittest.cc
index c5c7e17..41dba07e 100644
--- a/chrome/browser/sharing/click_to_call/click_to_call_utils_unittest.cc
+++ b/chrome/browser/sharing/click_to_call/click_to_call_utils_unittest.cc
@@ -112,6 +112,26 @@
   EXPECT_FALSE(ShouldOfferClickToCallForURL(&profile_, GURL(kNonTelUrl)));
 }
 
+TEST_F(ClickToCallUtilsTest, TelLinkWithFragment) {
+  GURL fragment("tel:123#456");
+  EXPECT_TRUE(ShouldOfferClickToCallForURL(&profile_, fragment));
+  EXPECT_EQ("123", fragment.GetContent());
+}
+
+TEST_F(ClickToCallUtilsTest, TelLinkWithEncodedCharacters) {
+  // %23 == '#'
+  EXPECT_FALSE(ShouldOfferClickToCallForURL(&profile_, GURL("tel:123%23456")));
+  // %2A == '*'
+  EXPECT_FALSE(ShouldOfferClickToCallForURL(&profile_, GURL("tel:123%2A456")));
+  EXPECT_FALSE(ShouldOfferClickToCallForURL(&profile_, GURL("tel:123*456")));
+  // %25 == '%'
+  EXPECT_FALSE(ShouldOfferClickToCallForURL(&profile_, GURL("tel:123%25456")));
+
+  // %2B == '+'
+  EXPECT_TRUE(ShouldOfferClickToCallForURL(&profile_, GURL("tel:%2B44123")));
+  EXPECT_TRUE(ShouldOfferClickToCallForURL(&profile_, GURL("tel:+44123")));
+}
+
 TEST_F(ClickToCallUtilsTest,
        SelectionText_ValidPhoneNumberRegex_OfferForSelection) {
   // Stores a mapping of selected text to expected phone number parsed.
@@ -187,3 +207,13 @@
   EXPECT_EQ(base::nullopt,
             ExtractPhoneNumberForClickToCall(&profile_, "+1234567890123456"));
 }
+
+TEST_F(ClickToCallUtilsTest, IsUrlSafeForClickToCall) {
+  EXPECT_FALSE(IsUrlSafeForClickToCall(GURL("tel:123%23456")));
+  EXPECT_FALSE(IsUrlSafeForClickToCall(GURL("tel:123%2A456")));
+  EXPECT_FALSE(IsUrlSafeForClickToCall(GURL("tel:123*456")));
+  EXPECT_FALSE(IsUrlSafeForClickToCall(GURL("tel:123%25456")));
+
+  EXPECT_TRUE(IsUrlSafeForClickToCall(GURL("tel:%2B44123")));
+  EXPECT_TRUE(IsUrlSafeForClickToCall(GURL("tel:+44123")));
+}
diff --git a/chrome/browser/ui/views/sharing/click_to_call_browsertest.cc b/chrome/browser/ui/views/sharing/click_to_call_browsertest.cc
index 5e6cbaf..5c40bd5 100644
--- a/chrome/browser/ui/views/sharing/click_to_call_browsertest.cc
+++ b/chrome/browser/ui/views/sharing/click_to_call_browsertest.cc
@@ -109,7 +109,7 @@
   menu->ExecuteCommand(IDC_CONTENT_CONTEXT_SHARING_CLICK_TO_CALL_SINGLE_DEVICE,
                        0);
   CheckLastReceiver(*devices[0]);
-  CheckLastSharingMessageSent(GetUnescapedURLContent(GURL(kTelUrl)));
+  CheckLastSharingMessageSent(GURL(kTelUrl).GetContent());
 }
 
 IN_PROC_BROWSER_TEST_F(ClickToCallBrowserTest, ContextMenu_NoDevicesAvailable) {
@@ -127,6 +127,42 @@
       IDC_CONTENT_CONTEXT_SHARING_CLICK_TO_CALL_MULTIPLE_DEVICES));
 }
 
+IN_PROC_BROWSER_TEST_F(ClickToCallBrowserTest, ContextMenu_UnsafeTelLink) {
+  Init(sync_pb::SharingSpecificFields::CLICK_TO_CALL_V2,
+       sync_pb::SharingSpecificFields::UNKNOWN);
+  auto devices = sharing_service()->GetDeviceCandidates(
+      sync_pb::SharingSpecificFields::CLICK_TO_CALL_V2);
+  ASSERT_EQ(1u, devices.size());
+
+  std::unique_ptr<TestRenderViewContextMenu> menu = InitContextMenu(
+      GURL("tel:%23*999%23"), kLinkText, kTextWithoutPhoneNumber);
+  EXPECT_FALSE(menu->IsItemPresent(
+      IDC_CONTENT_CONTEXT_SHARING_CLICK_TO_CALL_SINGLE_DEVICE));
+  EXPECT_FALSE(menu->IsItemPresent(
+      IDC_CONTENT_CONTEXT_SHARING_CLICK_TO_CALL_MULTIPLE_DEVICES));
+}
+
+IN_PROC_BROWSER_TEST_F(ClickToCallBrowserTest, ContextMenu_EscapedCharacters) {
+  Init(sync_pb::SharingSpecificFields::CLICK_TO_CALL_V2,
+       sync_pb::SharingSpecificFields::UNKNOWN);
+  auto devices = sharing_service()->GetDeviceCandidates(
+      sync_pb::SharingSpecificFields::CLICK_TO_CALL_V2);
+  ASSERT_EQ(1u, devices.size());
+
+  GURL phone_number("tel:%2B44%20123");
+  std::unique_ptr<TestRenderViewContextMenu> menu =
+      InitContextMenu(phone_number, kLinkText, kTextWithoutPhoneNumber);
+  ASSERT_TRUE(menu->IsItemPresent(
+      IDC_CONTENT_CONTEXT_SHARING_CLICK_TO_CALL_SINGLE_DEVICE));
+  EXPECT_FALSE(menu->IsItemPresent(
+      IDC_CONTENT_CONTEXT_SHARING_CLICK_TO_CALL_MULTIPLE_DEVICES));
+
+  menu->ExecuteCommand(IDC_CONTENT_CONTEXT_SHARING_CLICK_TO_CALL_SINGLE_DEVICE,
+                       0);
+  CheckLastReceiver(*devices[0]);
+  CheckLastSharingMessageSent(phone_number.GetContent());
+}
+
 IN_PROC_BROWSER_TEST_F(ClickToCallBrowserTest,
                        ContextMenu_DevicesAvailable_SyncTurnedOff) {
   if (base::FeatureList::IsEnabled(kSharingSendViaSync)) {
@@ -181,7 +217,7 @@
     sub_menu_model->ActivatedAt(device_id);
 
     CheckLastReceiver(*device);
-    CheckLastSharingMessageSent(GetUnescapedURLContent(GURL(kTelUrl)));
+    CheckLastSharingMessageSent(GURL(kTelUrl).GetContent());
     device_id++;
   }
 }
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 9011ef81..33a028d 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4451,6 +4451,7 @@
       "../browser/password_manager/android/update_password_infobar_delegate_android_unittest.cc",
       "../browser/permissions/permission_prompt_android_unittest.cc",
       "../browser/profiles/android/profile_resolver_unittest.cc",
+      "../browser/sharing/click_to_call/click_to_call_message_handler_android_unittest.cc",
       "../browser/sharing/sms/sms_fetch_request_handler_unittest.cc",
       "../browser/touch_to_fill/touch_to_fill_controller_unittest.cc",
       "../browser/translate/translate_manager_render_view_host_android_unittest.cc",
diff --git a/tools/metrics/histograms/histograms_xml/sharing/histograms.xml b/tools/metrics/histograms/histograms_xml/sharing/histograms.xml
index 0491b0a..62a740a 100644
--- a/tools/metrics/histograms/histograms_xml/sharing/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/sharing/histograms.xml
@@ -79,6 +79,17 @@
   </summary>
 </histogram>
 
+<histogram name="Sharing.ClickToCallPhoneNumberValid" units="BooleanValid"
+    expires_after="M95">
+  <owner>knollr@chromium.org</owner>
+  <owner>peter@chromium.org</owner>
+  <summary>
+    Records if a received phone number is valid. Invalid numbers might suggest
+    that the remote device tried to send malicious data. Logged when handling a
+    Click to Call message on Android received from a Chrome desktop instance.
+  </summary>
+</histogram>
+
 <histogram name="Sharing.ClickToCallSelectedAppIndex" units="index"
     expires_after="2021-08-22">
 <!-- Name completed by histogram_suffixes name="SharingClickToCallUi" -->