Skip to content

Commit

Permalink
Feature/ignore provided urls (#142)
Browse files Browse the repository at this point in the history
* Added support for disabling the middleware for a given set of URLs

* Ran dotnet-format

* Minor version bump

* Ran dotnet-format on code base

* Fixed typo in comment

* Added comment explaining why we're using Microsoft.AspNetCore.TestHost version 8

* Added test for null ignore list supplied to middleware config
  • Loading branch information
jamie-taylor-rjj authored Dec 3, 2024
1 parent 53bf356 commit 348ce8f
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 88 deletions.
10 changes: 9 additions & 1 deletion example/OwaspHeaders.Core.Example/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ public class HomeController(ILogger<HomeController> logger) : ControllerBase
[HttpGet(Name = "/")]
public IEnumerable<string> Get()
{
return HttpContext.Response.Headers.Select(h => h.ToString()).ToArray();
return GetHeaders;
}

[HttpGet("skipthis", Name = "SkipThis")]
public IEnumerable<string> SkipThis()
{
return GetHeaders;
}

private IEnumerable<string> GetHeaders => HttpContext.Response.Headers.Select(h => h.ToString()).ToArray();
}
3 changes: 2 additions & 1 deletion example/OwaspHeaders.Core.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@

app.UseAuthorization();

app.UseSecureHeadersMiddleware();
var listOfUrlsToIgnore = new List<string> { "/skipthis" };
app.UseSecureHeadersMiddleware(urlIgnoreList: listOfUrlsToIgnore);

app.MapControllers();

Expand Down
19 changes: 19 additions & 0 deletions src/Extensions/SecureHeadersMiddlewareBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,25 @@ public static SecureHeadersMiddlewareConfiguration UseCrossOriginResourcePolicy(
return config;
}

/// <summary>
/// Used to set a list of URLs that the we want the middleware to NOT operate on
/// </summary>
/// <param name="config"></param>
/// <param name="urlsToIgnore">
/// A list of URLs that we the middleware to not operate on
/// </param>
/// <returns></returns>
public static SecureHeadersMiddlewareConfiguration SetUrlsToIgnore(
this SecureHeadersMiddlewareConfiguration config,
List<string> urlsToIgnore = null)
{
if (urlsToIgnore != null)
{
config.UrlsToIgnore = urlsToIgnore;
}
return config;
}

/// <summary>
/// Return the completed <see cref="SecureHeadersMiddlewareConfiguration"/> ready for consumption by the
/// <see cref="SecureHeadersMiddleware"/> class
Expand Down
13 changes: 10 additions & 3 deletions src/Extensions/SecureHeadersMiddlewareExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public static class SecureHeadersMiddlewareExtensions
/// url for the current best practises:
/// https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Best_Practices
/// </remarks>
public static SecureHeadersMiddlewareConfiguration BuildDefaultConfiguration()
public static SecureHeadersMiddlewareConfiguration BuildDefaultConfiguration(
List<string> urlIgnoreList = null)
{
return SecureHeadersMiddlewareBuilder
.CreateBuilder()
Expand All @@ -32,6 +33,7 @@ public static SecureHeadersMiddlewareConfiguration BuildDefaultConfiguration()
.UseCacheControl()
.UseXssProtection()
.UseCrossOriginResourcePolicy()
.SetUrlsToIgnore(urlIgnoreList)
.Build();
}

Expand All @@ -47,6 +49,11 @@ public static SecureHeadersMiddlewareConfiguration BuildDefaultConfiguration()
/// [OPTIONAL] An instance of the <see cref="SecureHeadersMiddlewareConfiguration" />
/// containing all the config for each request
/// </param>
/// <param name="urlIgnoreList">
/// A list of URLs to ignore when processes requests. For example, to disable the entire
/// middleware when the user accesses "/path-to-ignore", add this to the list and the
/// middleware will be disabled for that URL.
/// </param>
/// <returns>
/// The <see cref="IApplicationBuilder"/> with the <see cref="SecureHeadersMiddleware" /> added
/// </returns>
Expand All @@ -55,11 +62,11 @@ public static SecureHeadersMiddlewareConfiguration BuildDefaultConfiguration()
/// then the default value from <see cref="BuildDefaultConfiguration"/> will be provided.
/// </remarks>
public static IApplicationBuilder UseSecureHeadersMiddleware(this IApplicationBuilder builder,
SecureHeadersMiddlewareConfiguration config = null)
SecureHeadersMiddlewareConfiguration config = null, List<string> urlIgnoreList = null)
{
ObjectGuardClauses.ObjectCannotBeNull(builder, nameof(builder),
"cannot be null when setting up OWASP Secure Headers in OwaspHeaders.Core");
return builder.UseMiddleware<SecureHeadersMiddleware>(config ?? BuildDefaultConfiguration());
return builder.UseMiddleware<SecureHeadersMiddleware>(config ?? BuildDefaultConfiguration(urlIgnoreList));
}
}
}
6 changes: 6 additions & 0 deletions src/Models/SecureHeadersMiddlewareConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,11 @@ public class SecureHeadersMiddlewareConfiguration
public ExpectCt ExpectCt { get; set; }

