HTTPS certificate validation options

New: Enable HTTPS certificate validation by default
New: Option to disable certificate validation for all or only local addresses
This commit is contained in:
Mark McDowall 2019-03-28 19:20:40 -07:00 committed by Mark McDowall
parent 439870a546
commit 7d06e5d684
17 changed files with 185 additions and 135 deletions

View File

@ -10,6 +10,18 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormInputButton from 'Components/Form/FormInputButton'; import FormInputButton from 'Components/Form/FormInputButton';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
];
const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
{ key: 'disabled', value: 'Disabled' }
];
class SecuritySettings extends Component { class SecuritySettings extends Component {
// //
@ -57,15 +69,10 @@ class SecuritySettings extends Component {
authenticationMethod, authenticationMethod,
username, username,
password, password,
apiKey apiKey,
certificateValidation
} = settings; } = settings;
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
];
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
return ( return (
@ -146,6 +153,19 @@ class SecuritySettings extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>Certificate Validation</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="certificateValidation"
values={certificateValidationOptions}
helpText="Change how strict HTTPS certification validation is"
onChange={onInputChange}
{...certificateValidation}
/>
</FormGroup>
<ConfirmModal <ConfirmModal
isOpen={this.state.isConfirmApiKeyResetModalOpen} isOpen={this.state.isConfirmApiKeyResetModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}

View File

@ -0,0 +1,30 @@
using System.Net;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Test.ExtensionTests
{
[TestFixture]
public class IPAddressExtensionsFixture
{
[TestCase("::1")]
[TestCase("10.64.5.1")]
[TestCase("127.0.0.1")]
[TestCase("172.16.0.1")]
[TestCase("192.168.5.1")]
public void should_return_true_for_local_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeTrue();
}
[TestCase("1.2.3.4")]
[TestCase("172.55.0.1")]
[TestCase("192.55.0.1")]
public void should_return_false_for_public_ip_address(string ipAddress)
{
IPAddress.Parse(ipAddress).IsLocalAddress().Should().BeFalse();
}
}
}

View File

@ -84,6 +84,7 @@
<Compile Include="ExtensionTests\IEnumerableExtensionTests\ExceptByFixture.cs" /> <Compile Include="ExtensionTests\IEnumerableExtensionTests\ExceptByFixture.cs" />
<Compile Include="ExtensionTests\IEnumerableExtensionTests\IntersectByFixture.cs" /> <Compile Include="ExtensionTests\IEnumerableExtensionTests\IntersectByFixture.cs" />
<Compile Include="ExtensionTests\Int64ExtensionFixture.cs" /> <Compile Include="ExtensionTests\Int64ExtensionFixture.cs" />
<Compile Include="ExtensionTests\IPAddressExtensionsFixture.cs" />
<Compile Include="ExtensionTests\UrlExtensionsFixture.cs" /> <Compile Include="ExtensionTests\UrlExtensionsFixture.cs" />
<Compile Include="HashUtilFixture.cs" /> <Compile Include="HashUtilFixture.cs" />
<Compile Include="Http\HttpClientFixture.cs" /> <Compile Include="Http\HttpClientFixture.cs" />

View File

@ -0,0 +1,29 @@
using System.Net;
namespace NzbDrone.Common.Extensions
{
public static class IPAddressExtensions
{
public static bool IsLocalAddress(this IPAddress ipAddress)
{
if (ipAddress.ToString() == "::1")
{
return true;
}
byte[] bytes = ipAddress.GetAddressBytes();
switch (bytes[0])
{
case 10:
case 127:
return true;
case 172:
return bytes[1] < 32 && bytes[1] >= 16;
case 192:
return bytes[1] == 168;
default:
return false;
}
}
}
}

View File

@ -5,7 +5,6 @@ using System.Net;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Security;
namespace NzbDrone.Common.Http.Dispatchers namespace NzbDrone.Common.Http.Dispatchers
{ {
@ -75,11 +74,6 @@ namespace NzbDrone.Common.Http.Dispatchers
} }
catch (WebException e) catch (WebException e)
{ {
if (e.Status == WebExceptionStatus.SecureChannelFailure && OsInfo.IsWindows)
{
SecurityProtocolPolicy.DisableTls12();
}
httpWebResponse = (HttpWebResponse)e.Response; httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null) if (httpWebResponse == null)

View File

@ -102,6 +102,7 @@
<Compile Include="Disk\DestinationAlreadyExistsException.cs" /> <Compile Include="Disk\DestinationAlreadyExistsException.cs" />
<Compile Include="Exceptions\SonarrStartupException.cs" /> <Compile Include="Exceptions\SonarrStartupException.cs" />
<Compile Include="EnvironmentInfo\RuntimeMode.cs" /> <Compile Include="EnvironmentInfo\RuntimeMode.cs" />
<Compile Include="Extensions\IPAddressExtensions.cs" />
<Compile Include="Extensions\RegexExtensions.cs" /> <Compile Include="Extensions\RegexExtensions.cs" />
<Compile Include="Extensions\DictionaryExtensions.cs" /> <Compile Include="Extensions\DictionaryExtensions.cs" />
<Compile Include="Disk\OsPath.cs" /> <Compile Include="Disk\OsPath.cs" />
@ -216,8 +217,6 @@
<Compile Include="Properties\SharedAssemblyInfo.cs" /> <Compile Include="Properties\SharedAssemblyInfo.cs" />
<Compile Include="Reflection\ReflectionExtensions.cs" /> <Compile Include="Reflection\ReflectionExtensions.cs" />
<Compile Include="Extensions\ResourceExtensions.cs" /> <Compile Include="Extensions\ResourceExtensions.cs" />
<Compile Include="Security\SecurityProtocolPolicy.cs" />
<Compile Include="Security\X509CertificateValidationPolicy.cs" />
<Compile Include="Serializer\HttpUriConverter.cs" /> <Compile Include="Serializer\HttpUriConverter.cs" />
<Compile Include="Serializer\IntConverter.cs" /> <Compile Include="Serializer\IntConverter.cs" />
<Compile Include="Serializer\Json.cs" /> <Compile Include="Serializer\Json.cs" />

View File

@ -1,66 +0,0 @@
using System;
using System.Net;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Security
{
public static class SecurityProtocolPolicy
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(SecurityProtocolPolicy));
private const SecurityProtocolType Tls11 = (SecurityProtocolType)768;
private const SecurityProtocolType Tls12 = (SecurityProtocolType)3072;
public static void Register()
{
if (OsInfo.IsNotWindows)
{
// This was never meant to be used on mono, and will cause issues with mono 5 and higher if btls is enabled.
return;
}
try
{
// TODO: In v3 we should drop support for SSL3 because its very insecure. Only leaving it enabled because some people might rely on it.
var protocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls;
if (Enum.IsDefined(typeof(SecurityProtocolType), Tls11))
{
protocol |= Tls11;
}
// Enabling Tls1.2 invalidates certificates using md5, so we disable Tls12 on the fly if that happens.
if (Enum.IsDefined(typeof(SecurityProtocolType), Tls12))
{
protocol |= Tls12;
}
ServicePointManager.SecurityProtocol = protocol;
}
catch (Exception ex)
{
Logger.Debug(ex, "Failed to set TLS security protocol.");
}
}
public static void DisableTls12()
{
try
{
var protocol = ServicePointManager.SecurityProtocol;
if (protocol.HasFlag(Tls12))
{
Logger.Warn("Disabled Tls1.2 due to remote certificate error.");
ServicePointManager.SecurityProtocol = protocol & ~Tls12;
}
}
catch (Exception ex)
{
Logger.Debug(ex, "Failed to disable TLS 1.2 security protocol.");
}
}
}
}

