Blindar el cliente HTTP en sgcWebSockets | Blog de eSeGeCe

Blindar el cliente HTTP en sgcWebSockets

· Componentes

Ya escribimos antes sobre el blindaje del servidor HTTP. Este artículo es la otra mitad de la historia. Normalmente el cliente se trata como el lado de confianza de la conversación, pero no lo es: envía tus claves de API, sigue cualquier redirección que devuelva el host remoto, lee la respuesta entera en memoria y guarda todas las cookies que la respuesta le pida guardar. Un extremo hostil, o simplemente comprometido, puede volver esos cuatro comportamientos en tu contra.

sgcWebSockets 2026.7 añade un conjunto de interruptores en el lado del cliente que cierran cada uno de esos agujeros. Todos ellos están desactivados por defecto, así que actualizar no cambia nada en una aplicación existente hasta que decides activarlos. Están disponibles en el cliente HTTP de bajo nivel, en todos los clientes REST y de IA ya preparados, y en los clientes HTTP/2 y HTTP/3.

Credenciales que siguen una redirección

La fuga clásica del lado del cliente. Llamas a una API con una cabecera Authorization, el servidor responde con un 302 que apunta a un host distinto, y el cliente repite obedientemente la petición allí, cabecera y cookies incluidas. Tu token de portador acaba de ser entregado a una máquina con la que nunca quisiste hablar. La redirección ni siquiera necesita ser maliciosa, basta con un atacante capaz de influir en una redirección abierta del host legítimo.

StripAuthOnCrossHostRedirect elimina las cabeceras Authorization y Cookie en cuanto una redirección abandona el host original. La petición sigue la redirección igualmente, sólo que llega sin tus credenciales.

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;

Redirecciones que degradan a HTTP en claro

El segundo problema de las redirecciones. Empiezas en https://, el servidor responde con un Location que empieza por http://, y el cliente repite tan contento la petición en texto plano. Cualquiera en el camino puede ahora leer el cuerpo y reescribir la respuesta. Es exactamente la degradación que HSTS existe para evitar en los navegadores, y hasta ahora el cliente HTTP no tenía equivalente.

NoInsecureRedirect rechaza una redirección que llevaría la petición de HTTPS a HTTP. La redirección se rechaza en vez de seguirse.

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');

Los dos interruptores de redirección son independientes, y en la práctica quieres los dos. Activa el primero y una redirección a otro host deja de llevarse tu token. Activa el segundo y una redirección ya no puede quitarte la seguridad del transporte.

Respuestas sin límite de tamaño

Un cliente HTTP por defecto lee todo lo que el servidor envíe. Si el servidor manda un cuerpo de respuesta de diez gigabytes, o una respuesta fragmentada donde un solo fragmento anuncia una longitud de dos gigabytes, el cliente intenta reservar sitio para él. Eso es una denegación de servicio de una línea contra tu propio proceso, y no exige que el atacante rompa nada, sólo que responda a tu petición con más datos de los que esperabas.

Dos límites cierran esto. MaxResponseSize acota el tamaño total del cuerpo de una respuesta, y MaxChunkSize acota un único fragmento de una respuesta fragmentada. Ambos toman el cero como "sin límite", que es el valor por defecto, así que nada cambia hasta que los configuras. Cuando se supera un límite, la lectura se aborta y se lanza una excepción, el cliente no sigue haciendo crecer el búfer.

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;

Elige un número que refleje lo que se supone que el extremo debe devolver realmente. Si estás llamando a una API JSON que responde con unos pocos kilobytes, un límite de unos pocos megabytes es generoso y aun así convierte una lectura sin límite en una acotada.

Cookies reclamadas para todo un TLD

Una respuesta puede enviar una cookie con un atributo Domain. Nada en el formato del protocolo le impide reclamar Domain=com o Domain=co.uk. Un cliente que acepte eso está ahora dispuesto a enviar la cookie a todos los hosts bajo ese sufijo, que es el equivalente en cookies a repartir una llave maestra. Los navegadores llevan años rechazando esto usando la Public Suffix List.

RejectPublicSuffixCookies trae la misma regla al componente. Una cookie cuyo dominio es un sufijo público en lugar de un dominio registrable real se descarta. Ten en cuenta que activarlo también activa la gestión de cookies en el cliente, porque el tarro de cookies es lo que filtra.

oHTTP.RejectPublicSuffixCookies := True;

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