public CrossOriginResourcePolicy CrossOriginResourcePolicy { get; set; }

/// <summary>
/// A list of URLs that, when requested, should be ignored completely by
/// the middleware
/// </summary>
public List<string> UrlsToIgnore { get; set; } = [];
}
}
2 changes: 1 addition & 1 deletion src/OwaspHeaders.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<!-- NuGet metadata -->
<PackageId>OwaspHeaders.Core</PackageId>
<Version>9.3.0</Version>
<Version>9.4.0</Version>
<Authors>Jamie Taylor</Authors>
<Company>RJJ Software Ltd</Company>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
141 changes: 79 additions & 62 deletions src/SecureHeadersMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;

namespace OwaspHeaders.Core
{
Expand All @@ -23,86 +24,102 @@ public async Task InvokeAsync(HttpContext httpContext)
throw new ArgumentException($"Expected an instance of the {nameof(SecureHeadersMiddlewareConfiguration)} object.");
}

if (config.UseHsts)
if (!RequestShouldBeIgnored(httpContext.Request.Path))
{
httpContext.TryAddHeader(Constants.StrictTransportSecurityHeaderName,
config.HstsConfiguration.BuildHeaderValue());
}
if (config.UseHsts)
{
httpContext.TryAddHeader(Constants.StrictTransportSecurityHeaderName,
config.HstsConfiguration.BuildHeaderValue());
}

if (config.UseXFrameOptions)
{
httpContext.TryAddHeader(Constants.XFrameOptionsHeaderName,
config.XFrameOptionsConfiguration.BuildHeaderValue());
}
if (config.UseXFrameOptions)
{
httpContext.TryAddHeader(Constants.XFrameOptionsHeaderName,
config.XFrameOptionsConfiguration.BuildHeaderValue());
}

if (config.UseXssProtection)
{
httpContext.TryAddHeader(Constants.XssProtectionHeaderName,
config.XssConfiguration.BuildHeaderValue());
}
if (config.UseXssProtection)
{
httpContext.TryAddHeader(Constants.XssProtectionHeaderName,
config.XssConfiguration.BuildHeaderValue());
}

if (config.UseXContentTypeOptions)
{
httpContext.TryAddHeader(Constants.XContentTypeOptionsHeaderName, "nosniff");
}
if (config.UseXContentTypeOptions)
{
httpContext.TryAddHeader(Constants.XContentTypeOptionsHeaderName, "nosniff");
}

if (config.UseContentSecurityPolicyReportOnly)
{
if (string.IsNullOrWhiteSpace(_calculatedContentSecurityPolicy))
if (config.UseContentSecurityPolicyReportOnly)
{
_calculatedContentSecurityPolicy = config.ContentSecurityPolicyReportOnlyConfiguration.BuildHeaderValue();
if (string.IsNullOrWhiteSpace(_calculatedContentSecurityPolicy))
{
_calculatedContentSecurityPolicy =
config.ContentSecurityPolicyReportOnlyConfiguration.BuildHeaderValue();
}

httpContext.TryAddHeader(Constants.ContentSecurityPolicyReportOnlyHeaderName,
_calculatedContentSecurityPolicy);
}
httpContext.TryAddHeader(Constants.ContentSecurityPolicyReportOnlyHeaderName,
_calculatedContentSecurityPolicy);
}
else if (config.UseContentSecurityPolicy)
{
if (string.IsNullOrWhiteSpace(_calculatedContentSecurityPolicy))
else if (config.UseContentSecurityPolicy)
{
_calculatedContentSecurityPolicy = config.ContentSecurityPolicyConfiguration.BuildHeaderValue();
if (string.IsNullOrWhiteSpace(_calculatedContentSecurityPolicy))
{
_calculatedContentSecurityPolicy = config.ContentSecurityPolicyConfiguration.BuildHeaderValue();
}

httpContext.TryAddHeader(Constants.ContentSecurityPolicyHeaderName,
_calculatedContentSecurityPolicy);
}
httpContext.TryAddHeader(Constants.ContentSecurityPolicyHeaderName,
_calculatedContentSecurityPolicy);
}

if (config.UseXContentSecurityPolicy)
{
httpContext.TryAddHeader(Constants.XContentSecurityPolicyHeaderName,
config.ContentSecurityPolicyConfiguration.BuildHeaderValue());
}
if (config.UseXContentSecurityPolicy)
{
httpContext.TryAddHeader(Constants.XContentSecurityPolicyHeaderName,
config.ContentSecurityPolicyConfiguration.BuildHeaderValue());
}

if (config.UsePermittedCrossDomainPolicy)
{
httpContext.TryAddHeader(Constants.PermittedCrossDomainPoliciesHeaderName,
config.PermittedCrossDomainPolicyConfiguration.BuildHeaderValue());
}
if (config.UsePermittedCrossDomainPolicy)
{
httpContext.TryAddHeader(Constants.PermittedCrossDomainPoliciesHeaderName,
config.PermittedCrossDomainPolicyConfiguration.BuildHeaderValue());
}

