Skip to content

Commit dc3d37f

Browse files
authored
Support for X-Forwarded-Prefix in ForwardedHeadersMiddleware (#49249)
1 parent a681e53 commit dc3d37f

File tree

6 files changed

+327
-8
lines changed

6 files changed

+327
-8
lines changed

src/Middleware/HttpOverrides/src/ForwardedHeaders.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ public enum ForwardedHeaders
2626
/// </summary>
2727
XForwardedProto = 1 << 2,
2828
/// <summary>
29-
/// Process X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto.
29+
/// Process X-Forwarded-Prefix, which identifies the original path base used by the client.
3030
/// </summary>
31-
All = XForwardedFor | XForwardedHost | XForwardedProto
31+
XForwardedPrefix = 1 << 3,
32+
/// <summary>
33+
/// Process X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto and X-Forwarded-Prefix.
34+
/// </summary>
35+
All = XForwardedFor | XForwardedHost | XForwardedProto | XForwardedPrefix
3236
}

src/Middleware/HttpOverrides/src/ForwardedHeadersDefaults.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public static class ForwardedHeadersDefaults
2424
/// </summary>
2525
public static string XForwardedProtoHeaderName { get; } = "X-Forwarded-Proto";
2626

27+
/// <summary>
28+
/// X-Forwarded-Prefix
29+
/// </summary>
30+
public static string XForwardedPrefixHeaderName { get; } = "X-Forwarded-Prefix";
31+
2732
/// <summary>
2833
/// X-Original-For
2934
/// </summary>
@@ -38,4 +43,9 @@ public static class ForwardedHeadersDefaults
3843
/// X-Original-Proto
3944
/// </summary>
4045
public static string XOriginalProtoHeaderName { get; } = "X-Original-Proto";
46+
47+
/// <summary>
48+
/// X-Original-Prefix
49+
/// </summary>
50+
public static string XOriginalPrefixHeaderName { get; } = "X-Original-Prefix";
4151
}

src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ public ForwardedHeadersMiddleware(RequestDelegate next, ILoggerFactory loggerFac
5353
EnsureOptionNotNullorWhitespace(options.Value.ForwardedForHeaderName, nameof(options.Value.ForwardedForHeaderName));
5454
EnsureOptionNotNullorWhitespace(options.Value.ForwardedHostHeaderName, nameof(options.Value.ForwardedHostHeaderName));
5555
EnsureOptionNotNullorWhitespace(options.Value.ForwardedProtoHeaderName, nameof(options.Value.ForwardedProtoHeaderName));
56+
EnsureOptionNotNullorWhitespace(options.Value.ForwardedPrefixHeaderName, nameof(options.Value.ForwardedPrefixHeaderName));
5657
EnsureOptionNotNullorWhitespace(options.Value.OriginalForHeaderName, nameof(options.Value.OriginalForHeaderName));
5758
EnsureOptionNotNullorWhitespace(options.Value.OriginalHostHeaderName, nameof(options.Value.OriginalHostHeaderName));
5859
EnsureOptionNotNullorWhitespace(options.Value.OriginalProtoHeaderName, nameof(options.Value.OriginalProtoHeaderName));
60+
EnsureOptionNotNullorWhitespace(options.Value.OriginalPrefixHeaderName, nameof(options.Value.OriginalPrefixHeaderName));
5961

6062
_options = options.Value;
6163
_logger = loggerFactory.CreateLogger<ForwardedHeadersMiddleware>();
@@ -126,8 +128,8 @@ public Task Invoke(HttpContext context)
126128
public void ApplyForwarders(HttpContext context)
127129
{
128130
// Gather expected headers.
129-
string[]? forwardedFor = null, forwardedProto = null, forwardedHost = null;
130-
bool checkFor = false, checkProto = false, checkHost = false;
131+
string[]? forwardedFor = null, forwardedProto = null, forwardedHost = null, forwardedPrefix = null;
132+
bool checkFor = false, checkProto = false, checkHost = false, checkPrefix = false;
131133
int entryCount = 0;
132134

133135
var request = context.Request;
@@ -165,6 +167,21 @@ public void ApplyForwarders(HttpContext context)
165167
entryCount = Math.Max(forwardedHost.Length, entryCount);
166168
}
167169

170+
if (_options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedPrefix))
171+
{
172+
checkPrefix = true;
173+
forwardedPrefix = requestHeaders.GetCommaSeparatedValues(_options.ForwardedPrefixHeaderName);
174+
if (_options.RequireHeaderSymmetry
175+
&& ((checkFor && forwardedFor!.Length != forwardedPrefix.Length)
176+
|| (checkProto && forwardedProto!.Length != forwardedPrefix.Length)
177+
|| (checkHost && forwardedHost!.Length != forwardedPrefix.Length)))
178+
{
179+
_logger.LogWarning(1, "Parameter count mismatch between X-Forwarded-Prefix and X-Forwarded-Host and X-Forwarded-For or X-Forwarded-Proto.");
180+
return;
181+
}
182+
entryCount = Math.Max(forwardedPrefix.Length, entryCount);
183+
}
184+
168185
// Apply ForwardLimit, if any
169186
if (_options.ForwardLimit.HasValue && entryCount > _options.ForwardLimit)
170187
{
@@ -189,6 +206,10 @@ public void ApplyForwarders(HttpContext context)
189206
{
190207
set.Host = forwardedHost[forwardedHost.Length - i - 1];
191208
}
209+
if (checkPrefix && i < forwardedPrefix!.Length)
210+
{
211+
set.Prefix = forwardedPrefix[forwardedPrefix.Length - i - 1];
212+
}
192213
sets[i] = set;
193214
}
194215

