Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Libraries/Microsoft.Teams.Apps/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public partial class App
internal IServiceProvider? Provider { get; set; }
internal IContainer Container { get; set; }

private readonly IEnumerable<string>? _additionalAllowedDomains;
internal readonly IReadOnlyList<string>? _additionalAllowedDomains;
private readonly CloudEnvironment _cloud;
internal string UserAgent
{
Expand All @@ -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)
{
Expand Down
7 changes: 5 additions & 2 deletions Libraries/Microsoft.Teams.Apps/AppOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ public class AppOptions
public CloudEnvironment? Cloud { get; set; }

/// <summary>
/// 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 <c>"*.example.com"</c>, URL suffixes, or full URLs are NOT supported.
Comment thread
corinagum marked this conversation as resolved.
/// Pass <c>["*"]</c> as the sole wildcard to accept any hostname (disables service-URL validation).
Comment thread
corinagum marked this conversation as resolved.
/// </summary>
/// <example>new[] { "api.my-custom-channel.com" }</example>
public IEnumerable<string>? AdditionalAllowedDomains { get; set; }
Comment thread
corinagum marked this conversation as resolved.

public AppOptions()
Expand Down
55 changes: 55 additions & 0 deletions Tests/Microsoft.Teams.Apps.Tests/AppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,59 @@ await Assert.ThrowsAsync<ArgumentException>(() =>
app.Reply("19:abc@thread.skype", "not-a-number", new MessageActivity("Hello")));
}

[Fact]
public void Test_App_AdditionalAllowedDomains_DefensivelyCopied()
{
// Guards against a future regression: if App stored the caller's IEnumerable
// by reference, post-construction mutation (or lazy re-enumeration) could
// change validator behavior at runtime.
var mutableList = new List<string> { "first.example.com" };
var options = new AppOptions { AdditionalAllowedDomains = mutableList };
var app = new App(options);

mutableList.Add("mutation.example.com");

Assert.NotNull(app._additionalAllowedDomains);
Assert.Single(app._additionalAllowedDomains);
Assert.Equal("first.example.com", app._additionalAllowedDomains[0]);
Comment thread
corinagum marked this conversation as resolved.
Outdated
}

[Fact]
public void Test_App_AdditionalAllowedDomains_NullStaysNull()
{
var options = new AppOptions { AdditionalAllowedDomains = null };
var app = new App(options);
Assert.Null(app._additionalAllowedDomains);
}

[Fact]
public void Test_App_AdditionalAllowedDomains_EmptyListPreserved()
{
var options = new AppOptions { AdditionalAllowedDomains = Array.Empty<string>() };
var app = new App(options);

Assert.NotNull(app._additionalAllowedDomains);
Assert.Empty(app._additionalAllowedDomains);
}

[Fact]
public void Test_App_AdditionalAllowedDomains_MaterializesLazyEnumerable()
{
// Lazy IEnumerable is materialized at construction so late evaluation
// cannot alter validator behavior.
var callCount = 0;
IEnumerable<string> Lazy()
{
callCount++;
yield return "api.custom-channel.com";
}

var options = new AppOptions { AdditionalAllowedDomains = Lazy() };
var app = new App(options);

Assert.Equal(1, callCount);
Assert.NotNull(app._additionalAllowedDomains);
Assert.Single(app._additionalAllowedDomains);
}

}
Loading