diff --git a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs index a251373f..ef568159 100644 --- a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs +++ b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs @@ -45,11 +45,6 @@ public class CloudEnvironment /// public string GraphScope { get; } - /// - /// Allowed service URL hostnames for this cloud environment. - /// - public IReadOnlyList AllowedServiceUrls { get; } - public CloudEnvironment( string loginEndpoint, string loginTenant, @@ -57,8 +52,7 @@ public CloudEnvironment( string tokenServiceUrl, string openIdMetadataUrl, string tokenIssuer, - string graphScope, - string[]? allowedServiceUrls = null) + string graphScope) { LoginEndpoint = loginEndpoint.TrimEnd('/'); LoginTenant = loginTenant; @@ -67,7 +61,6 @@ public CloudEnvironment( OpenIdMetadataUrl = openIdMetadataUrl; TokenIssuer = tokenIssuer; GraphScope = graphScope; - AllowedServiceUrls = allowedServiceUrls is not null ? Array.AsReadOnly(allowedServiceUrls) : []; } /// @@ -80,8 +73,7 @@ public CloudEnvironment( tokenServiceUrl: "https://token.botframework.com", openIdMetadataUrl: "https://login.botframework.com/v1/.well-known/openidconfiguration", tokenIssuer: "https://api.botframework.com", - graphScope: "https://graph.microsoft.com/.default", - allowedServiceUrls: ["smba.trafficmanager.net", "smba.onyx.prod.teams.trafficmanager.net", "smba.infra.gcc.teams.microsoft.com"] + graphScope: "https://graph.microsoft.com/.default" ); /// @@ -94,8 +86,7 @@ public CloudEnvironment( tokenServiceUrl: "https://tokengcch.botframework.azure.us", openIdMetadataUrl: "https://login.botframework.azure.us/v1/.well-known/openidconfiguration", tokenIssuer: "https://api.botframework.us", - graphScope: "https://graph.microsoft.us/.default", - allowedServiceUrls: ["smba.infra.gov.teams.microsoft.us"] + graphScope: "https://graph.microsoft.us/.default" ); /// @@ -108,8 +99,7 @@ public CloudEnvironment( tokenServiceUrl: "https://apiDoD.botframework.azure.us", openIdMetadataUrl: "https://login.botframework.azure.us/v1/.well-known/openidconfiguration", tokenIssuer: "https://api.botframework.us", - graphScope: "https://dod-graph.microsoft.us/.default", - allowedServiceUrls: ["smba.infra.dod.teams.microsoft.us"] + graphScope: "https://dod-graph.microsoft.us/.default" ); /// @@ -122,8 +112,7 @@ public CloudEnvironment( tokenServiceUrl: "https://token.botframework.azure.cn", openIdMetadataUrl: "https://login.botframework.azure.cn/v1/.well-known/openidconfiguration", tokenIssuer: "https://api.botframework.azure.cn", - graphScope: "https://microsoftgraph.chinacloudapi.cn/.default", - allowedServiceUrls: ["frontend.botapi.msg.infra.teams.microsoftonline.cn"] + graphScope: "https://microsoftgraph.chinacloudapi.cn/.default" ); /// @@ -137,12 +126,11 @@ public CloudEnvironment WithOverrides( string? tokenServiceUrl = null, string? openIdMetadataUrl = null, string? tokenIssuer = null, - string? graphScope = null, - string[]? allowedServiceUrls = null) + string? graphScope = null) { if (loginEndpoint is null && loginTenant is null && botScope is null && tokenServiceUrl is null && openIdMetadataUrl is null && tokenIssuer is null && - graphScope is null && allowedServiceUrls is null) + graphScope is null) { return this; } @@ -154,8 +142,7 @@ tokenServiceUrl is null && openIdMetadataUrl is null && tokenIssuer is null && tokenServiceUrl ?? TokenServiceUrl, openIdMetadataUrl ?? OpenIdMetadataUrl, tokenIssuer ?? TokenIssuer, - graphScope ?? GraphScope, - allowedServiceUrls ?? [.. AllowedServiceUrls] + graphScope ?? GraphScope ); } diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index 4d20690a..4126dfe5 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -41,8 +41,6 @@ public partial class App internal IServiceProvider? Provider { get; set; } internal IContainer Container { get; set; } - private readonly IEnumerable? _additionalAllowedDomains; - private readonly CloudEnvironment _cloud; internal string UserAgent { get @@ -55,7 +53,6 @@ internal string UserAgent public App(AppOptions? options = null) { var cloud = options?.Cloud ?? CloudEnvironment.Public; - _cloud = cloud; Logger = options?.Logger ?? new ConsoleLogger(); Storage = options?.Storage ?? new LocalStorage(); @@ -63,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(); @@ -386,11 +377,6 @@ private async Task Process(ISenderPlugin sender, ActivityEvent @event, Logger.Debug(path); var serviceUrl = @event.Activity.ServiceUrl ?? @event.Token.ServiceUrl; - if (!ServiceUrlValidator.IsAllowed(serviceUrl, _cloud, _additionalAllowedDomains)) - { - Logger.Warn($"Rejected service URL: {serviceUrl}"); - throw new InvalidOperationException("Service URL is not from an allowed domain"); - } var reference = new ConversationReference() { diff --git a/Libraries/Microsoft.Teams.Apps/AppOptions.cs b/Libraries/Microsoft.Teams.Apps/AppOptions.cs index e887cf12..766016bc 100644 --- a/Libraries/Microsoft.Teams.Apps/AppOptions.cs +++ b/Libraries/Microsoft.Teams.Apps/AppOptions.cs @@ -18,12 +18,6 @@ public class AppOptions public OAuthSettings OAuth { get; set; } = new OAuthSettings(); public CloudEnvironment? Cloud { get; set; } - /// - /// Additional allowed service URL hostnames beyond the built-in defaults. - /// Use this if your bot receives activities from non-standard channels. - /// - public IEnumerable? AdditionalAllowedDomains { get; set; } - public AppOptions() { 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/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs index 6889601f..d82780b4 100644 --- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs +++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs @@ -11,7 +11,6 @@ public class TeamsSettings public string? ClientSecret { get; set; } public string? TenantId { get; set; } public string? Cloud { get; set; } - public string[]? AdditionalAllowedDomains { get; set; } /// Override the Azure AD login endpoint. public string? LoginEndpoint { get; set; } @@ -81,11 +80,6 @@ public AppOptions Apply(AppOptions? options = null) existingCredentials.Cloud = cloud; } - if (AdditionalAllowedDomains is { Length: > 0 }) - { - options.AdditionalAllowedDomains = AdditionalAllowedDomains; - } - return options; } } 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)); - } -}