Renforcer la sécurité du client HTTP dans sgcWebSockets | eSeGeCe Blog

Renforcer la sécurité du client HTTP dans sgcWebSockets

· Composants

Nous avons déjà écrit sur le renforcement de la sécurité du serveur HTTP. Cet article est l'autre moitié de l'histoire. Un client est normalement considéré comme le côté digne de confiance de la conversation, mais il ne l'est pas : il envoie vos clés d'API, il suit n'importe quelle redirection renvoyée par l'hôte distant, il lit la réponse entière en mémoire, et il stocke tous les cookies que la réponse lui demande de stocker. Un point de terminaison hostile, ou simplement compromis, peut retourner ces quatre comportements contre vous.

sgcWebSockets 2026.7 ajoute un ensemble d'interrupteurs côté client qui referment chacun de ces trous. Tous sont désactivés par défaut, de sorte que la mise à jour ne change rien dans une application existante tant que vous ne décidez pas de les activer. Ils sont disponibles sur le client HTTP de bas niveau, sur tous les clients REST et IA prêts à l'emploi, ainsi que sur les clients HTTP/2 et HTTP/3.

Des identifiants qui suivent une redirection

La fuite classique côté client. Vous appelez une API avec un en-tête Authorization, le serveur répond avec un 302 pointant vers un autre hôte, et le client répète docilement la requête là-bas, en-tête et cookies compris. Votre jeton bearer vient d'être remis à une machine avec laquelle vous n'aviez jamais eu l'intention de parler. La redirection n'a même pas besoin d'être malveillante, il suffit d'un attaquant capable d'influencer une redirection ouverte sur l'hôte légitime.

StripAuthOnCrossHostRedirect supprime les en-têtes Authorization et Cookie dès qu'une redirection quitte l'hôte d'origine. La requête suit toujours la redirection, elle arrive simplement sans vos identifiants.

uses
  sgcHTTP_Client;

var
  oHTTP: TsgcIdHTTP;
begin
  oHTTP := TsgcIdHTTP.Create(nil);
  try
    oHTTP.StripAuthOnCrossHostRedirect := True;

    oHTTP.Request.CustomHeaders.Values['Authorization'] := 'Bearer ' + vToken;
    vResponse := oHTTP.Get('https://api.example.com/v1/account');
  finally
    oHTTP.Free;
  end;
end;

Des redirections qui rétrogradent vers du HTTP en clair

Le deuxième problème de redirection. Vous démarrez sur https://, le serveur répond avec un Location qui commence par http://, et le client répète joyeusement la requête en clair. Tout ce qui se trouve sur le chemin peut désormais lire le corps et réécrire la réponse. C'est exactement la rétrogradation que HSTS existe pour empêcher dans les navigateurs, et jusqu'à présent le client HTTP n'avait aucun équivalent.

NoInsecureRedirect refuse une redirection qui ferait passer la requête de HTTPS à HTTP. La redirection est rejetée au lieu d'être suivie.

oHTTP.NoInsecureRedirect := True;

// A 302 from https://api.example.com to http://api.example.com
// is now blocked instead of silently downgraded.
vResponse := oHTTP.Get('https://api.example.com/v1/data');

Les deux interrupteurs de redirection sont indépendants, et en pratique vous voulez les deux. Activez le premier et une redirection vers un autre hôte n'emporte plus votre jeton. Activez le second et une redirection ne peut plus vous priver de votre sécurité de transport.

Des réponses sans limite de taille

Un client HTTP par défaut lit tout ce que le serveur envoie. Si le serveur envoie un corps de réponse de dix gigaoctets, ou une réponse en blocs où un seul bloc annonce une longueur de deux gigaoctets, le client tente de lui allouer de la place. C'est un déni de service en une ligne contre votre propre processus, et il n'exige pas de l'attaquant qu'il casse quoi que ce soit, seulement qu'il réponde à votre requête avec plus de données que vous n'en attendiez.

Deux plafonds referment cela. MaxResponseSize limite la taille totale d'un corps de réponse, et MaxChunkSize limite un bloc unique d'une réponse en blocs. Tous deux acceptent zéro comme « illimité », qui est la valeur par défaut, donc rien ne change tant que vous ne les définissez pas. Lorsqu'une limite est dépassée, la lecture est interrompue et une exception est levée, le client ne continue pas à faire grossir le tampon.

uses
  sgcHTTP_Client;

var
  oHTTP: TsgcIdHTTP;