if (config.UseReferrerPolicy)
{
httpContext.TryAddHeader(Constants.ReferrerPolicyHeaderName,
config.ReferrerPolicy.BuildHeaderValue());
}
if (config.UseReferrerPolicy)
{
httpContext.TryAddHeader(Constants.ReferrerPolicyHeaderName,
config.ReferrerPolicy.BuildHeaderValue());
}

if (config.UseExpectCt)
{
httpContext.TryAddHeader(Constants.ExpectCtHeaderName,
config.ExpectCt.BuildHeaderValue());
}
if (config.UseExpectCt)
{
httpContext.TryAddHeader(Constants.ExpectCtHeaderName,
config.ExpectCt.BuildHeaderValue());
}

if (config.UseCacheControl)
{
httpContext.TryAddHeader(Constants.CacheControlHeaderName,
config.CacheControl.BuildHeaderValue());
}
if (config.UseCacheControl)
{
httpContext.TryAddHeader(Constants.CacheControlHeaderName,
config.CacheControl.BuildHeaderValue());
}

if (config.UseCrossOriginResourcePolicy)
{
httpContext.TryAddHeader(Constants.CrossOriginResourcePolicyHeaderName,
config.CrossOriginResourcePolicy.BuildHeaderValue());
if (config.UseCrossOriginResourcePolicy)
{
httpContext.TryAddHeader(Constants.CrossOriginResourcePolicyHeaderName,
config.CrossOriginResourcePolicy.BuildHeaderValue());
}
}

// Call the next middleware in the chain
await next(httpContext);
}

private bool RequestShouldBeIgnored(PathString requestedPath)
{
if (config.UrlsToIgnore.Count == 0)
{
return false;
}

return requestedPath.HasValue && config.UrlsToIgnore.Any(url => url.Equals(requestedPath.Value!, StringComparison.InvariantCulture));
}
}
}
99 changes: 99 additions & 0 deletions tests/OwaspHeaders.Core.Tests/CustomHeaders/UrlIgnoreListTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace OwaspHeaders.Core.Tests.CustomHeaders
{
public class UrlIgnoreListTests
{
private readonly string UrlToIgnore = "/ignore-me";
private readonly string UrlWontIgnore = "/do-not-ignore-me";
private readonly TestServer TestServer;

public UrlIgnoreListTests()
{
var host = new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRouting();
})
.Configure(app =>
{
app.UseRouting();
app.UseSecureHeadersMiddleware(urlIgnoreList: [UrlToIgnore]);
app.UseEndpoints(endpoints =>
{
endpoints.MapGet(UrlToIgnore, () =>
TypedResults.Text("Hello Tests"));
endpoints.MapGet(UrlWontIgnore, () =>
TypedResults.Text("Hello Tests"));
});
});
})
.Start();

TestServer = host.GetTestServer();
TestServer.BaseAddress = new Uri("https://example.com/");
}

[Fact]
public async Task Invoke_IgnoreList_Contains_TargetUrl_NoHeadersAdded()
{
// arrange

// Act
var context = await TestServer.SendAsync(c =>
{
c.Request.Path = UrlToIgnore;
c.Request.Method = HttpMethods.Get;
});

// Assert
Assert.NotNull(context.Response);
Assert.NotNull(context.Response.Headers);

// Checking none of the default headers are present
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.StrictTransportSecurityHeaderName);
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.XFrameOptionsHeaderName);
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.XssProtectionHeaderName);
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.XContentTypeOptionsHeaderName);
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.ContentSecurityPolicyHeaderName);
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.PermittedCrossDomainPoliciesHeaderName);
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.ReferrerPolicyHeaderName);
Assert.DoesNotContain(context.Response.Headers, h => h.Key == Constants.CacheControlHeaderName);
}

[Fact]
public async Task Invoke_IgnoreList_DoesntContain_TargetUrl_NoHeadersAdded()
{
// arrange

// Act
var context = await TestServer.SendAsync(c =>
{
c.Request.Path = UrlWontIgnore;
c.Request.Method = HttpMethods.Get;
});

// Assert
Assert.NotNull(context.Response);
Assert.NotNull(context.Response.Headers);

// Checking all of the default headers are present
Assert.Contains(context.Response.Headers, h => h.Key == Constants.StrictTransportSecurityHeaderName);
Assert.Contains(context.Response.Headers, h => h.Key == Constants.XFrameOptionsHeaderName);
Assert.Contains(context.Response.Headers, h => h.Key == Constants.XssProtectionHeaderName);
Assert.Contains(context.Response.Headers, h => h.Key == Constants.XContentTypeOptionsHeaderName);
Assert.Contains(context.Response.Headers, h => h.Key == Constants.ContentSecurityPolicyHeaderName);
Assert.Contains(context.Response.Headers, h => h.Key == Constants.PermittedCrossDomainPoliciesHeaderName);
Assert.Contains(context.Response.Headers, h => h.Key == Constants.ReferrerPolicyHeaderName);
Assert.Contains(context.Response.Headers, h => h.Key == Constants.CacheControlHeaderName);
}
}
}
Loading

0 comments on commit 348ce8f

Please sign in to comment.