Goによるプライベートネットワークへのアクセスを禁止するHTTPクライアントの実装

クローラのように、ユーザからの入力に応じて任意のURLにHTTPリクエストを発行するソフトウェアは、誤ってプライベートネットワークへのリクエストを処理しないようにする必要があります。悪意のあるユーザが故意にプライベートなネットワークに対してリクエストして、内部情報にアクセスするといった攻撃を行う可能性があるからです。

PerlではLWPx::ParanoidAgentLWPx::ParanoidHandlerといったモジュールが便利です。これらのモジュールは、リクエスト先のURLをチェックしてプライベートネットワークへのリクエストを禁止してくれます。単にIPアドレスをチェックするだけでなく、ホスト名をDNSで解決して得られたIPアドレスをチェックしたり、リダイレクト先のURLをチェックしたりしてくれます。まさに偏執的です。

このLWPx::ParanoidAgentと同様の機能をもったGoのライブラリを実装しました。

github.com

ParanoidhttpはGoの標準ライブラリのnet/httpで提供されているhttp.Clientのparanoidなバージョンを作ってくれるファクトリです。以下のように利用できます。

// DefaultClientを利用すると簡単に利用できます(http.DefaultClientと同じ設定です)
res, _ := paranoidhttp.DefaultClient.Get("http://www.hatena.ne.jp")

// プライベートネットワークへのアクセス時にはエラーが返ります
_, err = paranoidhttp.DefaultClient.Get("http://192.168.0.1")

// attackers-host.net がプライベートネットワークのIPに名前解決されたり、
// プライベートネットワークにリダイレクトされてもエラーになります
_, err = paranoidhttp.DefaultClient.Get("http://attackers-host.net")

// 自分でhttp.Clientを作ってカスタマイズもできます
// 内部で利用されるhttp.Transportやhttp.Dialerのオブジェクトも返却されるので
// 同様にカスタマイズできます
client, transport, dialer := paranoidhttp.NewClient()
client.Timeout = 10 * time.Second
transport.DisableCompression = true
dialer.KeepAlive = 60 * time.Second

便利ですね。

実装的には、http.Tranportをラップしてリクエスト時にリクエスト先のホストをチェックしています。URLのホストがIPアドレスでない場合はDNSに問い合わせてIPアドレスを取得してチェックします。

とにかく使いたい機能だけを実装したという感じで以下のような制限があります。

  • ブラックリスト/ホワイトリスト機能がない
  • 名前解決時のタイムアウトが設定されてない(OSのリゾルバのタイムアウトに依存してる)
  • 名前解決にかかる時間がhttp.Clientのタイムアウトに含まれない
  • IPv6に対応してない(よくわからないのでIPv6では接続できないようにしてある)

がだいたい問題ないと思います。必要あればissueたてたり、PullRequestをおねがいします。一応実環境でも使っています。

以上、どうぞご利用ください。

追記

TOCTOU攻撃に対して脆弱であるとの指摘をいただきました。ホストやIPの安全性チェックの直後に、DNSレコードを変更するような操作によって、危険なIPアドレスへのアクセスをひきおこす可能性がありました。

Goによるプライベートネットワークへのアクセスを禁止するHTTPクライアントの実装 - はこべブログ ♨

外部で名前解決してアドレスチェックしてから、HTTPライブラリ内でもう一度名前解決してるようにみえる / Dialer実装内での名前解決を「置き換える」形で実装しないとTOCTOU攻撃が可能になるのでよくない / 修正された

2015/08/05 10:36
b.hatena.ne.jp

指摘をうけて修正しました。ありがとうございます!