Skip to content

Commit

Permalink
Allow specifying different properties for endpoints (#172)
Browse files Browse the repository at this point in the history
* nit: Remove dead code

* Remove unused package references

* Remove ICustomHeaderService and make header service static

* Add support for adding multiple named policies, and referencing them in the middleware

* Add support for specifying a different header policy for a given endpoint

* Allow configuring the default policy in the services

* Add ReadMe

* Add support for MVC attributes with named policies
  • Loading branch information
andrewlock committed Sep 6, 2024
1 parent bef205d commit 6471076
Show file tree
Hide file tree
Showing 23 changed files with 676 additions and 394 deletions.
101 changes: 100 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ When you install the package, it should be added to your `.csproj`. Alternativel
</Project>
```

Simply add the middleware to your ASP.NET Core application by configuring it as part of your normal `Startup` pipeline. Note that the order of middleware matters, so to apply the headers to all requests it should be configured first in your pipeline.
There are various ways to configure the headers for your application.

In the simplest scenario, add the middleware to your ASP.NET Core application by configuring it as part of your normal `Startup` pipeline (or `WebApplication` in .NET 6+). Note that the order of middleware matters, so to apply the headers to all requests it should be configured first in your pipeline.

To use the default security headers for your application, add the middleware using:

Expand Down Expand Up @@ -152,6 +154,103 @@ public void Configure(IApplicationBuilder app)
}
```

## Applying different headers to different endpoints

In some situations, you may need to apply different security headers to different endpoints. For example, you may want to have a very restrictive Content-Security-Policy by default, but then have a more relaxed on specific endpoints that require it. This is supported, but requires more configuration.

### 1. Configure your policies using `AddSecurityHeaderPolicies()`

You can configure named and default policies by calling `AddSecurityHeaderPolicies()` on `IServiceCollection`. You can configure the default policy to use, as well as any named policies. For example, the following configures the default policy (used when `UseSecurityHeaders()` is called without any arguments), and a named policy:

```csharp
var builder = WebApplication.CreateBuilder();

// 👇 Call AddSecurityHeaderPolicies()
builder.Services.AddSecurityHeaderPolicies()
.SetDefaultPolicy(policy => policy.AddDefaultSecurityHeaders()) // 👈 Configure the default policy
.AddPolicy("CustomPolicy", policy => policy.AddCustomHeader("X-Custom", "SomeValue")); // 👈 Configure named policies
```

### 2. Add the default middleware early to the pipeline

The security headers middleware can only add headers to _all_ requests if it is early in the middleware pipeline, so it's important to add the headders middleware at the start of your middleware pipeline. However, if you want to have endpoint-specific policies, then you _also_ need to place the middleware after the call to `UseRouting()`, as that is the point at which the endpoint that will be executed is selected.

```csharp
var builder = WebApplication.CreateBuilder();

// 👇 Configure policies as shown previously
builder.Services.AddSecurityHeaderPolicies()
.SetDefaultPolicy(policy => policy.AddDefaultSecurityHeaders())
.AddPolicy("CustomPolicy", policy => policy.AddCustomHeader("X-Custom", "SomeValue"));

var app = builder.Build();

// Set the default headers for requests that
// 👇 don't make it to the routing middleware
app.UseSecurityHeaders();

app.UseStaticFiles(); // other middleware
app.UseAuthentication();
app.UseRouting();

app.UseSecurityHeaders(); // 👈 Add after the routing middleware
app.UseAuthorization();

app.MapGet("/", () => "Hello world");
app.Run();
```

Note that if you pass a policy to any call to `UseSecurityHeaders()` it will override the "default" policy used at that point.

### 3. Apply custom policies to endpoints

To apply a non-default policy to an endpoint, use the `WithSecurityHeadersPolicy(policy)` endpoint extension method, and pass in the name of the policy to apply:

```csharp
var builder = WebApplication.CreateBuilder();

builder.Services.AddSecurityHeaderPolicies()
.SetDefaultPolicy(policy => policy.AddDefaultSecurityHeaders())
.AddPolicy("CustomPolicy", policy => policy.AddCustomHeader("X-Custom", "SomeValue"));

var app = builder.Build();

app.UseSecurityHeaders();

app.UseStaticFiles();
app.UseAuthentication();
app.UseRouting();

app.UseSecurityHeaders();
app.UseAuthorization();

app.MapGet("/", () => "Hello world")
.WithSecurityHeadersPolicy("CustomPolicy"); // 👈 Apply a named policy to the endpoint
app.Run();
```

If you're using MVC controllers or Razor Pages, you can apply the `[SecurityHeadersPolicy(policyName)]` attribute to your endpoints:

```csharp
public class HomeController : ControllerBase
{
[SecurityHeadersPolicy("CustomHeader")] // 👈 Apply a custom header to the endpoint
public IActionResult Index()
{
return View();
}
}
```

Each call to `UseSecurityHeaders()` will re-evaluate the applicable policies; the headers are applied just before the response is sent. The policy to apply is determined as follows, with the first applicable policy selected.

1. If an endpoint has been selected, and a named policy is applied, use that.
2. If a named or policy instance is passed to the `SecurityHeadersMiddleware`, use that.
3. If the default policy has been set using `SetDefaultPolicy()`, use that.
4. Otherwise, apply the default headers (those added by `AddDefaultSecurityHeaders()`)


## RemoveServerHeader

One point to be aware of is that the `RemoveServerHeader` method will rarely (ever?) be sufficient to remove the `Server` header from your output. If any subsequent middleware in your application pipeline add the header, then this will be able to remove it. However Kestrel will generally add the `Server` header too late in the pipeline to be able to modify it.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using NetEscapades.AspNetCore.SecurityHeaders;

// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// Header extension methods for <see cref="IEndpointConventionBuilder"/>.
/// </summary>
public static class EndpointConventionBuilderExtensions
{
/// <summary>
/// Adds a security headers policy with the provided policy name to the endpoint(s).
/// </summary>
/// <param name="builder">The endpoint convention builder.</param>
/// <param name="policyName">The security headers policy to use.</param>
/// <returns>The original convention builder parameter.</returns>
/// <typeparam name="TBuilder">The type of the endpoint convention builder</typeparam>
public static TBuilder WithSecurityHeadersPolicy<TBuilder>(this TBuilder builder, string policyName)
where TBuilder : IEndpointConventionBuilder
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Add(endpointBuilder => { endpointBuilder.Metadata.Add(new SecurityHeadersPolicyMetadata(policyName)); });
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,18 @@ namespace NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;
/// <summary>
/// Provides programmatic configuration for Security Headers.
/// </summary>
[Obsolete("This class is unused since v0.5.0, and will be removed in a future version of the package")]
public class CustomHeaderOptions
internal class CustomHeaderOptions
{
private string _defaultPolicyName = "__DefaultSecurityHeadersPolicy";

/// <summary>
/// The collections of policies to apply
/// </summary>
/// <returns>The collection of policies, indexed by header name</returns>
public Dictionary<string, HeaderPolicyCollection> PolicyCollections { get; } =
new Dictionary<string, HeaderPolicyCollection>();
public Dictionary<string, HeaderPolicyCollection> NamedPolicyCollections { get; } = new();

/// <summary>
/// Gets or sets the name of the default policy
/// The default policy to apply
/// </summary>
/// <returns>The name of the default policy</returns>
public string DefaultPolicyName
{
get => _defaultPolicyName;
set => _defaultPolicyName = value ?? throw new ArgumentNullException(nameof(value));
}
public HeaderPolicyCollection? DefaultPolicy { get; set; }

/// <summary>
/// Gets the policy based on the <paramref name="name"/>
Expand All @@ -41,6 +32,6 @@ public string DefaultPolicyName
throw new ArgumentNullException(nameof(name));
}

return PolicyCollections.ContainsKey(name) ? PolicyCollections[name] : null;
return NamedPolicyCollections.GetValueOrDefault(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
namespace NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;

/// <summary>
/// Default implementation of <see cref="ICustomHeaderService"/>.
/// Handles processing of headers
/// </summary>
public class CustomHeaderService : ICustomHeaderService
internal static class CustomHeaderService
{
/// <summary>
/// Evaluates the given <paramref name="policies"/> using the passed in <paramref name="context"/>.
Expand All @@ -17,7 +17,7 @@ public class CustomHeaderService : ICustomHeaderService
/// <param name="policies">The <see cref="HeaderPolicyCollection"/> containing policies to be evaluated.</param>
/// <returns>A <see cref="CustomHeadersResult"/> which contains the result of policy evaluation and can be
/// used by the caller to set appropriate response headers.</returns>
public virtual CustomHeadersResult EvaluatePolicy(HttpContext context, HeaderPolicyCollection policies)
public static CustomHeadersResult EvaluatePolicy(HttpContext context, HeaderPolicyCollection policies)
{
if (context == null)
{
Expand Down Expand Up @@ -50,7 +50,7 @@ public virtual CustomHeadersResult EvaluatePolicy(HttpContext context, HeaderPol
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/> associated with the current call.</param>
/// <param name="result">The <see cref="CustomHeadersResult"/> used to read the allowed values.</param>
public virtual void ApplyResult(HttpResponse response, CustomHeadersResult result)
public static void ApplyResult(HttpResponse response, CustomHeadersResult result)
{
if (response == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;

/// <summary>
/// Results returned by <see cref="ICustomHeaderService"/>
/// Results returned by <see cref="CustomHeaderService"/>
/// </summary>
public class CustomHeadersResult
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace NetEscapades.AspNetCore.SecurityHeaders;

/// <summary>
/// Metadata about which policy to apply to and endpoint
/// </summary>
internal interface ISecurityHeadersPolicyMetadata
{
/// <summary>
/// The name of the policy to apply
/// </summary>
public string PolicyName { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using Microsoft.AspNetCore.Builder;

namespace NetEscapades.AspNetCore.SecurityHeaders.Infrastructure;

/// <summary>
/// Used to configure <see cref="HeaderPolicyCollection"/> for security headers
/// </summary>
public class SecurityHeaderPolicyBuilder
{
private readonly CustomHeaderOptions _options;

/// <summary>
/// Initializes a new instance of the <see cref="SecurityHeaderPolicyBuilder"/> class.
/// </summary>
/// <param name="options">The options where the configuration is stored</param>
internal SecurityHeaderPolicyBuilder(CustomHeaderOptions options)
{
_options = options;
}

/// <summary>
/// Adds a security header policy to the application with the given name.
/// This name can be used to refer to the policy in endpoints
/// </summary>
/// <param name="name">The name of the policy </param>
/// <param name="configurePolicy">An <see cref="Action{T}"/>to configure the security headers for the policy</param>
/// <returns>The <see cref="SecurityHeaderPolicyBuilder"/> for chaining</returns>
public SecurityHeaderPolicyBuilder AddPolicy(string name, Action<HeaderPolicyCollection> configurePolicy)
{
var policyCollection = new HeaderPolicyCollection();
configurePolicy(policyCollection);
return AddPolicy(name, policyCollection);
}

/// <summary>
/// Adds a security header policy to the application with the given name.
/// This name can be used to refer to the policy in endpoints
/// </summary>
/// <param name="name">The name of the policy </param>
/// <param name="policyCollection">The security headers for the policy</param>
/// <returns>The <see cref="SecurityHeaderPolicyBuilder"/> for chaining</returns>
public SecurityHeaderPolicyBuilder AddPolicy(string name, HeaderPolicyCollection policyCollection)
{
_options.NamedPolicyCollections[name] = policyCollection;
return this;
}

/// <summary>
/// Sets the default security header policy to use when no other named policy is provided
/// This policy is used wherever a named policy does not apply
/// </summary>
/// <param name="configurePolicy">An <see cref="Action{T}"/>to configure the security headers for the policy</param>
/// <returns>The <see cref="SecurityHeaderPolicyBuilder"/> for chaining</returns>
public SecurityHeaderPolicyBuilder SetDefaultPolicy(Action<HeaderPolicyCollection> configurePolicy)
{
var policyCollection = new HeaderPolicyCollection();
configurePolicy(policyCollection);
return SetDefaultPolicy(policyCollection);
}

/// <summary>
/// Adds the default security header policy to the application.
/// This policy is used wherever a named policy does not apply
/// </summary>
/// <param name="policyCollection">The security headers for the policy</param>
/// <returns>The <see cref="SecurityHeaderPolicyBuilder"/> for chaining</returns>
public SecurityHeaderPolicyBuilder SetDefaultPolicy(HeaderPolicyCollection policyCollection)
{
_options.DefaultPolicy = policyCollection;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace NetEscapades.AspNetCore.SecurityHeaders;

/// <summary>
/// Creates a new instance of <see cref="SecurityHeadersPolicyMetadata"/> with the specified <paramref name="policyName"/>
/// </summary>
/// <param name="policyName">The name of the policy to apply</param>
internal class SecurityHeadersPolicyMetadata(string policyName) : ISecurityHeadersPolicyMetadata
{
/// <inheritdoc/>
public string PolicyName { get; } = policyName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,7 @@
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.0'">
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

Expand Down
Loading

0 comments on commit 6471076

Please sign in to comment.