@@ -271,6 +292,20 @@ public void ApplyForwarders(HttpContext context)
271292
return;
272293
}
273294
}
295+
296+
if (checkPrefix)
297+
{
298+
if (!string.IsNullOrEmpty(set.Prefix) && set.Prefix[0] == '/')
299+
{
300+
applyChanges = true;
301+
currentValues.Prefix = set.Prefix;
302+
}
303+
else if (_options.RequireHeaderSymmetry)
304+
{
305+
_logger.LogWarning(5, $"Incorrect number of x-forwarded-prefix header values, see {nameof(_options.RequireHeaderSymmetry)}");
306+
return;
307+
}
308+
}
274309
}
275310

276311
if (applyChanges)
@@ -285,7 +320,8 @@ public void ApplyForwarders(HttpContext context)
285320
if (forwardedFor!.Length > entriesConsumed)
286321
{
287322
// Truncate the consumed header values
288-
requestHeaders[_options.ForwardedForHeaderName] = forwardedFor.Take(forwardedFor.Length - entriesConsumed).ToArray();
323+
requestHeaders[_options.ForwardedForHeaderName] =
324+
TruncateConsumedHeaderValues(forwardedFor, entriesConsumed);
289325
}
290326
else
291327
{
@@ -303,7 +339,8 @@ public void ApplyForwarders(HttpContext context)
303339
if (forwardedProto!.Length > entriesConsumed)
304340
{
305341
// Truncate the consumed header values
306-
requestHeaders[_options.ForwardedProtoHeaderName] = forwardedProto.Take(forwardedProto.Length - entriesConsumed).ToArray();
342+
requestHeaders[_options.ForwardedProtoHeaderName] =
343+
TruncateConsumedHeaderValues(forwardedProto, entriesConsumed);
307344
}
308345
else
309346
{
@@ -320,7 +357,8 @@ public void ApplyForwarders(HttpContext context)
320357
if (forwardedHost!.Length > entriesConsumed)
321358
{
322359
// Truncate the consumed header values
323-
requestHeaders[_options.ForwardedHostHeaderName] = forwardedHost.Take(forwardedHost.Length - entriesConsumed).ToArray();
360+
requestHeaders[_options.ForwardedHostHeaderName] =
361+
TruncateConsumedHeaderValues(forwardedHost, entriesConsumed);
324362
}
325363
else
326364
{
@@ -329,6 +367,29 @@ public void ApplyForwarders(HttpContext context)
329367
}
330368
request.Host = HostString.FromUriComponent(currentValues.Host);
331369
}
370+
371+
if (checkPrefix && currentValues.Prefix != null)
372+
{
373+
if (request.PathBase.HasValue)
374+
{
375+
// Save the original
376+
requestHeaders[_options.OriginalPrefixHeaderName] = request.PathBase.ToString();
377+
}
378+
379+
if (forwardedPrefix!.Length > entriesConsumed)
380+
{
381+
// Truncate the consumed header values
382+
requestHeaders[_options.ForwardedPrefixHeaderName] =
383+
TruncateConsumedHeaderValues(forwardedPrefix, entriesConsumed);
384+
}
385+
else
386+
{
387+
// All values were consumed
388+
requestHeaders.Remove(_options.ForwardedPrefixHeaderName);
389+
}
390+
391+
request.PathBase = PathString.FromUriComponent(currentValues.Prefix);
392+
}
332393
}
333394
}
334395