View File

@ -1,44 +0,0 @@
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Security
{
public static class X509CertificateValidationPolicy
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(X509CertificateValidationPolicy));
public static void Register()
{
ServicePointManager.ServerCertificateValidationCallback = ShouldByPassValidationError;
}
private static bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var request = sender as HttpWebRequest;
if (request == null)
{
return true;
}
var req = sender as HttpWebRequest;
var cert2 = certificate as X509Certificate2;
if (cert2 != null && req != null && cert2.SignatureAlgorithm.FriendlyName == "md5RSA")
{
Logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", req.RequestUri.Authority);
}
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
Logger.Debug("Certificate validation for {0} failed. {1}", request.Address, sslPolicyErrors);
return true;
}
}
}

View File

@ -8,6 +8,7 @@ using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Security;
namespace NzbDrone.Core.Configuration namespace NzbDrone.Core.Configuration
{ {
@ -345,6 +346,9 @@ namespace NzbDrone.Core.Configuration
public int BackupRetention => GetValueInt("BackupRetention", 28); public int BackupRetention => GetValueInt("BackupRetention", 28);
public CertificateValidationType CertificateValidation =>
GetValueEnum("CertificateValidation", CertificateValidationType.Enabled);
private string GetValue(string key) private string GetValue(string key)
{ {
return GetValue(key, string.Empty); return GetValue(key, string.Empty);

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Security;
namespace NzbDrone.Core.Configuration namespace NzbDrone.Core.Configuration
{ {
@ -82,5 +83,7 @@ namespace NzbDrone.Core.Configuration
string BackupFolder { get; } string BackupFolder { get; }
int BackupInterval { get; } int BackupInterval { get; }
int BackupRetention { get; } int BackupRetention { get; }
CertificateValidationType CertificateValidation { get; }
} }
} }

View File

@ -4,7 +4,7 @@ using System.Text;
namespace NzbDrone.Core namespace NzbDrone.Core
{ {
public static class Security public static class Hashing
{ {
public static string SHA256Hash(this string input) public static string SHA256Hash(this string input)
{ {

View File

@ -1166,7 +1166,9 @@
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="RootFolders\UnmappedFolder.cs" /> <Compile Include="RootFolders\UnmappedFolder.cs" />
<Compile Include="Security.cs" /> <Compile Include="Hashing.cs" />
<Compile Include="Security\CertificateValidationType.cs" />
<Compile Include="Security\X509CertificateValidationPolicy.cs" />
<Compile Include="SeriesStats\SeasonStatistics.cs" /> <Compile Include="SeriesStats\SeasonStatistics.cs" />
<Compile Include="SeriesStats\SeriesStatistics.cs" /> <Compile Include="SeriesStats\SeriesStatistics.cs" />
<Compile Include="SeriesStats\SeriesStatisticsRepository.cs" /> <Compile Include="SeriesStats\SeriesStatisticsRepository.cs" />

View File

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Security
{
public enum CertificateValidationType
{
Enabled = 0,
DisabledForLocalAddresses = 1,
Disabled = 2
}
}

View File

@ -0,0 +1,72 @@
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Security
{
public interface IX509CertificateValidationPolicy
{
void Register();
}
public class X509CertificateValidationPolicy : IX509CertificateValidationPolicy
{
private readonly IConfigService _configService;
private readonly Logger _logger;
public X509CertificateValidationPolicy(IConfigService configService, Logger logger)
{
_configService = configService;
_logger = logger;
}
public void Register()
{
ServicePointManager.ServerCertificateValidationCallback = ShouldByPassValidationError;
}
private bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var request = sender as HttpWebRequest;
if (request == null)
{
return true;
}
var req = sender as HttpWebRequest;
var cert2 = certificate as X509Certificate2;
if (cert2 != null && req != null && cert2.SignatureAlgorithm.FriendlyName == "md5RSA")
{
_logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", req.RequestUri.Authority);
}
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
var host = Dns.GetHostEntry(req.Host);
var certificateValidation = _configService.CertificateValidation;
if (certificateValidation == CertificateValidationType.Disabled)
{
return true;
}
if (certificateValidation == CertificateValidationType.DisabledForLocalAddresses && host.AddressList.All(i => i.IsLocalAddress()))
{
return true;
}
_logger.Error("Certificate validation for {0} failed. {1}", request.Address, sslPolicyErrors);
return false;
}
}
}

View File

@ -7,9 +7,9 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Common.Security;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Security;
namespace NzbDrone.Host namespace NzbDrone.Host
{ {
@ -22,9 +22,6 @@ namespace NzbDrone.Host
{ {
try try
{ {
SecurityProtocolPolicy.Register();
X509CertificateValidationPolicy.Register();
Logger.Info("Starting Sonarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version); Logger.Info("Starting Sonarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version);
if (!PlatformValidation.IsValidate(userAlert)) if (!PlatformValidation.IsValidate(userAlert))
@ -39,6 +36,7 @@ namespace NzbDrone.Host
var appMode = GetApplicationMode(startupContext); var appMode = GetApplicationMode(startupContext);
Start(appMode, startupContext); Start(appMode, startupContext);
_container.Resolve<IX509CertificateValidationPolicy>().Register();
if (startCallback != null) if (startCallback != null)
{ {

View File

@ -7,7 +7,6 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Common.Security;
using NzbDrone.Update.UpdateEngine; using NzbDrone.Update.UpdateEngine;
namespace NzbDrone.Update namespace NzbDrone.Update
@ -30,9 +29,6 @@ namespace NzbDrone.Update
{ {
try try
{ {
SecurityProtocolPolicy.Register();
X509CertificateValidationPolicy.Register();
var startupContext = new StartupContext(args); var startupContext = new StartupContext(args);
NzbDroneLogger.Register(startupContext, true, true); NzbDroneLogger.Register(startupContext, true, true);

View File

@ -1,6 +1,7 @@
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Core.Update; using NzbDrone.Core.Update;
using Sonarr.Http.REST; using Sonarr.Http.REST;
@ -34,6 +35,7 @@ namespace Sonarr.Api.V3.Config
public string ProxyPassword { get; set; } public string ProxyPassword { get; set; }
public string ProxyBypassFilter { get; set; } public string ProxyBypassFilter { get; set; }
public bool ProxyBypassLocalAddresses { get; set; } public bool ProxyBypassLocalAddresses { get; set; }
public CertificateValidationType CertificateValidation { get; set; }
public string BackupFolder { get; set; } public string BackupFolder { get; set; }
public int BackupInterval { get; set; } public int BackupInterval { get; set; }
public int BackupRetention { get; set; } public int BackupRetention { get; set; }
@ -72,6 +74,7 @@ namespace Sonarr.Api.V3.Config
ProxyPassword = configService.ProxyPassword, ProxyPassword = configService.ProxyPassword,
ProxyBypassFilter = configService.ProxyBypassFilter, ProxyBypassFilter = configService.ProxyBypassFilter,
ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses, ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses,
CertificateValidation = configService.CertificateValidation,
BackupFolder = configService.BackupFolder, BackupFolder = configService.BackupFolder,
BackupInterval = configService.BackupInterval, BackupInterval = configService.BackupInterval,
BackupRetention = configService.BackupRetention BackupRetention = configService.BackupRetention