Skip to content

Commit 9627892

Browse files
Harden AllowedBaseUrls validation in RestApiOperationRunner
Strengthened base URL comparison in ValidateUrl to enforce proper path-boundary matching when checking request URLs against configured AllowedBaseUrls, and added unit tests for the stricter validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 84ab822 commit 9627892

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,12 @@ private void ValidateUrl(Uri url)
252252
foreach (var baseUrl in allowedBaseUrls)
253253
{
254254
var baseUrlString = baseUrl.AbsoluteUri;
255-
if (urlString.StartsWith(baseUrlString, StringComparison.OrdinalIgnoreCase))
255+
if (!baseUrlString.EndsWith("/", StringComparison.Ordinal))
256+
{
257+
baseUrlString += "/";
258+
}
259+
if (string.Equals(urlString, baseUrl.AbsoluteUri, StringComparison.OrdinalIgnoreCase) ||
260+
urlString.StartsWith(baseUrlString, StringComparison.OrdinalIgnoreCase))
256261
{
257262
baseUrlAllowed = true;
258263
break;

dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,6 +2085,60 @@ public async Task ItShouldAllowCustomSchemesWhenConfiguredAsync()
20852085
await sut.RunAsync(operation, []);
20862086
}
20872087

2088+
[Fact]
2089+
public async Task ItShouldBlockRequestWithPrefixCollisionOnAllowedBaseUrlAsync()
2090+
{
2091+
// Arrange - attacker URL shares prefix with allowed base URL but diverges at path boundary
2092+
var operation = new RestApiOperation(
2093+
id: "test",
2094+
servers: [new RestApiServer("https://api.example.com/v1-evil")],
2095+
path: "/steal-data",
2096+
method: HttpMethod.Get,
2097+
description: "test operation",
2098+
parameters: [],
2099+
responses: new Dictionary<string, RestApiExpectedResponse>(),
2100+
securityRequirements: []
2101+
);
2102+
2103+
var validationOptions = new RestApiOperationServerUrlValidationOptions
2104+
{
2105+
AllowedBaseUrls = [new Uri("https://api.example.com/v1")]
2106+
};
2107+
2108+
var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object, serverUrlValidationOptions: validationOptions);
2109+
2110+
// Act & Assert - should be blocked because /v1-evil is not under /v1/
2111+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => sut.RunAsync(operation, []));
2112+
Assert.Contains("not allowed", exception.Message);
2113+
Assert.Contains("does not match", exception.Message);
2114+
}
2115+
2116+
[Fact]
2117+
public async Task ItShouldAllowRequestUnderAllowedBaseUrlWithPathAsync()
2118+
{
2119+
// Arrange - legitimate sub-path under allowed base URL
2120+
var operation = new RestApiOperation(
2121+
id: "test",
2122+
servers: [new RestApiServer("https://api.example.com/v1")],
2123+
path: "/users",
2124+
method: HttpMethod.Get,
2125+
description: "test operation",
2126+
parameters: [],
2127+
responses: new Dictionary<string, RestApiExpectedResponse>(),
2128+
securityRequirements: []
2129+
);
2130+
2131+
var validationOptions = new RestApiOperationServerUrlValidationOptions
2132+
{
2133+
AllowedBaseUrls = [new Uri("https://api.example.com/v1")]
2134+
};
2135+
2136+
var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object, serverUrlValidationOptions: validationOptions);
2137+
2138+
// Act & Assert - should not throw; /v1/users is under /v1/
2139+
await sut.RunAsync(operation, []);
2140+
}
2141+
20882142
/// <summary>
20892143
/// Disposes resources used by this class.
20902144
/// </summary>

0 commit comments

Comments
 (0)