2026.7 también corrige dos errores relacionados con cookies y redirecciones que estaban silenciosamente mal. Las cabeceras Location relativas ahora se resuelven correctamente en URLs absolutas, una redirección 307 o 308 ahora conserva el método y el cuerpo de la petición tal como exige la especificación, y una cookie cuya fecha de caducidad no se puede interpretar se trata como una cookie de sesión en vez de descartarse. Max-Age sigue teniendo prioridad sobre Expires cuando ambos están presentes.

Subir un cuerpo grande: Expect: 100-continue y peticiones fragmentadas

No todas las opciones nuevas tienen que ver con ataques, dos de ellas tienen que ver con no malgastar ancho de banda. Si haces un POST de un cuerpo grande a un extremo que lo va a rechazar (credenciales incorrectas, tipo de contenido equivocado, una cuota ya agotada), el comportamiento por defecto es empujar el cuerpo entero por el cable primero y leer el 401 después.

UseExpect100Continue envía las cabeceras de la petición con un Expect: 100-continue y espera a que el servidor diga que está dispuesto a recibir el cuerpo. Si el servidor responde con un estado final en su lugar, nunca envías la carga útil. Si no llega ninguna autorización, el cuerpo se envía igualmente, así que un servidor que ignore la cabecera sigue funcionando.

UseChunkedTransferEncoding cubre el caso contrario, una subida cuyo tamaño no conoces de antemano. El cuerpo se envía en fragmentos en lugar de con un Content-Length fijo, así que puedes transmitirlo desde un generador, un compresor o una tubería sin almacenarlo entero en memoria antes.

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;

Los mismos interruptores en los clientes de API ya preparados

La parte interesante es dónde viven estas propiedades. Se vuelven a exponer en TsgcHTTPAPI_client, la clase base de todos los clientes REST ya preparados de la librería. Eso significa que las configuras directamente en el componente que ya estás usando, sin tener que bajar al objeto HTTP de bajo nivel que hay debajo.

Eso cubre los clientes de IA y LLM (OpenAI, Anthropic, Gemini, DeepSeek, Grok, Mistral, Ollama), el cliente OAuth2, Google Cloud, WhatsApp, AWS SQS y WebPush, y las unidades de API de Amazon AWS y 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;

Una clave de API enviada a un proveedor de LLM es exactamente el tipo de credencial que no quieres que siga una redirección fuera del dominio del proveedor, así que este es el sitio donde activar los interruptores no te cuesta nada y te aporta mucho.

HTTP/2 y HTTP/3

El cliente HTTP/2 obtiene las mismas protecciones a través de su objeto de opciones, TsgcWSHTTP2Client_Options, y el cliente HTTP/3 expone el límite de respuesta directamente.

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;

Una línea para una base TLS segura

Nada de lo anterior sirve si la capa TLS no verifica nada. sgcWebSockets 2026.7 añade un preajuste para eso. Todos los objetos de opciones TLS de la librería, tanto en los componentes WebSocket como en los HTTP, publican una propiedad Preset de tipo TsgcTLSPreset. Su valor por defecto es tlspCustom, que mantiene lo que hayas configurado a mano.

Ponerla en tlspSecureDefaults activa la verificación del certificado, TLS 1.2 o superior, y la verificación del nombre de host en una sola línea.

uses
  sgcWebSocket_Types;

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

Combina el preajuste con los interruptores de redirección y los límites de respuesta y tendrás un cliente que verifica con quién está hablando, se niega a degradarse, no entrega sus credenciales a un tercero, y al que no se le puede obligar a reservar memoria sin límite.

.NET

La edición gestionada incluye las mismas protecciones en 2026.7. La eliminación de credenciales en las redirecciones, el bloqueo de la degradación de HTTPS, los límites por fragmento y de respuesta total, el rechazo de cookies de sufijo público, Expect: 100-continue y los cuerpos de petición fragmentados están todos ahí, incluido el conjunto de clientes HTTP/2. Los mismos nombres, con la capitalización de C#.

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

Disponibilidad

Las opciones de blindaje del cliente HTTP están disponibles en sgcWebSockets 2026.7 en Delphi 7 hasta 13 y C++Builder (Win32/Win64, Linux64, macOS, Android e iOS), y en la edición .NET. Todo lo descrito aquí es opcional y está desactivado por defecto, así que la actualización es directa.

Los clientes con una suscripción activa pueden descargar la nueva compilación desde el área de clientes. Los usuarios de la versión de prueba pueden obtener el instalador actualizado en esegece.com/products/websockets/download.

¿Preguntas, comentarios o ayuda para decidir cuáles de estos interruptores debería activar tu aplicación? Ponte en contacto, recibirás respuesta de las personas que escribieron el código.