begin
  oHTTP := TsgcIdHTTP.Create(nil);
  try
    oHTTP.MaxResponseSize := 10 * 1024 * 1024;  // 10 MB total, 0 = unlimited
    oHTTP.MaxChunkSize    := 1024 * 1024;       // 1 MB per chunk, 0 = unlimited

    vResponse := oHTTP.Get('https://api.example.com/v1/report');
  finally
    oHTTP.Free;
  end;
end;

Choisissez un nombre qui reflète ce que le point de terminaison est réellement censé renvoyer. Si vous appelez une API JSON qui répond avec quelques kilooctets, un plafond de quelques mégaoctets est généreux et transforme tout de même une lecture sans limite en une lecture bornée.

Des cookies revendiqués pour tout un TLD

Une réponse peut envoyer un cookie avec un attribut Domain. Rien dans le format de transmission ne l'empêche de revendiquer Domain=com ou Domain=co.uk. Un client qui accepte cela est désormais prêt à envoyer le cookie à tous les hôtes situés sous ce suffixe, ce qui est l'équivalent, pour les cookies, de distribuer un passe-partout. Les navigateurs refusent cela depuis des années grâce à la Public Suffix List.

RejectPublicSuffixCookies apporte la même règle au composant. Un cookie dont le domaine est un suffixe public plutôt qu'un vrai domaine enregistrable est écarté. Notez que son activation active aussi la gestion des cookies sur le client, car c'est un pot à cookies qu'il filtre.

oHTTP.RejectPublicSuffixCookies := True;

// Set-Cookie: session=abc; Domain=co.uk   ->  rejected
// Set-Cookie: session=abc; Domain=example.co.uk  ->  accepted

2026.7 corrige également deux bogues liés aux cookies et aux redirections qui étaient discrètement erronés auparavant. Les en-têtes Location relatifs sont maintenant résolus correctement en URL absolues, une redirection 307 ou 308 préserve désormais la méthode et le corps de la requête comme la spécification l'exige, et un cookie dont la date d'expiration ne peut pas être analysée est traité comme un cookie de session au lieu d'être jeté. Max-Age l'emporte toujours sur Expires lorsque les deux sont présents.

Envoyer un gros corps : Expect: 100-continue et requêtes en blocs

Toutes les nouvelles options ne concernent pas les attaques, deux d'entre elles visent à ne pas gaspiller de bande passante. Si vous envoyez en POST un gros corps vers un point de terminaison qui va le rejeter (mauvais identifiants, type de contenu incorrect, quota déjà épuisé), le comportement par défaut consiste à pousser d'abord le corps entier sur le réseau, puis à lire le 401 ensuite.

UseExpect100Continue envoie les en-têtes de la requête avec un Expect: 100-continue et attend que le serveur indique qu'il est disposé à recevoir le corps. Si le serveur répond plutôt par un statut final, vous n'envoyez jamais la charge utile. Si aucun feu vert n'arrive, le corps est envoyé quand même, de sorte qu'un serveur qui ignore l'en-tête fonctionne toujours.

UseChunkedTransferEncoding couvre le cas inverse, un envoi dont vous ne connaissez pas la taille à l'avance. Le corps est envoyé en blocs au lieu de l'être avec un Content-Length fixe, ce qui vous permet de le diffuser depuis un générateur, un compresseur ou un tube sans avoir à tout mettre en tampon au préalable.

uses
  Classes, sgcHTTP_Client;

var
  oHTTP: TsgcIdHTTP;
  oBody: TStream;
begin
  oHTTP := TsgcIdHTTP.Create(nil);
  try
    oHTTP.UseExpect100Continue       := True;   // ask before uploading
    oHTTP.UseChunkedTransferEncoding := True;   // size not known up front

    oBody := TFileStream.Create('large_upload.bin', fmOpenRead or fmShareDenyWrite);
    try
      vResponse := oHTTP.Post('https://api.example.com/v1/upload', oBody);
    finally
      oBody.Free;
    end;
  finally
    oHTTP.Free;
  end;
end;

Les mêmes interrupteurs sur les clients d'API prêts à l'emploi

La partie intéressante est l'endroit où vivent ces propriétés. Elles sont réexposées sur TsgcHTTPAPI_client, la classe de base de tous les clients REST prêts à l'emploi de la bibliothèque. Cela signifie que vous les définissez directement sur le composant que vous utilisez déjà, sans avoir à descendre jusqu'à l'objet HTTP de bas niveau situé en dessous.

