diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index 6810cc54..4126dfe5 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -60,12 +60,6 @@ public App(AppOptions? options = null) Plugins = options?.Plugins ?? []; OAuth = options?.OAuth ?? new OAuthSettings(); Provider = options?.Provider; - _additionalAllowedDomains = options?.AdditionalAllowedDomains; - - if (_additionalAllowedDomains?.Contains("*") == true) - { - Logger.Warn("Service URL validation is disabled via wildcard in AdditionalAllowedDomains"); - } TokenClient = new Common.Http.HttpClient(); Client = options?.Client ?? options?.ClientFactory?.CreateClient() ?? new Common.Http.HttpClient(); diff --git a/Libraries/Microsoft.Teams.Apps/ServiceUrlValidator.cs b/Libraries/Microsoft.Teams.Apps/ServiceUrlValidator.cs deleted file mode 100644 index a8b8828d..00000000 --- a/Libraries/Microsoft.Teams.Apps/ServiceUrlValidator.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Teams.Api.Auth; - -namespace Microsoft.Teams.Apps; - -/// -/// Validates service URLs against known allowed domains. -/// -public static class ServiceUrlValidator -{ - /// - /// Validates that a service URL hostname is allowed. - /// Checks against the cloud environment's allowed service URLs, - /// plus any additional domains provided by the caller. - /// Localhost is always allowed for local development. - /// - public static bool IsAllowed(string? serviceUrl, CloudEnvironment cloud, IEnumerable? additionalDomains = null) - { - if (string.IsNullOrEmpty(serviceUrl)) - return true; // No URL to validate - - if (!Uri.TryCreate(serviceUrl, UriKind.Absolute, out var uri)) - return false; - - var hostname = uri.Host.ToLowerInvariant(); - - if (hostname is "localhost" or "127.0.0.1") - return true; - - if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - return false; - - var allowed = cloud.AllowedServiceUrls.Concat(additionalDomains ?? []).Select(d => d.ToLowerInvariant()).ToList(); - if (allowed.Contains("*")) - return true; - - return allowed.Contains(hostname); - } -} diff --git a/Tests/Microsoft.Teams.Apps.Tests/ServiceUrlValidatorTests.cs b/Tests/Microsoft.Teams.Apps.Tests/ServiceUrlValidatorTests.cs deleted file mode 100644 index 01b28942..00000000 --- a/Tests/Microsoft.Teams.Apps.Tests/ServiceUrlValidatorTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Microsoft.Teams.Api.Auth; - -namespace Microsoft.Teams.Apps.Tests; - -public class ServiceUrlValidatorTests -{ - // --- Public cloud --- - - [Theory] - [InlineData("https://smba.trafficmanager.net/teams/")] - [InlineData("https://smba.trafficmanager.net/amer/")] - [InlineData("https://smba.onyx.prod.teams.trafficmanager.net")] - public void IsAllowed_AcceptsPublicCloudDomains(string serviceUrl) - { - Assert.True(ServiceUrlValidator.IsAllowed(serviceUrl, CloudEnvironment.Public)); - } - - // --- Government clouds --- - - [Fact] - public void IsAllowed_AcceptsUSGovDomain() - { - Assert.True(ServiceUrlValidator.IsAllowed("https://smba.infra.gov.teams.microsoft.us/gcch/", CloudEnvironment.USGov)); - } - - [Fact] - public void IsAllowed_AcceptsDoDDomain() - { - Assert.True(ServiceUrlValidator.IsAllowed("https://smba.infra.dod.teams.microsoft.us/", CloudEnvironment.USGovDoD)); - } - - [Fact] - public void IsAllowed_AcceptsChinaDomain() - { - Assert.True(ServiceUrlValidator.IsAllowed("https://frontend.botapi.msg.infra.teams.microsoftonline.cn", CloudEnvironment.China)); - } - - // --- Cross-cloud rejection --- - - [Fact] - public void IsAllowed_RejectsGovDomainWithPublicCloud() - { - Assert.False(ServiceUrlValidator.IsAllowed("https://smba.infra.gov.teams.microsoft.us/", CloudEnvironment.Public)); - } - - // --- Localhost --- - - [Theory] - [InlineData("http://localhost:3978")] - [InlineData("https://localhost:443")] - [InlineData("http://127.0.0.1:3978")] - public void IsAllowed_AcceptsLocalhost(string serviceUrl) - { - Assert.True(ServiceUrlValidator.IsAllowed(serviceUrl, CloudEnvironment.Public)); - } - - // --- Rejected domains --- - - [Theory] - [InlineData("https://evil.com")] - [InlineData("https://botframework.com.evil.com")] - [InlineData("https://attacker.net/api")] - [InlineData("https://attacker.trafficmanager.net")] - public void IsAllowed_RejectsUnknownDomains(string serviceUrl) - { - Assert.False(ServiceUrlValidator.IsAllowed(serviceUrl, CloudEnvironment.Public)); - } - - // --- Empty / null --- - - [Theory] - [InlineData("")] - [InlineData(null)] - public void IsAllowed_AcceptsEmptyOrNull(string? serviceUrl) - { - Assert.True(ServiceUrlValidator.IsAllowed(serviceUrl!, CloudEnvironment.Public)); - } - - // --- Invalid URLs --- - - [Fact] - public void IsAllowed_RejectsInvalidUrl() - { - Assert.False(ServiceUrlValidator.IsAllowed("not-a-url", CloudEnvironment.Public)); - } - - // --- Additional domains --- - - [Fact] - public void IsAllowed_AcceptsAdditionalDomains() - { - var additional = new[] { "api.custom-channel.com" }; - Assert.True(ServiceUrlValidator.IsAllowed("https://api.custom-channel.com", CloudEnvironment.Public, additional)); - } - - [Fact] - public void IsAllowed_RejectsWhenNotInAdditionalDomains() - { - var additional = new[] { "api.custom-channel.com" }; - Assert.False(ServiceUrlValidator.IsAllowed("https://evil.com", CloudEnvironment.Public, additional)); - } - - // --- Wildcard --- - - [Fact] - public void IsAllowed_AcceptsAnyDomainWithWildcard() - { - var additional = new[] { "*" }; - Assert.True(ServiceUrlValidator.IsAllowed("https://anything.example.com", CloudEnvironment.Public, additional)); - } - - // --- botframework.com not in default --- - - [Theory] - [InlineData("https://webchat.botframework.com")] - [InlineData("https://directline.botframework.com")] - public void IsAllowed_RejectsBotframeworkByDefault(string serviceUrl) - { - Assert.False(ServiceUrlValidator.IsAllowed(serviceUrl, CloudEnvironment.Public)); - } -}