[backend/core] Don't allow connections to local addresses by default (ISH-330, ISH-331)
This commit is contained in:
parent
a1120ac1e1
commit
849ecd9841
5 changed files with 94 additions and 2 deletions
|
@ -58,6 +58,9 @@ public sealed class Config
|
||||||
public bool AuthorizedFetch { get; init; } = true;
|
public bool AuthorizedFetch { get; init; } = true;
|
||||||
public bool AttachLdSignatures { get; init; } = false;
|
public bool AttachLdSignatures { get; init; } = false;
|
||||||
public bool AcceptLdSignatures { get; init; } = false;
|
public bool AcceptLdSignatures { get; init; } = false;
|
||||||
|
public bool AllowLoopback { get; init; } = false;
|
||||||
|
public bool AllowLocalIPv6 { get; init; } = false;
|
||||||
|
public bool AllowLocalIPv4 { get; init; } = false;
|
||||||
public ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic;
|
public ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic;
|
||||||
public Enums.Registrations Registrations { get; init; } = Enums.Registrations.Closed;
|
public Enums.Registrations Registrations { get; init; } = Enums.Registrations.Closed;
|
||||||
public Enums.FederationMode FederationMode { get; init; } = Enums.FederationMode.BlockList;
|
public Enums.FederationMode FederationMode { get; init; } = Enums.FederationMode.BlockList;
|
||||||
|
|
34
Iceshrimp.Backend/Core/Extensions/IPAddressExtensions.cs
Normal file
34
Iceshrimp.Backend/Core/Extensions/IPAddressExtensions.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
|
||||||
|
public static class IPAddressExtensions
|
||||||
|
{
|
||||||
|
public static bool IsLoopback(this IPAddress address) => IPAddress.IsLoopback(address);
|
||||||
|
|
||||||
|
public static bool IsLocalIPv6(this IPAddress address) => address.AddressFamily == AddressFamily.InterNetworkV6 &&
|
||||||
|
(address.IsIPv6LinkLocal ||
|
||||||
|
address.IsIPv6SiteLocal ||
|
||||||
|
address.IsIPv6UniqueLocal);
|
||||||
|
|
||||||
|
public static bool IsLocalIPv4(this IPAddress address) => address.AddressFamily == AddressFamily.InterNetwork &&
|
||||||
|
IsPrivateIPv4(address.GetAddressBytes());
|
||||||
|
|
||||||
|
private static bool IsPrivateIPv4(byte[] ipv4Bytes)
|
||||||
|
{
|
||||||
|
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||||
|
|
||||||
|
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||||
|
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||||
|
|
||||||
|
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||||
|
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||||
|
|
||||||
|
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||||
|
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||||
|
|
||||||
|
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||||
|
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,25 +9,43 @@ namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
public class CustomHttpClient : HttpClient
|
public class CustomHttpClient : HttpClient
|
||||||
{
|
{
|
||||||
|
private static readonly FastFallback FastFallbackHandler = new();
|
||||||
|
|
||||||
private static readonly HttpMessageHandler InnerHandler = new SocketsHttpHandler
|
private static readonly HttpMessageHandler InnerHandler = new SocketsHttpHandler
|
||||||
{
|
{
|
||||||
AutomaticDecompression = DecompressionMethods.All,
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
ConnectCallback = new FastFallback().ConnectCallback,
|
ConnectCallback = FastFallbackHandler.ConnectCallback,
|
||||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
|
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
|
||||||
PooledConnectionLifetime = TimeSpan.FromMinutes(60)
|
PooledConnectionLifetime = TimeSpan.FromMinutes(60)
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly HttpMessageHandler Handler = new RedirectHandler(InnerHandler);
|
private static readonly HttpMessageHandler Handler = new RedirectHandler(InnerHandler);
|
||||||
|
|
||||||
public CustomHttpClient(IOptions<Config.InstanceSection> options) : base(Handler)
|
public CustomHttpClient(
|
||||||
|
IOptions<Config.InstanceSection> options,
|
||||||
|
IOptionsMonitor<Config.SecuritySection> security,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
) : base(Handler)
|
||||||
{
|
{
|
||||||
|
// Configure HTTP client options
|
||||||
DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", options.Value.UserAgent);
|
DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", options.Value.UserAgent);
|
||||||
Timeout = TimeSpan.FromSeconds(30);
|
Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
// Configure FastFallback
|
||||||
|
FastFallbackHandler.Logger = loggerFactory.CreateLogger<FastFallback>();
|
||||||
|
FastFallbackHandler.Security = security;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapted from https://github.com/KazWolfe/Dalamud/blob/767cc49ecb80e29dbdda2fa8329d3c3341c964fe/Dalamud/Networking/Http/HappyEyeballsCallback.cs
|
// Adapted from https://github.com/KazWolfe/Dalamud/blob/767cc49ecb80e29dbdda2fa8329d3c3341c964fe/Dalamud/Networking/Http/HappyEyeballsCallback.cs
|
||||||
private class FastFallback(int connectionBackoff = 75)
|
private class FastFallback(int connectionBackoff = 75)
|
||||||
{
|
{
|
||||||
|
public ILogger<FastFallback>? Logger { private get; set; }
|
||||||
|
public IOptionsMonitor<Config.SecuritySection>? Security { private get; set; }
|
||||||
|
|
||||||
|
private bool AllowLoopback => Security?.CurrentValue.AllowLoopback ?? false;
|
||||||
|
private bool AllowLocalIPv4 => Security?.CurrentValue.AllowLocalIPv4 ?? false;
|
||||||
|
private bool AllowLocalIPv6 => Security?.CurrentValue.AllowLocalIPv6 ?? false;
|
||||||
|
|
||||||
public async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken token)
|
public async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken token)
|
||||||
{
|
{
|
||||||
var sortedRecords = await GetSortedAddresses(context.DnsEndPoint.Host, token);
|
var sortedRecords = await GetSortedAddresses(context.DnsEndPoint.Host, token);
|
||||||
|
@ -40,6 +58,30 @@ public class CustomHttpClient : HttpClient
|
||||||
{
|
{
|
||||||
var record = sortedRecords[i];
|
var record = sortedRecords[i];
|
||||||
|
|
||||||
|
if (record.IsIPv4MappedToIPv6)
|
||||||
|
record = record.MapToIPv4();
|
||||||
|
|
||||||
|
if (!AllowLoopback && record.IsLoopback())
|
||||||
|
{
|
||||||
|
Logger?.LogWarning("Refusing to connect to loopback address {address} due to possible SSRF",
|
||||||
|
record.ToString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AllowLocalIPv6 && record.IsLocalIPv6())
|
||||||
|
{
|
||||||
|
Logger?.LogWarning("Refusing to connect to local IPv6 address {address} due to possible SSRF",
|
||||||
|
record.ToString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AllowLocalIPv4 && record.IsLocalIPv4())
|
||||||
|
{
|
||||||
|
Logger?.LogWarning("Refusing to connect to local IPv4 address {address} due to possible SSRF",
|
||||||
|
record.ToString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
delayCts.CancelAfter(connectionBackoff * i);
|
delayCts.CancelAfter(connectionBackoff * i);
|
||||||
|
|
||||||
var task = AttemptConnection(record, context.DnsEndPoint.Port, linkedToken.Token, delayCts.Token);
|
var task = AttemptConnection(record, context.DnsEndPoint.Port, linkedToken.Token, delayCts.Token);
|
||||||
|
@ -50,6 +92,9 @@ public class CustomHttpClient : HttpClient
|
||||||
delayCts = nextDelayCts;
|
delayCts = nextDelayCts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tasks.Count == 0)
|
||||||
|
throw new Exception($"Can't connect to {context.DnsEndPoint.Host}: no candidate addresses remaining");
|
||||||
|
|
||||||
NetworkStream? stream = null;
|
NetworkStream? stream = null;
|
||||||
Exception? lastException = null;
|
Exception? lastException = null;
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,15 @@ AttachLdSignatures = false
|
||||||
;; Whether to accept activities signed using LD signatures
|
;; Whether to accept activities signed using LD signatures
|
||||||
AcceptLdSignatures = false
|
AcceptLdSignatures = false
|
||||||
|
|
||||||
|
;; Whether to allow requests to IPv4 & IPv6 loopback addresses
|
||||||
|
AllowLoopback = false
|
||||||
|
|
||||||
|
;; Whether to allow requests to local IPv4 addresses (RFC1918, link-local)
|
||||||
|
AllowLocalIPv4 = false
|
||||||
|
|
||||||
|
;; Whether to allow requests to local IPv6 addresses (RFC3513, ULA, link-local)
|
||||||
|
AllowLocalIPv6 = false
|
||||||
|
|
||||||
;; The level of detail in API error responses
|
;; The level of detail in API error responses
|
||||||
;; Options: [None, Basic, Full]
|
;; Options: [None, Basic, Full]
|
||||||
ExceptionVerbosity = Basic
|
ExceptionVerbosity = Basic
|
||||||
|
|
|
@ -359,6 +359,7 @@
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AP/@EntryIndexedValue">AP</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AP/@EntryIndexedValue">AP</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AS/@EntryIndexedValue">AS</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AS/@EntryIndexedValue">AS</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FF/@EntryIndexedValue">FF</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FF/@EntryIndexedValue">FF</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue">IP</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LD/@EntryIndexedValue">LD</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LD/@EntryIndexedValue">LD</s:String>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|
Loading…
Add table
Reference in a new issue