Cela couvre les clients IA et LLM (OpenAI, Anthropic, Gemini, DeepSeek, Grok, Mistral, Ollama), le client OAuth2, Google Cloud, WhatsApp, AWS SQS et WebPush, ainsi que les unités API Amazon AWS et Google Cloud.

uses
  sgcHTTP_API_OpenAI;

var
  oOpenAI: TsgcHTTP_API_OpenAI;
begin
  oOpenAI := TsgcHTTP_API_OpenAI.Create(nil);
  try
    oOpenAI.StripAuthOnCrossHostRedirect := True;   // never leak the API key
    oOpenAI.NoInsecureRedirect           := True;   // never fall back to http
    oOpenAI.RejectPublicSuffixCookies    := True;
    oOpenAI.MaxResponseSize              := 32 * 1024 * 1024;
    oOpenAI.MaxChunkSize                 := 1024 * 1024;

    // ... use the client as usual
  finally
    oOpenAI.Free;
  end;
end;

Une clé d'API envoyée à un fournisseur de LLM est exactement le genre d'identifiant que vous ne voulez pas voir suivre une redirection hors du domaine du fournisseur, c'est donc l'endroit où activer ces interrupteurs ne vous coûte rien et vous rapporte beaucoup.

HTTP/2 et HTTP/3

Le client HTTP/2 bénéficie des mêmes protections via son objet d'options, TsgcWSHTTP2Client_Options, et le client HTTP/3 expose directement le plafond de réponse.

uses
  sgcHTTP;

var
  oHTTP2: TsgcHTTP2Client;
begin
  oHTTP2 := TsgcHTTP2Client.Create(nil);
  try
    oHTTP2.HTTP2Options.StripAuthOnCrossHostRedirect := True;
    oHTTP2.HTTP2Options.NoInsecureRedirect           := True;
    oHTTP2.HTTP2Options.MaxResponseSize              := 10 * 1024 * 1024;
  finally
    oHTTP2.Free;
  end;
end;

// HTTP/3
oHTTP3.MaxResponseSize := 10 * 1024 * 1024;

Une ligne pour une base TLS sécurisée

Rien de tout ce qui précède ne sert si la couche TLS ne vérifie rien. sgcWebSockets 2026.7 ajoute un préréglage pour cela. Tous les objets d'options TLS de la bibliothèque, sur les composants WebSocket comme sur les composants HTTP, publient une propriété Preset de type TsgcTLSPreset. Sa valeur par défaut est tlspCustom, qui conserve ce que vous avez configuré à la main.

La définir à tlspSecureDefaults active la vérification du certificat, TLS 1.2 ou supérieur, et la vérification du nom d'hôte en une seule ligne.

uses
  sgcWebSocket_Types;

begin
  oHTTP.TLSOptions.Preset := tlspSecureDefaults;
  // certificate verification on, TLS 1.2+, hostname verification on
end;

Combinez le préréglage avec les interrupteurs de redirection et les plafonds de réponse, et vous obtenez un client qui vérifie à qui il parle, refuse de rétrograder, ne remet pas ses identifiants à un tiers, et ne peut pas être amené à allouer de la mémoire sans limite.

.NET

L'édition managée fournit les mêmes protections dans 2026.7. La suppression des identifiants lors des redirections, le blocage du passage de HTTPS à HTTP, les plafonds par bloc et sur la réponse totale, le rejet des cookies de suffixe public, Expect: 100-continue et les corps de requête en blocs sont tous présents, y compris sur l'ensemble des clients HTTP/2. Mêmes noms, casse C#.

client.StripAuthOnCrossHostRedirect = true;
client.NoInsecureRedirect           = true;
client.RejectPublicSuffixCookies    = true;
client.MaxResponseSize              = 10 * 1024 * 1024;
client.MaxChunkSize                 = 1024 * 1024;

Disponibilité

Les options de renforcement du client HTTP sont disponibles dans sgcWebSockets 2026.7 sur Delphi 7 à 13 et C++Builder (Win32/Win64, Linux64, macOS, Android et iOS), ainsi que dans l'édition .NET. Tout ce qui est décrit ici est optionnel et désactivé par défaut, la mise à jour se fait donc sans aucune adaptation.

Les clients disposant d'un abonnement actif peuvent télécharger la nouvelle version depuis l'espace client. Les utilisateurs de la version d'essai peuvent récupérer l'installateur mis à jour sur esegece.com/products/websockets/download.

Des questions, des remarques, ou besoin d'aide pour décider lesquels de ces interrupteurs votre application devrait activer ? Contactez-nous, vous recevrez une réponse des personnes qui ont écrit le code.