@@ -362,6 +423,7 @@ private struct SetOfForwarders
362423
public IPEndPoint? RemoteIpAndPort;
363424
public string Host;
364425
public string Scheme;
426+
public string Prefix;
365427
}
366428

367429
// Empty was checked for by the caller
@@ -423,4 +485,12 @@ private static bool TryValidateHostPort(string hostText, int offset)
423485

424486
return hostText.AsSpan(offset + 1).IndexOfAnyExceptInRange('0', '9') < 0;
425487
}
488+
489+
private static string[] TruncateConsumedHeaderValues(string[] forwarded, int entriesConsumed)
490+
{
491+
var newLength = forwarded.Length - entriesConsumed;
492+
var remaining = new string[newLength];
493+
Array.Copy(forwarded, remaining, newLength);
494+
return remaining;
495+
}
426496
}

src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ public class ForwardedHeadersOptions
2929
/// </summary>
3030
public string ForwardedProtoHeaderName { get; set; } = ForwardedHeadersDefaults.XForwardedProtoHeaderName;
3131

32+
/// <summary>
33+
/// Gets or sets the header used to retrieve the value for the path base.
34+
/// Defaults to the value specified by <see cref="ForwardedHeadersDefaults.XForwardedPrefixHeaderName"/>
35+
/// </summary>
36+
public string ForwardedPrefixHeaderName { get; set; } = ForwardedHeadersDefaults.XForwardedPrefixHeaderName;
37+
3238
/// <summary>
3339
/// Gets or sets the header used to store the original value of client IP before applying forwarded headers.
3440
/// Defaults to the value specified by <see cref="ForwardedHeadersDefaults.XOriginalForHeaderName"/>
@@ -50,6 +56,13 @@ public class ForwardedHeadersOptions
5056
/// <seealso cref="ForwardedHeadersDefaults"/>
5157
public string OriginalProtoHeaderName { get; set; } = ForwardedHeadersDefaults.XOriginalProtoHeaderName;
5258

59+
/// <summary>
60+
/// Gets or sets the header used to store the original path base before applying forwarded headers.
61+
/// Defaults to the value specified by <see cref="ForwardedHeadersDefaults.XOriginalPrefixHeaderName"/>
62+
/// </summary>
63+
/// <seealso cref="ForwardedHeadersDefaults"/>
64+
public string OriginalPrefixHeaderName { get; set; } = ForwardedHeadersDefaults.XOriginalPrefixHeaderName;
65+
5366
/// <summary>
5467
/// Identifies which forwarders should be processed.
5568
/// </summary>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
#nullable enable
2+
~static Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersDefaults.XForwardedPrefixHeaderName.get -> string
3+
~static Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersDefaults.XOriginalPrefixHeaderName.get -> string
4+
Microsoft.AspNetCore.Builder.ForwardedHeadersOptions.ForwardedPrefixHeaderName.get -> string!
5+
Microsoft.AspNetCore.Builder.ForwardedHeadersOptions.ForwardedPrefixHeaderName.set -> void
6+
Microsoft.AspNetCore.Builder.ForwardedHeadersOptions.OriginalPrefixHeaderName.get -> string!
7+
Microsoft.AspNetCore.Builder.ForwardedHeadersOptions.OriginalPrefixHeaderName.set -> void
8+
*REMOVED*Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedHost | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto -> Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders
9+
Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedHost | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedPrefix -> Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders
10+
Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedPrefix = 8 -> Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders
11+
static Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersDefaults.XForwardedPrefixHeaderName.get -> string!
12+
static Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersDefaults.XOriginalPrefixHeaderName.get -> string!
213
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.Parse(System.ReadOnlySpan<char> networkSpan) -> Microsoft.AspNetCore.HttpOverrides.IPNetwork!
314
static Microsoft.AspNetCore.HttpOverrides.IPNetwork.TryParse(System.ReadOnlySpan<char> networkSpan, out Microsoft.AspNetCore.HttpOverrides.IPNetwork? network) -> bool

0 commit comments

Comments
 (0)