Skip to content

Commit

Permalink
Implement header allow lists #1114 (#1137)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tratcher authored Jul 14, 2021
1 parent 9c6a822 commit bfb6406
Show file tree
Hide file tree
Showing 13 changed files with 897 additions and 3 deletions.
99 changes: 99 additions & 0 deletions docs/docfx/articles/transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,39 @@ AnotherHeader: AnotherValue

This removes the named header.

### RequestHeadersAllowed

| Key | Value | Required |
|-----|-------|----------|
| RequestHeadersAllowed | A semicolon separated list of allowed header names. | yes |

Config:
```JSON
{
"RequestHeadersAllowed": "Header1;header2"
}
```
Code:
```csharp
routeConfig = routeConfig.WithTransformRequestHeadersAllowed("Header1", "header2");
```
```C#
transformBuilderContext.AddRequestHeadersAllowed("Header1", "header2");
```

YARP copies most request headers to the proxy request by default (see [RequestHeadersCopy](#RequestHeadersCopy)). Some security models only allow specific headers to be proxied. This transform disables RequestHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list.

Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities.

Example:
```
Header1: value1
Header2: value2
AnotherHeader: AnotherValue
```

Only header1 and header2 are copied to the proxy request.

### X-Forwarded

| Key | Value | Default | Required |
Expand Down Expand Up @@ -710,6 +743,39 @@ This removes the named header.

`When` specifies if the response header should be included for successful responses or for all responses. Any response with a status code less than 400 is considered a success.

### ResponseHeadersAllowed

| Key | Value | Required |
|-----|-------|----------|
| ResponseHeadersAllowed | A semicolon separated list of allowed header names. | yes |

Config:
```JSON
{
"ResponseHeadersAllowed": "Header1;header2"
}
```
Code:
```csharp
routeConfig = routeConfig.WithTransformResponseHeadersAllowed("Header1", "header2");
```
```C#
transformBuilderContext.AddResponseHeadersAllowed("Header1", "header2");
```

YARP copies most response headers from the proxy response by default (see [ResponseHeadersCopy](#ResponseHeadersCopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list.

Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities.

Example:
```
Header1: value1
Header2: value2
AnotherHeader: AnotherValue
```

Only header1 and header2 are copied from the proxy response.

### ResponseTrailersCopy

| Key | Value | Default | Required |
Expand Down Expand Up @@ -793,6 +859,39 @@ This removes the named trailing header.

ResponseTrailerRemove follows the same structure and guidance as ResponseHeaderRemove.

### ResponseTrailersAllowed

| Key | Value | Required |
|-----|-------|----------|
| ResponseTrailersAllowed | A semicolon separated list of allowed header names. | yes |

Config:
```JSON
{
"ResponseTrailersAllowed": "Header1;header2"
}
```
Code:
```csharp
routeConfig = routeConfig.WithTransformResponseTrailersAllowed("Header1", "header2");
```
```C#
transformBuilderContext.AddResponseTrailersAllowed("Header1", "header2");
```

YARP copies most response trailers from the proxy response by default (see [ResponseTrailersCopy](#ResponseTrailersCopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseTrailersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list.

Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities.

Example:
```
Header1: value1
Header2: value2
AnotherHeader: AnotherValue
```

Only header1 and header2 are copied from the proxy response.

## Extensibility

### AddRequestTransform
Expand Down
58 changes: 58 additions & 0 deletions src/ReverseProxy/Transforms/RequestHeadersAllowedTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;

namespace Yarp.ReverseProxy.Transforms
{
/// <summary>
/// Copies only allowed request headers.
/// </summary>
public class RequestHeadersAllowedTransform : RequestTransform
{
public RequestHeadersAllowedTransform(string[] allowedHeaders)
{
if (allowedHeaders is null)
{
throw new ArgumentNullException(nameof(allowedHeaders));
}

AllowedHeaders = allowedHeaders;
AllowedHeadersSet = new HashSet<string>(allowedHeaders, StringComparer.OrdinalIgnoreCase);
}

internal string[] AllowedHeaders { get; }

private HashSet<string> AllowedHeadersSet { get; }

/// <inheritdoc/>
public override ValueTask ApplyAsync(RequestTransformContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

Debug.Assert(!context.HeadersCopied);

foreach (var header in context.HttpContext.Request.Headers)
{
var headerName = header.Key;
var headerValue = header.Value;
if (!StringValues.IsNullOrEmpty(headerValue)
&& AllowedHeadersSet.Contains(headerName))
{
AddHeader(context, headerName, headerValue);
}
}

context.HeadersCopied = true;

return default;
}
}
}
23 changes: 23 additions & 0 deletions src/ReverseProxy/Transforms/RequestHeadersTransformExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ public static RouteConfig WithTransformRequestHeaderRemove(this RouteConfig rout
});
}

/// <summary>
/// Clones the route and adds the transform which will only copy the allowed request headers. Other transforms
/// that modify or append to existing headers may be affected if not included in the allow list.
/// </summary>
public static RouteConfig WithTransformRequestHeadersAllowed(this RouteConfig route, params string[] allowedHeaders)
{
return route.WithTransform(transform =>
{
transform[RequestHeadersTransformFactory.RequestHeadersAllowedKey] = string.Join(';', allowedHeaders);
});
}

/// <summary>
/// Adds the transform which will append or set the request header.
/// </summary>
Expand All @@ -75,6 +87,17 @@ public static TransformBuilderContext AddRequestHeaderRemove(this TransformBuild
return context;
}

/// <summary>
/// Adds the transform which will only copy the allowed request headers. Other transforms
/// that modify or append to existing headers may be affected if not included in the allow list.
/// </summary>
public static TransformBuilderContext AddRequestHeadersAllowed(this TransformBuilderContext context, params string[] allowedHeaders)
{
context.CopyRequestHeaders = false;
context.RequestTransforms.Add(new RequestHeadersAllowedTransform(allowedHeaders));
return context;
}

/// <summary>
/// Adds the transform which will copy or remove the original host header.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/ReverseProxy/Transforms/RequestHeadersTransformFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal sealed class RequestHeadersTransformFactory : ITransformFactory
internal static readonly string RequestHeaderOriginalHostKey = "RequestHeaderOriginalHost";
internal static readonly string RequestHeaderKey = "RequestHeader";
internal static readonly string RequestHeaderRemoveKey = "RequestHeaderRemove";
internal static readonly string RequestHeadersAllowedKey = "RequestHeadersAllowed";
internal static readonly string AppendKey = "Append";
internal static readonly string SetKey = "Set";

Expand Down Expand Up @@ -46,6 +47,10 @@ public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionar
{
TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);
}
else if (transformValues.TryGetValue(RequestHeadersAllowedKey, out var _))
{
TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);
}
else
{
return false;
Expand Down Expand Up @@ -87,6 +92,16 @@ public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, s
TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);
context.AddRequestHeaderRemove(removeHeaderName);
}
else if (transformValues.TryGetValue(RequestHeadersAllowedKey, out var allowedHeaders))
{
TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);
var headersList = allowedHeaders.Split(';', options: StringSplitOptions.RemoveEmptyEntries
#if NET
| StringSplitOptions.TrimEntries
#endif
);
context.AddRequestHeadersAllowed(headersList);
}
else
{
return false;
Expand Down
72 changes: 72 additions & 0 deletions src/ReverseProxy/Transforms/ResponseHeadersAllowedTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Yarp.ReverseProxy.Transforms
{
/// <summary>
/// Copies only allowed response headers.
/// </summary>
public class ResponseHeadersAllowedTransform : ResponseTransform
{
public ResponseHeadersAllowedTransform(string[] allowedHeaders)
{
if (allowedHeaders is null)
{
throw new ArgumentNullException(nameof(allowedHeaders));
}

AllowedHeaders = allowedHeaders;
AllowedHeadersSet = new HashSet<string>(allowedHeaders, StringComparer.OrdinalIgnoreCase);
}

internal string[] AllowedHeaders { get; }

private HashSet<string> AllowedHeadersSet { get; }

/// <inheritdoc/>
public override ValueTask ApplyAsync(ResponseTransformContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

Debug.Assert(!context.HeadersCopied);

// See https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/HttpTransformer.cs#L67-L77
var responseHeaders = context.HttpContext.Response.Headers;
CopyResponseHeaders(context.ProxyResponse.Headers, responseHeaders);
if (context.ProxyResponse.Content != null)
{
CopyResponseHeaders(context.ProxyResponse.Content.Headers, responseHeaders);
}

context.HeadersCopied = true;

return default;
}

// See https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/HttpTransformer.cs#L102-L115
private void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination)
{
foreach (var header in source)
{
var headerName = header.Key;
if (AllowedHeadersSet.Contains(headerName))
{
Debug.Assert(header.Value is string[]);
destination.Append(headerName, header.Value as string[] ?? header.Value.ToArray());
}
}
}
}
}
Loading

0 comments on commit bfb6406

Please sign in to comment.