Skip to content

Commit b6fbeed

Browse files
committed
Validate that HTTPS config is not applied to non-HTTPS endpoints
1 parent abdc360 commit b6fbeed

File tree

4 files changed

+146
-5
lines changed

4 files changed

+146
-5
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,4 +629,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
629629
<data name="SniNameCannotBeEmpty" xml:space="preserve">
630630
<value>The endpoint {endpointName} is invalid because an SNI configuration section has an empty string as its key. Use a wildcard ('*') SNI section to match all server names.</value>
631631
</data>
632+
<data name="EndpointHasUnusedHttpsConfig" xml:space="preserve">
633+
<value>The non-HTTPS endpoint {endpointName} includes HTTPS-only configuration for {keyName}.</value>
634+
</data>
632635
</root>

src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ internal class ConfigurationReader
1616
private const string CertificatesKey = "Certificates";
1717
private const string CertificateKey = "Certificate";
1818
private const string SslProtocolsKey = "SslProtocols";
19+
private const string EndpointDefaultsKey = "EndpointDefaults";
1920
private const string EndpointsKey = "Endpoints";
2021
private const string UrlKey = "Url";
2122
private const string ClientCertificateModeKey = "ClientCertificateMode";
2223
private const string SniKey = "Sni";
2324

24-
internal const string EndpointDefaultsKey = "EndpointDefaults";
25-
2625
private readonly IConfiguration _configuration;
2726

2827
private IDictionary<string, CertificateConfig> _certificates;
@@ -131,7 +130,7 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
131130
return endpoints;
132131
}
133132

134-
private Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, string endpointName)
133+
private static Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, string endpointName)
135134
{
136135
var sniDictionary = new Dictionary<string, SniConfig>(0, StringComparer.OrdinalIgnoreCase);
137136

@@ -177,7 +176,7 @@ private Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, s
177176
}
178177

179178

180-
private ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
179+
private static ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
181180
{
182181
if (Enum.TryParse<ClientCertificateMode>(clientCertificateMode, ignoreCase: true, out var result))
183182
{
@@ -211,6 +210,29 @@ private Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, s
211210
return acc;
212211
});
213212
}
213+
214+
internal static void ThrowIfContainsHttpsOnlyConfiguration(EndpointConfig endpoint)
215+
{
216+
if (endpoint.Certificate.IsFileCert || endpoint.Certificate.IsStoreCert)
217+
{
218+
throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, CertificateKey));
219+
}
220+
221+
if (endpoint.ClientCertificateMode.HasValue)
222+
{
223+
throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, ClientCertificateModeKey));
224+
}
225+
226+
if (endpoint.SslProtocols.HasValue)
227+
{
228+
throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, SslProtocolsKey));
229+
}
230+
231+
if (endpoint.Sni.Count > 0)
232+
{
233+
throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, SniKey));
234+
}
235+
}
214236
}
215237

216238
// "EndpointDefaults": {
@@ -223,7 +245,6 @@ internal class EndpointDefaults
223245
public HttpProtocols? Protocols { get; set; }
224246
public SslProtocols? SslProtocols { get; set; }
225247
public ClientCertificateMode? ClientCertificateMode { get; set; }
226-
public Dictionary<string, SniConfig> Sni { get; set; }
227248
}
228249

229250
// "EndpointName": {

src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ public void Load()
295295
{
296296
var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https);
297297

298+
if (!https)
299+
{
300+
ConfigurationReader.ThrowIfContainsHttpsOnlyConfiguration(endpoint);
301+
}
302+
298303
Options.ApplyEndpointDefaults(listenOptions);
299304

300305
if (endpoint.Protocols.HasValue)

src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,88 @@ public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPfxFileDoesNotEx
432432
}
433433
}
434434

