sgcWebSockets の HTTP クライアントを堅牢化する | eSeGeCe ブログ

sgcWebSockets の HTTP クライアントを堅牢化する

· コンポーネント

以前、HTTP サーバーの堅牢化について書きました。この記事はその話のもう半分です。クライアントは通常、会話の中で信頼できる側として扱われますが、実際にはそうではありません。クライアントはあなたの API キーを送信し、リモートホストが返してきたリダイレクトにそのまま追従し、レスポンス全体をメモリに読み込み、レスポンスが保存を求めてきた Cookie を何でも保存します。悪意のあるエンドポイント、あるいは単に侵害されたエンドポイントは、これら4つの挙動すべてをあなたに対して悪用できます。

sgcWebSockets 2026.7 は、これらの穴をひとつずつ塞ぐクライアント側のスイッチ群を追加します。そのすべてが既定では無効なので、あなたがオプトインすると決めるまで、アップグレードしても既存のアプリケーションの動作は何も変わりません。これらは低レベルの HTTP クライアント、すべての既製の REST および AI クライアント、そして HTTP/2 と HTTP/3 のクライアントで利用できます。

リダイレクトに追従してしまう認証情報

典型的なクライアント側の情報漏えいです。Authorization ヘッダーを付けて API を呼び出すと、サーバーが別のホストを指す 302 を返し、クライアントは律儀にヘッダーと Cookie を含めてそのリクエストをそこで繰り返します。あなたのベアラートークンは、話しかけるつもりのなかったマシンに手渡されてしまったわけです。リダイレクトが悪意あるものである必要すらありません。正規のホスト上のオープンリダイレクトを操作できる攻撃者がいれば十分です。

StripAuthOnCrossHostRedirect は、リダイレクトが元のホストを離れた時点で AuthorizationCookie のヘッダーを取り除きます。リクエストはリダイレクトに追従しますが、あなたの認証情報を持たずに到着します。

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 へダウングレードするリダイレクト

2つ目のリダイレクトの問題です。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');

2つのリダイレクト用スイッチは独立しており、実際には両方を有効にしたくなるはずです。1つ目を有効にすれば、クロスホストのリダイレクトがトークンを運ぶことはなくなります。2つ目を有効にすれば、リダイレクトが転送レイヤーのセキュリティを剥ぎ取ることはできなくなります。

サイズ制限のないレスポンス

既定の HTTP クライアントは、サーバーが送ってきたものを何でも読み込みます。サーバーが10ギガバイトのレスポンスボディを送ってきたり、単一のチャンクが2ギガバイトの長さを宣言するチャンク形式のレスポンスを送ってきたりすると、クライアントはそのための領域を確保しようとします。これは自分自身のプロセスに対する一行のサービス拒否攻撃であり、攻撃者は何かを破る必要すらなく、あなたのリクエストに対して想定以上のデータで応答するだけで済みます。

2つの上限がこれを塞ぎます。MaxResponseSize はレスポンスボディの合計サイズを制限し、MaxChunkSize はチャンク形式レスポンスの単一チャンクを制限します。どちらもゼロを「無制限」として扱い、それが既定値なので、設定するまで何も変わりません。制限を超えると読み込みは中止されて例外が発生し、クライアントはバッファを増やし続けません。

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 属性付きの Cookie を送信できます。ワイヤーフォーマットには Domain=comDomain=co.uk を主張することを妨げるものは何もありません。それを受け入れるクライアントは、そのサフィックス配下のあらゆるホストにその Cookie を送信するようになります。これは Cookie 版のマスターキーを配り歩くようなものです。ブラウザーは長年、Public Suffix List を使ってこれを拒否してきました。

RejectPublicSuffixCookies は同じルールをコンポーネントにもたらします。ドメインが実際に登録可能なドメインではなくパブリックサフィックスである Cookie は破棄されます。なお、これを有効にすると、クライアント上の Cookie 管理も同時に有効になります。フィルターの対象となるのが Cookie ジャーだからです。

oHTTP.RejectPublicSuffixCookies := True;

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

2026.7 では、これまで密かに誤っていた Cookie とリダイレクト関連の2つのバグも修正しています。相対的な Location ヘッダーは正しく絶対 URL に解決されるようになり、307 や 308 のリダイレクトは仕様どおりリクエストメソッドとボディを保持するようになり、有効期限を解析できない Cookie は破棄されるのではなくセッション Cookie として扱われるようになりました。両方が存在する場合、Max-AgeExpires より優先されるのは変わりません。

大きなボディのアップロード: Expect: 100-continue とチャンク形式のリクエスト

新しいオプションのすべてが攻撃対策というわけではありません。うち2つは帯域を無駄にしないためのものです。大きなボディを 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;

このプリセットをリダイレクト用スイッチとレスポンスの上限と組み合わせれば、通信相手を検証し、ダウングレードを拒否し、認証情報を第三者に渡さず、無制限のメモリ確保を強いられることのないクライアントが手に入ります。

.NET

マネージド版も 2026.7 で同じ保護機能を提供します。リダイレクト時の認証情報の除去、HTTPS ダウングレードのブロック、チャンク単位および合計のレスポンス上限、パブリックサフィックス Cookie の拒否、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 から更新されたインストーラーを入手できます。

ご質問やご意見、あるいはアプリケーションでどのスイッチを有効にすべきかの判断にお困りですか。お問い合わせください。コードを書いた本人が返信します。