[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 AttachLdSignatures { 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 Enums.Registrations Registrations { get; init; } = Enums.Registrations.Closed;
|
||||
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
|
||||
{
|
||||
private static readonly FastFallback FastFallbackHandler = new();
|
||||
|
||||
private static readonly HttpMessageHandler InnerHandler = new SocketsHttpHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
ConnectCallback = new FastFallback().ConnectCallback,
|
||||
ConnectCallback = FastFallbackHandler.ConnectCallback,
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(60)
|
||||
};
|
||||
|
||||
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);
|
||||
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
|
||||
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)
|
||||
{
|
||||
var sortedRecords = await GetSortedAddresses(context.DnsEndPoint.Host, token);
|
||||
|
@ -40,6 +58,30 @@ public class CustomHttpClient : HttpClient
|
|||
{
|
||||
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);
|
||||
|
||||
var task = AttemptConnection(record, context.DnsEndPoint.Port, linkedToken.Token, delayCts.Token);
|
||||
|
@ -50,6 +92,9 @@ public class CustomHttpClient : HttpClient
|
|||
delayCts = nextDelayCts;
|
||||
}
|
||||
|
||||
if (tasks.Count == 0)
|
||||
throw new Exception($"Can't connect to {context.DnsEndPoint.Host}: no candidate addresses remaining");
|
||||
|
||||
NetworkStream? stream = null;
|
||||
Exception? lastException = null;
|
||||
|
||||
|
|
|
@ -28,6 +28,15 @@ AttachLdSignatures = false
|
|||
;; Whether to accept activities signed using LD signatures
|
||||
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
|
||||
;; Options: [None, Basic, Full]
|
||||
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/=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/=IP/@EntryIndexedValue">IP</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_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
|
|
Loading…
Add table
Reference in a new issue