Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts, scan de code-barres, consultation hors-ligne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.7 KiB
C#
116 lines
4.7 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
|
|
namespace MaBibli.Api.Services.Couvertures;
|
|
|
|
/// <summary>
|
|
/// Garde-fou SSRF : n'autorise la connexion qu'à des adresses IP publiquement routables.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// ⚠️ <b>Ce garde est la raison pour laquelle le proxy de couvertures est acceptable.</b> Le
|
|
/// service .NET écoute sur <c>127.0.0.1</c> et vit sur un serveur YunoHost qui héberge d'autres
|
|
/// applications, souvent sur des ports locaux. Un proxy qui irait chercher n'importe quelle URL
|
|
/// permettrait de faire lire au serveur ce que le client ne peut pas atteindre lui-même —
|
|
/// c'est-à-dire exactement ce que la contrainte « écouter sur 127.0.0.1 uniquement » cherche à
|
|
/// empêcher.
|
|
/// </para>
|
|
/// <para>
|
|
/// ⚠️ <b>La vérification est faite au moment de CONNECTER, pas avant la requête.</b> Résoudre le
|
|
/// nom d'abord puis laisser <c>HttpClient</c> résoudre à nouveau laisserait passer un
|
|
/// <i>DNS rebinding</i> : un nom qui répond une adresse publique à la vérification et
|
|
/// <c>127.0.0.1</c> à la connexion. Ici, la socket se connecte <b>aux adresses déjà validées</b>,
|
|
/// et à aucune autre.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class GardeAdresses
|
|
{
|
|
/// <summary>
|
|
/// Rappel de connexion pour <see cref="SocketsHttpHandler.ConnectCallback"/>.
|
|
/// </summary>
|
|
public static async ValueTask<Stream> ConnecterAsync(
|
|
SocketsHttpConnectionContext contexte, CancellationToken ct)
|
|
{
|
|
var adresses = await Dns.GetHostAddressesAsync(contexte.DnsEndPoint.Host, ct);
|
|
var permises = adresses.Where(EstPubliquementRoutable).ToArray();
|
|
|
|
if (permises.Length == 0)
|
|
{
|
|
// Message volontairement muet sur ce qui a été résolu : l'appelant n'a pas à
|
|
// apprendre la topologie du réseau du serveur.
|
|
throw new HttpRequestException("Adresse non autorisée.");
|
|
}
|
|
|
|
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
|
|
|
|
try
|
|
{
|
|
await socket.ConnectAsync(permises, contexte.DnsEndPoint.Port, ct);
|
|
return new NetworkStream(socket, ownsSocket: true);
|
|
}
|
|
catch
|
|
{
|
|
socket.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vrai si l'adresse est publiquement routable, c'est-à-dire hors de tout ce qui pourrait
|
|
/// désigner le serveur lui-même ou son réseau local.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Liste <b>négative</b>, contrairement à la plupart des filtres du projet : ici, laisser
|
|
/// passer une plage oubliée est une faille, alors qu'en écarter une de trop ne coûte qu'une
|
|
/// couverture non mise en cache.
|
|
/// </remarks>
|
|
public static bool EstPubliquementRoutable(IPAddress adresse)
|
|
{
|
|
// Une adresse IPv4 encapsulée en IPv6 (::ffff:127.0.0.1) doit être jugée sur son IPv4,
|
|
// sans quoi le bouclage passerait déguisé.
|
|
if (adresse.IsIPv4MappedToIPv6)
|
|
{
|
|
adresse = adresse.MapToIPv4();
|
|
}
|
|
|
|
if (IPAddress.IsLoopback(adresse))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (adresse.AddressFamily == AddressFamily.InterNetwork)
|
|
{
|
|
var o = adresse.GetAddressBytes();
|
|
|
|
return o[0] switch
|
|
{
|
|
0 => false, // 0.0.0.0/8 « ce réseau »
|
|
10 => false, // 10.0.0.0/8 privé
|
|
127 => false, // bouclage (déjà couvert, par sûreté)
|
|
169 when o[1] == 254 => false, // 169.254.0.0/16 lien-local (métadonnées cloud)
|
|
172 when o[1] >= 16 && o[1] <= 31 => false, // 172.16.0.0/12 privé
|
|
192 when o[1] == 168 => false, // 192.168.0.0/16 privé
|
|
192 when o[1] == 0 && o[2] == 0 => false, // 192.0.0.0/24 usages protocolaires
|
|
100 when o[1] >= 64 && o[1] <= 127 => false, // 100.64.0.0/10 CGNAT
|
|
>= 224 => false, // multicast et réservé
|
|
_ => true,
|
|
};
|
|
}
|
|
|
|
if (adresse.AddressFamily == AddressFamily.InterNetworkV6)
|
|
{
|
|
if (adresse.IsIPv6LinkLocal || adresse.IsIPv6SiteLocal || adresse.IsIPv6Multicast
|
|
|| adresse.Equals(IPAddress.IPv6Any))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// fc00::/7 : adresses locales uniques (l'équivalent IPv6 du privé).
|
|
return (adresse.GetAddressBytes()[0] & 0xFE) != 0xFC;
|
|
}
|
|
|
|
// Ni IPv4 ni IPv6 : on ne sait pas juger, donc on refuse.
|
|
return false;
|
|
}
|
|
}
|