A rewrite rule that matches Request.Path and re-emits the same captured segment (even a no-op/identity rule) can change the path value, because the rule matches against Request.Path.ToString() (escaped form) and reassigns via PathString.FromUriComponent() (decodes again). A path already containing a literal %2e (from Kestrel's normal single decode) becomes .. after passing through the rule.
Repro
app.UseRewriter(new RewriteOptions()
.AddRewrite("^test/(.*)", "test/$1", skipRemainingRules: true));
app.Run(ctx => ctx.Response.WriteAsync(ctx.Request.Path));
Request: GET /test/%252e%252e/secret
- Before rewrite:
Request.Path = /test/%2e%2e/secret
- After rewrite:
Request.Path = /test/../secret
Expected: pass-through rewrite rule should not change Request.Path's value.
A rewrite rule that matches
Request.Pathand re-emits the same captured segment (even a no-op/identity rule) can change the path value, because the rule matches againstRequest.Path.ToString()(escaped form) and reassigns viaPathString.FromUriComponent()(decodes again). A path already containing a literal%2e(from Kestrel's normal single decode) becomes..after passing through the rule.Repro
Request:
GET /test/%252e%252e/secretRequest.Path=/test/%2e%2e/secretRequest.Path=/test/../secretExpected: pass-through rewrite rule should not change
Request.Path's value.