Skip to content

Commit 58d87c9

Browse files
int0x81raman-m
andauthored
#2116 Escaping unsafe pattern values of Regex constructor ​​derived from URL query parameter values containing special Regex chars (#2150)
* regex escape handling for url templates * refactored regex method to lamda version * Quick code review by @raman-m * added acceptance test for url regex bug * moved acceptance test to routing tests * Convert to theory: define 2 test cases --------- Co-authored-by: Raman Maksimchuk <dotnet044@gmail.com>
1 parent 8e66be7 commit 58d87c9

File tree

3 files changed

+58
-2
lines changed

3 files changed

+58
-2
lines changed

src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(Downst
124124
foreach (var nAndV in templatePlaceholderNameAndValues)
125125
{
126126
var name = nAndV.Name.Trim(OpeningBrace, ClosingBrace);
127-
128-
var rgx = new Regex($@"\b{name}={nAndV.Value}\b");
127+
var value = Regex.Escape(nAndV.Value); // to ensure a placeholder value containing special Regex characters from URL query parameters is safely used in a Regex constructor, it's necessary to escape the value
128+
var rgx = new Regex($@"\b{name}={value}\b");
129129

130130
if (rgx.IsMatch(downstreamRequest.Query))
131131
{

test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs

+19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Http;
22
using Ocelot.Configuration.File;
3+
using System.Web;
34

45
namespace Ocelot.AcceptanceTests.Routing
56
{
@@ -1167,6 +1168,24 @@ public void should_fix_issue_271()
11671168
.BDDfy();
11681169
}
11691170

1171+
[Theory]
1172+
[Trait("Bug", "2116")]
1173+
[InlineData("debug()")] // no query
1174+
[InlineData("debug%28%29")] // debug()
1175+
public void Should_change_downstream_path_by_upstream_path_when_path_contains_malicious_characters(string path)
1176+
{
1177+
var port = PortFinder.GetRandomPort();
1178+
var configuration = GivenDefaultConfiguration(port, "/api/{path}", "/routed/api/{path}");
1179+
var decodedDownstreamUrlPath = $"/routed/api/{HttpUtility.UrlDecode(path)}";
1180+
this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", decodedDownstreamUrlPath, HttpStatusCode.OK, string.Empty))
1181+
.And(x => _steps.GivenThereIsAConfiguration(configuration))
1182+
.And(x => _steps.GivenOcelotIsRunning())
1183+
.When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/{path}")) // should be encoded
1184+
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
1185+
.And(x => ThenTheDownstreamUrlPathShouldBe(decodedDownstreamUrlPath))
1186+
.BDDfy();
1187+
}
1188+
11701189
private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, HttpStatusCode statusCode, string responseBody)
11711190
{
11721191
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>

test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs

+37
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,43 @@ public void Should_map_when_query_parameters_has_same_names_with_placeholder()
611611
ThenTheQueryStringIs($"?roleId={roleid}&{everything}");
612612
}
613613

614+
[Theory]
615+
[Trait("Bug", "2116")]
616+
[InlineData("api/debug()")] // no query
617+
[InlineData("api/debug%28%29")] // debug()
618+
public void ShouldNotFailToHandleUrlWithSpecialRegexChars(string urlPath)
619+
{
620+
// Arrange
621+
var withGetMethod = new List<string> { "Get" };
622+
var downstreamRoute = new DownstreamRouteBuilder()
623+
.WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder()
624+
.WithOriginalValue("/routed/api/{path}")
625+
.Build())
626+
.WithDownstreamPathTemplate("/api/{path}")
627+
.WithUpstreamHttpMethod(withGetMethod)
628+
.WithDownstreamScheme(Uri.UriSchemeHttp)
629+
.Build();
630+
GivenTheDownStreamRouteIs(new DownstreamRouteHolder(
631+
new List<PlaceholderNameAndValue>
632+
{
633+
new("{path}", urlPath),
634+
},
635+
new RouteBuilder().WithDownstreamRoute(downstreamRoute)
636+
.WithUpstreamHttpMethod(withGetMethod)
637+
.Build()
638+
));
639+
GivenTheDownstreamRequestUriIs($"http://localhost:5000/{urlPath}");
640+
GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build());
641+
GivenTheUrlReplacerWillReturn($"routed/{urlPath}");
642+
643+
// Act
644+
WhenICallTheMiddleware();
645+
646+
// Assert
647+
ThenTheDownstreamRequestUriIs($"http://localhost:5000/routed/{urlPath}");
648+
Assert.Equal((int)HttpStatusCode.OK, _httpContext.Response.StatusCode);
649+
}
650+
614651
private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config)
615652
{
616653
var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null);

0 commit comments

Comments
 (0)