Exclude setting HSTS on localhost and allow for user to specify excluded domains. (#274)
This commit is contained in:
parent
3488c24804
commit
892cebb27e
|
|
@ -61,7 +61,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsP
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpsPolicySample", "samples\HttpsPolicySample\HttpsPolicySample.csproj", "{AC424AEE-4883-49C6-945F-2FC916B8CA1C}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpsPolicySample", "samples\HttpsPolicySample\HttpsPolicySample.csproj", "{AC424AEE-4883-49C6-945F-2FC916B8CA1C}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsPolicy.Tests", "test\Microsoft.AspNetCore.HttpsEnforcement.Tests\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", "{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsPolicy.Tests", "test\Microsoft.AspNetCore.HttpsPolicy.Tests\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", "{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -22,6 +23,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
|
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly StringValues _strictTransportSecurityValue;
|
private readonly StringValues _strictTransportSecurityValue;
|
||||||
|
private readonly IList<string> _excludedHosts;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the HSTS middleware.
|
/// Initialize the HSTS middleware.
|
||||||
|
|
@ -30,24 +32,20 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
/// <param name="options"></param>
|
/// <param name="options"></param>
|
||||||
public HstsMiddleware(RequestDelegate next, IOptions<HstsOptions> options)
|
public HstsMiddleware(RequestDelegate next, IOptions<HstsOptions> options)
|
||||||
{
|
{
|
||||||
if (next == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(next));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options == null)
|
if (options == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
_next = next;
|
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||||
|
|
||||||
var hstsOptions = options.Value;
|
var hstsOptions = options.Value;
|
||||||
var maxAge = Convert.ToInt64(Math.Floor(hstsOptions.MaxAge.TotalSeconds))
|
var maxAge = Convert.ToInt64(Math.Floor(hstsOptions.MaxAge.TotalSeconds))
|
||||||
.ToString(CultureInfo.InvariantCulture);
|
.ToString(CultureInfo.InvariantCulture);
|
||||||
var includeSubdomains = hstsOptions.IncludeSubDomains ? IncludeSubDomains : StringSegment.Empty;
|
var includeSubdomains = hstsOptions.IncludeSubDomains ? IncludeSubDomains : StringSegment.Empty;
|
||||||
var preload = hstsOptions.Preload ? Preload : StringSegment.Empty;
|
var preload = hstsOptions.Preload ? Preload : StringSegment.Empty;
|
||||||
_strictTransportSecurityValue = new StringValues($"max-age={maxAge}{includeSubdomains}{preload}");
|
_strictTransportSecurityValue = new StringValues($"max-age={maxAge}{includeSubdomains}{preload}");
|
||||||
|
_excludedHosts = hstsOptions.ExcludedHosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -57,12 +55,29 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task Invoke(HttpContext context)
|
public Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
if (context.Request.IsHttps)
|
if (context.Request.IsHttps && !IsHostExcluded(context.Request.Host.Host))
|
||||||
{
|
{
|
||||||
context.Response.Headers[HeaderNames.StrictTransportSecurity] = _strictTransportSecurityValue;
|
context.Response.Headers[HeaderNames.StrictTransportSecurity] = _strictTransportSecurityValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _next(context);
|
return _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsHostExcluded(string host)
|
||||||
|
{
|
||||||
|
if (_excludedHosts == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < _excludedHosts.Count; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(host, _excludedHosts[i], StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.HttpsPolicy
|
namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
{
|
{
|
||||||
|
|
@ -35,5 +36,15 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
/// to preload HSTS sites on fresh install. See https://hstspreload.org/.
|
/// to preload HSTS sites on fresh install. See https://hstspreload.org/.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool Preload { get; set; }
|
public bool Preload { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of host names that will not add the HSTS header.
|
||||||
|
/// </summary>
|
||||||
|
public IList<string> ExcludedHosts { get; } = new List<string>
|
||||||
|
{
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1", // ipv4
|
||||||
|
"[::1]" // ipv6
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
|
|
||||||
var server = new TestServer(builder);
|
var server = new TestServer(builder);
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
client.BaseAddress = new Uri("https://localhost:5050");
|
client.BaseAddress = new Uri("https://example.com:5050");
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
|
|
||||||
var server = new TestServer(builder);
|
var server = new TestServer(builder);
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
client.BaseAddress = new Uri("https://localhost:5050");
|
client.BaseAddress = new Uri("https://example.com:5050");
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
@ -113,7 +114,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
|
|
||||||
var server = new TestServer(builder);
|
var server = new TestServer(builder);
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
client.BaseAddress = new Uri("https://localhost:5050");
|
client.BaseAddress = new Uri("https://example.com:5050");
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
@ -121,5 +122,98 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.Equal(expected, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault());
|
Assert.Equal(expected, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("localhost")]
|
||||||
|
[InlineData("Localhost")]
|
||||||
|
[InlineData("LOCALHOST")]
|
||||||
|
[InlineData("127.0.0.1")]
|
||||||
|
[InlineData("[::1]")]
|
||||||
|
public async Task DefaultExcludesCommonLocalhostDomains_DoesNotSetHstsHeader(string host)
|
||||||
|
{
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseHsts();
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
return context.Response.WriteAsync("Hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
client.BaseAddress = new Uri($"https://{host}:5050");
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Empty(response.Headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("localhost")]
|
||||||
|
[InlineData("127.0.0.1")]
|
||||||
|
[InlineData("[::1]")]
|
||||||
|
public async Task AllowLocalhostDomainsIfListIsReset_SetHstsHeader(string host)
|
||||||
|
{
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddHsts(options =>
|
||||||
|
{
|
||||||
|
options.ExcludedHosts.Clear();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseHsts();
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
return context.Response.WriteAsync("Hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
client.BaseAddress = new Uri($"https://{host}:5050");
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Single(response.Headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("example.com")]
|
||||||
|
[InlineData("Example.com")]
|
||||||
|
[InlineData("EXAMPLE.COM")]
|
||||||
|
public async Task AddExcludedDomains_DoesNotAddHstsHeader(string host)
|
||||||
|
{
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddHsts(options => {
|
||||||
|
options.ExcludedHosts.Add(host);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseHsts();
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
return context.Response.WriteAsync("Hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
client.BaseAddress = new Uri($"https://{host}:5050");
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Empty(response.Headers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
options.IncludeSubDomains = includeSubDomains;
|
options.IncludeSubDomains = includeSubDomains;
|
||||||
options.MaxAge = TimeSpan.FromSeconds(maxAge);
|
options.MaxAge = TimeSpan.FromSeconds(maxAge);
|
||||||
options.Preload = preload;
|
options.Preload = preload;
|
||||||
|
options.ExcludedHosts.Clear(); // allowing localhost for testing
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.Configure(app =>
|
.Configure(app =>
|
||||||
Loading…
Reference in New Issue