diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index 4d20690a..12009245 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -41,7 +41,7 @@ public partial class App internal IServiceProvider? Provider { get; set; } internal IContainer Container { get; set; } - private readonly IEnumerable? _additionalAllowedDomains; + private readonly IReadOnlyList? _additionalAllowedDomains; private readonly CloudEnvironment _cloud; internal string UserAgent { @@ -63,7 +63,9 @@ public App(AppOptions? options = null) Plugins = options?.Plugins ?? []; OAuth = options?.OAuth ?? new OAuthSettings(); Provider = options?.Provider; - _additionalAllowedDomains = options?.AdditionalAllowedDomains; + // Defensive copy so a caller-provided list can't mutate validator behavior + // after construction (IEnumerable may be lazy or mutable). + _additionalAllowedDomains = options?.AdditionalAllowedDomains?.ToList(); if (_additionalAllowedDomains?.Contains("*") == true) { diff --git a/Libraries/Microsoft.Teams.Apps/AppOptions.cs b/Libraries/Microsoft.Teams.Apps/AppOptions.cs index e887cf12..3c0f909c 100644 --- a/Libraries/Microsoft.Teams.Apps/AppOptions.cs +++ b/Libraries/Microsoft.Teams.Apps/AppOptions.cs @@ -19,9 +19,12 @@ public class AppOptions 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. + /// Additional service URL hostnames accepted beyond the cloud preset. + /// Entries must be bare hostnames matched exactly (case-insensitive) + /// wildcard patterns like "*.example.com", URL suffixes, or full URLs are NOT supported. + /// Pass ["*"] as the sole wildcard to accept any hostname (disables service-URL validation). /// + /// new[] { "api.my-custom-channel.com" } public IEnumerable? AdditionalAllowedDomains { get; set; } public AppOptions() diff --git a/Tests/Microsoft.Teams.Apps.Tests/AppTests.cs b/Tests/Microsoft.Teams.Apps.Tests/AppTests.cs index 5ce7a061..24e5087f 100644 --- a/Tests/Microsoft.Teams.Apps.Tests/AppTests.cs +++ b/Tests/Microsoft.Teams.Apps.Tests/AppTests.cs @@ -367,4 +367,39 @@ await Assert.ThrowsAsync(() => app.Reply("19:abc@thread.skype", "not-a-number", new MessageActivity("Hello"))); } + [Fact] + public void Test_App_AdditionalAllowedDomains_NullConstructionSucceeds() + { + var options = new AppOptions { AdditionalAllowedDomains = null }; + var exception = Record.Exception(() => new App(options)); + Assert.Null(exception); + } + + [Fact] + public void Test_App_AdditionalAllowedDomains_EmptyConstructionSucceeds() + { + var options = new AppOptions { AdditionalAllowedDomains = Array.Empty() }; + var exception = Record.Exception(() => new App(options)); + Assert.Null(exception); + } + + [Fact] + public void Test_App_AdditionalAllowedDomains_MaterializesLazyEnumerable() + { + // A lazy IEnumerable must be materialized at construction (not held and + // re-enumerated later), otherwise mutation between iterations could change + // validator behavior. Asserts the caller's lazy is invoked exactly once. + var callCount = 0; + IEnumerable Lazy() + { + callCount++; + yield return "api.custom-channel.com"; + } + + var options = new AppOptions { AdditionalAllowedDomains = Lazy() }; + _ = new App(options); + + Assert.Equal(1, callCount); + } + } \ No newline at end of file