@@ -2085,6 +2085,152 @@ 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+
2142+ [ Fact ]
2143+ public async Task ItShouldAllowRequestWhenAllowedBaseUrlContainsQueryOrFragmentAsync ( )
2144+ {
2145+ // Arrange - base URL misconfigured with query string; validation should ignore it
2146+ var operation = new RestApiOperation (
2147+ id : "test" ,
2148+ servers : [ new RestApiServer ( "https://api.example.com" ) ] ,
2149+ path : "/users" ,
2150+ method : HttpMethod . Get ,
2151+ description : "test operation" ,
2152+ parameters : [ ] ,
2153+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
2154+ securityRequirements : [ ]
2155+ ) ;
2156+
2157+ var validationOptions = new RestApiOperationServerUrlValidationOptions
2158+ {
2159+ AllowedBaseUrls = [ new Uri ( "https://api.example.com?x=1" ) ]
2160+ } ;
2161+
2162+ var sut = new RestApiOperationRunner ( this . _httpClient , this . _authenticationHandlerMock . Object , serverUrlValidationOptions : validationOptions ) ;
2163+
2164+ // Act & Assert - should not throw; query/fragment in base URL is stripped for comparison
2165+ await sut . RunAsync ( operation , [ ] ) ;
2166+ }
2167+
2168+ [ Fact ]
2169+ public async Task ItShouldBlockHostLevelPrefixCollisionAsync ( )
2170+ {
2171+ // Arrange - malicious host shares textual prefix with allowed host
2172+ var operation = new RestApiOperation (
2173+ id : "test" ,
2174+ servers : [ new RestApiServer ( "https://api.example.com.evil.com" ) ] ,
2175+ path : "/steal" ,
2176+ method : HttpMethod . Get ,
2177+ description : "test operation" ,
2178+ parameters : [ ] ,
2179+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
2180+ securityRequirements : [ ]
2181+ ) ;
2182+
2183+ var validationOptions = new RestApiOperationServerUrlValidationOptions
2184+ {
2185+ AllowedBaseUrls = [ new Uri ( "https://api.example.com" ) ]
2186+ } ;
2187+
2188+ var sut = new RestApiOperationRunner ( this . _httpClient , this . _authenticationHandlerMock . Object , serverUrlValidationOptions : validationOptions ) ;
2189+
2190+ // Act & Assert - should be blocked; api.example.com.evil.com is not api.example.com
2191+ var exception = await Assert . ThrowsAsync < InvalidOperationException > ( ( ) => sut . RunAsync ( operation , [ ] ) ) ;
2192+ Assert . Contains ( "not allowed" , exception . Message ) ;
2193+ }
2194+
2195+ [ Fact ]
2196+ public async Task ItShouldAllowRequestToBaseUrlPathWithQueryParametersAsync ( )
2197+ {
2198+ // Arrange - request to exact base URL path but with query parameters
2199+ var queryParameter = new RestApiParameter (
2200+ "user" ,
2201+ "string" ,
2202+ isRequired : true ,
2203+ false ,
2204+ RestApiParameterLocation . Query ,
2205+ RestApiParameterStyle . Form ) ;
2206+
2207+ var operation = new RestApiOperation (
2208+ id : "test" ,
2209+ servers : [ new RestApiServer ( "https://api.example.com/v1" ) ] ,
2210+ path : "/" ,
2211+ method : HttpMethod . Get ,
2212+ description : "test operation" ,
2213+ parameters : [ queryParameter ] ,
2214+ responses : new Dictionary < string , RestApiExpectedResponse > ( ) ,
2215+ securityRequirements : [ ]
2216+ ) ;
2217+
2218+ var arguments = new KernelArguments
2219+ {
2220+ { "user" , "1" } ,
2221+ } ;
2222+
2223+ var validationOptions = new RestApiOperationServerUrlValidationOptions
2224+ {
2225+ AllowedBaseUrls = [ new Uri ( "https://api.example.com/v1" ) ]
2226+ } ;
2227+
2228+ var sut = new RestApiOperationRunner ( this . _httpClient , this . _authenticationHandlerMock . Object , serverUrlValidationOptions : validationOptions ) ;
2229+
2230+ // Act & Assert - should not throw; the path portion matches the allowed base URL
2231+ await sut . RunAsync ( operation , arguments ) ;
2232+ }
2233+
20882234 /// <summary>
20892235 /// Disposes resources used by this class.
20902236 /// </summary>
0 commit comments