435+
[Fact]
436+
public void ConfigureEndpoint_ThrowsWhen_HttpsConfigIsDeclaredInNonHttpsEndpoints()
437+
{
438+
var serverOptions = CreateServerOptions();
439+
440+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
441+
{
442+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
443+
// We shouldn't need to specify a real cert, because KestrelConfigurationLoader should check whether the endpoint requires a cert before trying to load it.
444+
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Path", "fakecert.pfx"),
445+
}).Build();
446+
447+
var ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
448+
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Certificate"), ex.Message);
449+
450+
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
451+
{
452+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
453+
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Subject", "example.org"),
454+
}).Build();
455+
456+
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
457+
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Certificate"), ex.Message);
458+
459+
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
460+
{
461+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
462+
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
463+
}).Build();
464+
465+
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
466+
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "ClientCertificateMode"), ex.Message);
467+
468+
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
469+
{
470+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
471+
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", SslProtocols.Tls13.ToString()),
472+
}).Build();
473+
474+
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
475+
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "SslProtocols"), ex.Message);
476+
477+
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
478+
{
479+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
480+
new KeyValuePair<string, string>("Endpoints:End1:Sni:Protocols", HttpProtocols.Http1.ToString()),
481+
}).Build();
482+
483+
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
484+
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Sni"), ex.Message);
485+
}
486+
487+
[Fact]
488+
public void ConfigureEndpoint_DoesNotThrowWhen_HttpsConfigIsDeclaredInEndpointDefaults()
489+
{
490+
var serverOptions = CreateServerOptions();
491+
492+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
493+
{
494+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
495+
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
496+
}).Build();
497+
498+
var (_, endpointsToStart) = serverOptions.Configure(config).Reload();
499+
var end1 = Assert.Single(endpointsToStart);
500+
Assert.NotNull(end1?.EndpointConfig);
501+
Assert.Null(end1.EndpointConfig.ClientCertificateMode);
502+
503+
serverOptions = CreateServerOptions();
504+
505+
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
506+
{
507+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
508+
new KeyValuePair<string, string>("EndpointDefaults:SslProtocols:0", SslProtocols.Tls13.ToString()),
509+
}).Build();
510+
511+
(_, endpointsToStart) = serverOptions.Configure(config).Reload();
512+
end1 = Assert.Single(endpointsToStart);
513+
Assert.NotNull(end1?.EndpointConfig);
514+
Assert.Null(end1.EndpointConfig.SslProtocols);
515+
}
516+
435517
[ConditionalTheory]
436518
[InlineData("http1", HttpProtocols.Http1)]
437519
// [InlineData("http2", HttpProtocols.Http2)] // Not supported due to missing ALPN support. https://github.com/dotnet/corefx/issues/33016
@@ -746,6 +828,36 @@ public void EndpointConfigureSection_CanSetClientCertificateMode()
746828
Assert.True(ran2);
747829
}
748830

831+
832+
[Fact]
833+
public void EndpointConfigureSection_CanConfigureSni()
834+
{
835+
var serverOptions = CreateServerOptions();
836+
var certPath = Path.Combine("shared", "TestCertificates", "https-ecdsa.pem");
837+
var keyPath = Path.Combine("shared", "TestCertificates", "https-ecdsa.key");
838+
839+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
840+
{
841+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
842+
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Protocols", HttpProtocols.None.ToString()),
843+
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:SslProtocols:0", SslProtocols.Tls13.ToString()),
844+
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
845+
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:Path", certPath),
846+
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:KeyPath", keyPath),
847+
}).Build();
848+
849+
var (_, endpointsToStart) = serverOptions.Configure(config).Reload();
850+
var end1 = Assert.Single(endpointsToStart);
851+
var (name, sniConfig) = Assert.Single(end1?.EndpointConfig?.Sni);
852+
853+
Assert.Equal("*.example.org", name);
854+
Assert.Equal(HttpProtocols.None, sniConfig.Protocols);
855+
Assert.Equal(SslProtocols.Tls13, sniConfig.SslProtocols);
856+
Assert.Equal(ClientCertificateMode.RequireCertificate, sniConfig.ClientCertificateMode);
857+
Assert.Equal(certPath, sniConfig.Certificate.Path);
858+
Assert.Equal(keyPath, sniConfig.Certificate.KeyPath);
859+
}
860+
749861
[Fact]
750862
public void EndpointConfigureSection_CanOverrideClientCertificateModeFromConfigureHttpsDefaults()
751863
{

0 commit comments

Comments
 (0)