앞서 HTTP 서버의 보안 강화에 대해 다룬 적이 있습니다. 이번 글은 그 이야기의 나머지 절반입니다. 클라이언트는 보통 대화에서 신뢰할 수 있는 쪽으로 취급되지만, 실제로는 그렇지 않습니다. 클라이언트는 여러분의 API 키를 전송하고, 원격 호스트가 반환하는 리다이렉트를 무엇이든 따라가며, 응답 전체를 메모리로 읽어 들이고, 응답이 저장하라고 요청하는 쿠키를 그대로 저장합니다. 악의적인, 혹은 단순히 침해된 엔드포인트는 이 네 가지 동작을 모두 여러분에게 불리하게 돌려놓을 수 있습니다.
sgcWebSockets 2026.7은 이러한 구멍을 각각 막는 클라이언트 측 스위치를 추가합니다. 모든 스위치는 기본적으로 꺼져 있으므로, 업그레이드해도 여러분이 직접 켜기로 결정하기 전까지 기존 애플리케이션은 아무것도 달라지지 않습니다. 이 옵션들은 저수준 HTTP 클라이언트, 즉시 사용 가능한 모든 REST 및 AI 클라이언트, 그리고 HTTP/2와 HTTP/3 클라이언트에서 사용할 수 있습니다.
리다이렉트를 따라가는 자격 증명
전형적인 클라이언트 측 유출입니다. Authorization 헤더를 붙여 API를 호출하면 서버가 다른 호스트를 가리키는 302로 응답하고, 클라이언트는 헤더와 쿠키를 포함한 채 그 요청을 충실히 그곳으로 다시 보냅니다. 여러분의 베어러 토큰이 방금 전혀 대화할 의도가 없던 머신으로 넘어간 것입니다. 리다이렉트가 악의적일 필요조차 없습니다. 정상적인 호스트에 있는 오픈 리다이렉트를 조작할 수 있는 공격자만 있어도 충분합니다.
StripAuthOnCrossHostRedirect는 리다이렉트가 원래 호스트를 벗어나는 순간 Authorization과 Cookie 헤더를 제거합니다. 요청은 여전히 리다이렉트를 따라가지만, 여러분의 자격 증명 없이 도착합니다.
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;
평문 HTTP로 다운그레이드하는 리다이렉트
두 번째 리다이렉트 문제입니다. https://로 시작했는데 서버가 http://로 시작하는 Location으로 응답하면, 클라이언트는 아무렇지 않게 평문으로 요청을 다시 보냅니다. 이제 경로상의 누구든 본문을 읽고 응답을 다시 쓸 수 있습니다. 이것이 바로 브라우저에서 HSTS가 막으려는 다운그레이드이며, 지금까지 HTTP 클라이언트에는 이에 상응하는 기능이 없었습니다.
NoInsecureRedirect는 요청을 HTTPS에서 HTTP로 옮기는 리다이렉트를 거부합니다. 리다이렉트를 따라가는 대신 거절합니다.
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');
두 리다이렉트 스위치는 서로 독립적이며, 실무에서는 둘 다 켜는 것이 좋습니다. 첫 번째를 켜면 호스트가 바뀌는 리다이렉트가 더 이상 토큰을 실어 나르지 않습니다. 두 번째를 켜면 리다이렉트가 더 이상 전송 계층 보안을 벗겨낼 수 없습니다.
크기 제한이 없는 응답
기본 HTTP 클라이언트는 서버가 보내는 것을 무엇이든 읽습니다. 서버가 10기가바이트짜리 응답 본문을 보내거나, 단일 청크가 2기가바이트 길이를 선언하는 청크 응답을 보내면, 클라이언트는 그만한 공간을 할당하려고 시도합니다. 이것은 자기 자신의 프로세스에 대한 한 줄짜리 서비스 거부이며, 공격자가 무언가를 깨뜨릴 필요조차 없습니다. 여러분의 요청에 예상보다 많은 데이터로 응답하기만 하면 됩니다.
두 가지 상한이 이를 막습니다. MaxResponseSize는 응답 본문의 전체 크기를 제한하고, MaxChunkSize는 청크 응답의 개별 청크 크기를 제한합니다. 둘 다 0은 "무제한"을 의미하며 이것이 기본값이므로, 값을 설정하기 전까지는 아무것도 바뀌지 않습니다. 한계를 초과하면 읽기가 중단되고 예외가 발생합니다. 클라이언트가 버퍼를 계속 키우지 않습니다.
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;
해당 엔드포인트가 실제로 반환해야 하는 양을 반영하는 숫자를 고르십시오. 몇 킬로바이트로 응답하는 JSON API를 호출한다면 몇 메가바이트의 상한도 넉넉하며, 그래도 무제한 읽기를 유한한 읽기로 바꿔줍니다.
최상위 도메인 전체를 주장하는 쿠키
응답은 Domain 속성이 붙은 쿠키를 보낼 수 있습니다. 와이어 포맷 어디에도 Domain=com이나 Domain=co.uk를 주장하지 못하게 막는 것이 없습니다. 이를 받아들이는 클라이언트는 이제 해당 접미사 아래의 모든 호스트로 그 쿠키를 보내려 하게 되는데, 이는 쿠키 세계의 마스터 키를 나눠주는 것과 같습니다. 브라우저는 수년 전부터 Public Suffix List를 이용해 이를 거부해 왔습니다.
RejectPublicSuffixCookies는 같은 규칙을 컴포넌트에 가져옵니다. 도메인이 실제 등록 가능한 도메인이 아니라 공용 접미사인 쿠키는 폐기됩니다. 이 옵션을 켜면 클라이언트의 쿠키 관리도 함께 켜진다는 점에 유의하십시오. 필터링 대상이 바로 쿠키 저장소이기 때문입니다.
oHTTP.RejectPublicSuffixCookies := True;
// Set-Cookie: session=abc; Domain=co.uk -> rejected
// Set-Cookie: session=abc; Domain=example.co.uk -> accepted
2026.7은 이전에 조용히 잘못 동작하던 쿠키 및 리다이렉트 관련 버그 두 가지도 수정합니다. 상대 경로 Location 헤더가 이제 절대 URL로 올바르게 해석되고, 307 또는 308 리다이렉트는 사양이 요구하는 대로 요청 메서드와 본문을 보존하며, 만료 날짜를 파싱할 수 없는 쿠키는 버려지는 대신 세션 쿠키로 취급됩니다. 둘 다 있을 때는 여전히 Max-Age가 Expires보다 우선합니다.
큰 본문 업로드: Expect: 100-continue와 청크 요청
새 옵션이 모두 공격에 관한 것은 아닙니다. 그중 둘은 대역폭 낭비를 막기 위한 것입니다. 거부될 엔드포인트(잘못된 자격 증명, 잘못된 콘텐츠 타입, 이미 소진된 할당량)에 큰 본문을 POST하면, 기본 동작은 본문 전체를 먼저 회선으로 밀어 올린 다음 401을 읽는 것입니다.
UseExpect100Continue는 Expect: 100-continue와 함께 요청 헤더를 보내고, 서버가 본문을 받을 의사가 있는지 말해줄 때까지 기다립니다. 서버가 대신 최종 상태 코드로 응답하면, 여러분은 페이로드를 전혀 보내지 않습니다. 진행 신호가 전혀 오지 않으면 본문은 어쨌든 전송되므로, 이 헤더를 무시하는 서버에서도 정상 동작합니다.
UseChunkedTransferEncoding은 그 반대 경우, 즉 크기를 미리 알 수 없는 업로드를 다룹니다. 본문은 고정된 Content-Length 대신 청크 단위로 전송되므로, 제너레이터나 압축기, 파이프에서 전체를 먼저 버퍼링하지 않고 스트리밍으로 내보낼 수 있습니다.
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;
즉시 사용 가능한 API 클라이언트에도 동일한 스위치
흥미로운 부분은 이 속성들이 어디에 있느냐입니다. 이 속성들은 라이브러리에 포함된 모든 즉시 사용 가능한 REST 클라이언트의 기반 클래스인 TsgcHTTPAPI_client에 다시 노출됩니다. 즉, 아래에 있는 저수준 HTTP 객체에 손대지 않고도 이미 사용 중인 컴포넌트에서 직접 설정할 수 있다는 뜻입니다.
여기에는 AI 및 LLM 클라이언트(OpenAI, Anthropic, Gemini, DeepSeek, Grok, Mistral, Ollama), OAuth2 클라이언트, Google Cloud, WhatsApp, AWS SQS와 WebPush, 그리고 Amazon AWS와 Google Cloud API 유닛이 포함됩니다.
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;
LLM 제공자에게 보내는 API 키야말로 제공자 도메인 밖으로 나가는 리다이렉트를 따라가서는 안 되는 종류의 자격 증명입니다. 그래서 이 스위치를 켜는 것이 아무 비용도 들지 않으면서 큰 이득을 주는 지점이 바로 여기입니다.
HTTP/2와 HTTP/3
HTTP/2 클라이언트는 옵션 객체인 TsgcWSHTTP2Client_Options를 통해 동일한 보호 기능을 제공하며, HTTP/3 클라이언트는 응답 상한을 직접 노출합니다.
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;
안전한 TLS 기준선을 위한 한 줄
TLS 계층이 아무것도 검증하지 않는다면 위의 어떤 것도 도움이 되지 않습니다. sgcWebSockets 2026.7은 이를 위한 프리셋을 추가합니다. 라이브러리의 모든 TLS 옵션 객체는, WebSocket 컴포넌트든 HTTP 컴포넌트든 똑같이, TsgcTLSPreset 타입의 Preset 속성을 게시합니다. 기본값은 tlspCustom이며, 이는 여러분이 직접 구성한 설정을 그대로 유지합니다.
이를 tlspSecureDefaults로 설정하면 인증서 검증, TLS 1.2 이상, 호스트명 검증이 한 줄로 켜집니다.
uses
sgcWebSocket_Types;
begin
oHTTP.TLSOptions.Preset := tlspSecureDefaults;
// certificate verification on, TLS 1.2+, hostname verification on
end;
이 프리셋을 리다이렉트 스위치 및 응답 상한과 결합하면, 상대가 누구인지 검증하고, 다운그레이드를 거부하며, 자격 증명을 제3자에게 넘기지 않고, 무제한 메모리 할당을 강요당하지 않는 클라이언트가 됩니다.
.NET
매니지드 에디션도 2026.7에서 동일한 보호 기능을 제공합니다. 리다이렉트 자격 증명 제거, HTTPS 다운그레이드 차단, 청크별 및 전체 응답 상한, 공용 접미사 쿠키 거부, Expect: 100-continue와 청크 요청 본문이 모두 포함되며, HTTP/2 클라이언트 세트도 마찬가지입니다. 이름은 동일하고 C# 표기 규칙을 따릅니다.
client.StripAuthOnCrossHostRedirect = true;
client.NoInsecureRedirect = true;
client.RejectPublicSuffixCookies = true;
client.MaxResponseSize = 10 * 1024 * 1024;
client.MaxChunkSize = 1024 * 1024;
제공 범위
HTTP 클라이언트 보안 강화 옵션은 sgcWebSockets 2026.7에서 Delphi 7부터 13까지와 C++Builder(Win32/Win64, Linux64, macOS, Android, iOS), 그리고 .NET 에디션에서 사용할 수 있습니다. 여기서 설명한 모든 것은 선택 사항이며 기본값은 꺼짐이므로, 업그레이드는 그대로 적용해도 문제가 없습니다.
구독이 유효한 고객은 고객 영역에서 새 빌드를 내려받을 수 있습니다. 평가판 사용자는 esegece.com/products/websockets/download에서 최신 설치 파일을 받을 수 있습니다.
질문이나 피드백이 있으신가요? 아니면 어떤 스위치를 켜야 할지 결정하는 데 도움이 필요하신가요? 문의하기, 코드를 작성한 사람들에게서 직접 답변을 받으실 수 있습니다.
