[backend/core] Don't allow connections to local addresses by default (ISH-330, ISH-331)

This commit is contained in:
Laura Hausmann 2024-05-17 14:08:05 +02:00
parent a1120ac1e1
commit 849ecd9841
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
5 changed files with 94 additions and 2 deletions

View file

@ -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;

View 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;
}
}

View file

@ -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;

View file

@ -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

View file

@ -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>