本文へ移動
技術を、自社の仕事に。
判断と実行を助けるメディア

記事を検索

Expressのtrust proxy設定でreq.ipが偽装される問題

目次 · 7項目

管理画面を社内のIPアドレスからだけ開けるようにしている。ログインの試行回数を接続元ごとに制限している。操作ログに、どこから操作したかを req.ip で残している。ロードバランサーやリバースプロキシの後ろでExpressを動かし、こうした処理を req.ip に任せているなら、trust proxy の書き方を1行確かめてください。

2026年10月5日(UTC)に GitHub Advisory Database に登録された GHSA-jqcg-44mw-7w3h(CVE-2026-90711、重大度 Critical)は、Express が req.ip を決めるのに使う部品 proxy-addr の問題です。trust proxy に ::ffff:10.0.0.0/8 のようなIPv4を埋め込んだIPv6の表記を短いプレフィックス長で書くと、すべてのIPv4アドレスが信頼するプロキシとして扱われ、req.ip に利用者が X-Forwarded-For ヘッダーに書いた任意の値が入ります。アドバイザリと Express の変更履歴、編集部で 2.0.7 と 2.0.8 を動かして比べた結果から、確かめる点と更新の手順を整理します。

どの書き方が問題になるのか

アドバイザリは条件を次のように説明しています。

When an application configures a trust subnet as an IPv4-mapped IPv6 address with a short prefix, such as ::ffff:10.0.0.0/8 (the correct spelling is ::ffff:10.0.0.0/104), the subnet compiles with all-zero leading bits and matches every IPv4 address instead of the block it names.

::ffff:10.0.0.0 は128ビットのIPv6の中にIPv4を埋め込んだ表記で、先頭の96ビットが ::ffff: にあたります。IPv4の 10.0.0.0/8 と同じ範囲は /104(96+8)と書く必要があり、/8 と書くと「先頭の8ビットが0」という意味になります。proxy-addr 2.0.7 までは、これがすべてのIPv4アドレスに一致していました。::/1 のように先頭のビットが0のIPv6の範囲でも同じことが起きるとされています。

影響を受けるのは proxy-addr の 1.1.0 以上 2.0.8 未満です。Express の req.ip と req.ips はこの部品の結果を返すため、アドバイザリは「This defeats IP-based access control, rate limiting, geolocation, and audit logging.」と書いています。しかも「The misconfiguration compiles without any error」で、起動時には何も表示されません。

編集部で2.0.7と2.0.8を比べた

2026年10月10日に、Linux 上の Node.js 22.22.0 で2つの環境を作りました。

  • Express 5.2.1 + proxy-addr 2.0.7(古いロックファイルの状態を再現するため、overrides で 2.0.7 に固定)
  • Express 5.3.0 + proxy-addr 2.0.8(新規にインストール)

アプリは app.set('trust proxy', <設定>) のあと、接続元のアドレス・req.ip・req.ips を返すだけです。サーバーを 127.0.0.1 で待ち受け、同じ 127.0.0.1 から X-Forwarded-For: 198.51.100.7 を付けて送りました。接続元の 127.0.0.1 は、10.0.0.0/8 を表すどの設定の範囲にも入らないため、正しく動けば req.ip は 127.0.0.1 になるはずです。

express 5.2.1 / proxy-addr 2.0.7 / listen 127.0.0.1 / trust '::ffff:10.0.0.0/8'
  XFF 198.51.100.7 -> {"remote":"127.0.0.1","ip":"198.51.100.7","ips":["198.51.100.7"]}
express 5.3.0 / proxy-addr 2.0.8 / listen 127.0.0.1 / trust '::ffff:10.0.0.0/8'
  XFF 198.51.100.7 -> {"remote":"127.0.0.1","ip":"127.0.0.1","ips":[]}

2.0.7 では、範囲外からの接続なのに req.ip が送り手の書いた値になりました。X-Forwarded-For: 10.20.30.40 と社内のアドレスを名乗った場合も req.ip は 10.20.30.40 で、社内のアドレスだけを許可する作りなら外から通ってしまいます。2.0.8 では、どちらも 127.0.0.1 のままでした。

編集部の検証結果。接続元127.0.0.1からX-Forwarded-For 198.51.100.7を付けて送ったときのreq.ip。trust proxyが::ffff:10.0.0.0/8と::/1の場合、proxy-addr 2.0.7では偽装された198.51.100.7、2.0.8では実際の接続元127.0.0.1になった。10.0.0.0/8と::ffff:10.0.0.0/104はどちらの版でも127.0.0.1だった

IPv4の表記の 10.0.0.0/8、正しいプレフィックス長の ::ffff:10.0.0.0/104、名前で指定する uniquelocal は、2.0.7 でも 127.0.0.1 のままでした。対照として、接続元を正しく信頼する ::ffff:127.0.0.0/104 では、どちらの版でも X-Forwarded-For の値が入りました。2.0.8 でも、正しく書いた表記は従来どおり動きます。

検証の条件と限り

この環境は IPv6 で待ち受けられなかった(EAFNOSUPPORT)ため、:: で待ち受けて接続元が ::ffff:127.0.0.1 と見える場合は、proxy-addr を直接呼んで確かめました。結果は同じで、::ffff:10.0.0.0/8 のときだけ 2.0.7 が 198.51.100.7 を返しました。ロードバランサーを挟んだ実際の構成では試していません。

自分のアプリが当てはまるか確かめる

当てはまるのは、実行環境の proxy-addr が 2.0.7 以下で、かつ trust proxy に ::ffff: で始まりプレフィックス長が96未満の範囲(または ::/1 のような範囲)がある場合です。

proxy-addr の版はロックファイルで決まります。編集部の環境で新規にインストールすると、Express 5.2.1 でも 4.22.3 でも 2.0.8 が入りました。Express の版だけでは判断できないため、実際に入っている版を見ます。2.0.7 を固定した環境では、npm audit が「proxy-addr 1.1.0 - 2.0.7 / Severity: critical」と表示しました。

npm ls proxy-addr
npm audit
grep -rnE "trust proxy|proxy-addr|proxyaddr" --include=*.js --include=*.ts --include=*.mjs --include=*.cjs .
grep -rn "::ffff:" .env* config/ deploy/ 2>/dev/null

設定は、コードに直接書くほか、環境変数や設定ファイルから読んでいることがあります。::ffff:172.16.0.0/12 のように ::ffff: の後ろにIPv4と短いプレフィックス長が続いていれば、当てはまる可能性が高いと考えてください。なお Fastify は別の部品(@fastify/proxy-addr)を使っており、今回のアドバイザリの対象に含まれていません。同じ書き方での挙動は確かめていません。

直し方は2つ、両方やる

設定をIPv4の表記に直す

アドバイザリの回避策は「Write IPv4 trust subnets in plain IPv4 notation (for example 10.0.0.0/8).」です。IPv6の表記を使い続けるなら、::ffff:10.0.0.0/104 のようにプレフィックス長に96を足します。

// 直す前(2.0.7 以下では、すべての IPv4 の接続元を信頼してしまう)
app.set('trust proxy', '::ffff:10.0.0.0/8');
// 直した後
app.set('trust proxy', '10.0.0.0/8');

部品の更新だけで済ませると、逆向きの症状が出ます。proxy-addr を直接呼び、接続元を正規のプロキシに見立てた 10.1.1.1 として確かめたところ、::ffff:10.0.0.0/8 のままでは 2.0.8 が 10.1.1.1 を返し、利用者のアドレスが取れませんでした。10.0.0.0/8 に直すと X-Forwarded-For の値を返しました。

proxy-addrを2.0.8に上げる

Express 5 系では、10月9日に公開された 5.3.0 が要件を ^2.0.8 に上げています(変更履歴「Upgrade proxy-addr to ^2.0.8, which fixes CVE-2026-90711」)。Express 4 系の最新の 4.22.3(9月14日)は要件が ~2.0.7 のままですが、この範囲に 2.0.8 が含まれます。編集部の環境では、2.0.7 が記録されたロックファイルで npm install を実行しても 2.0.7 のままで、npm update proxy-addr で 2.0.8 に上がりました。npm ci はロックファイルのとおりに入れるため、更新したロックファイルをコミットしないと本番は古い版のままです。

npm update proxy-addr compression body-parser
npm ls proxy-addr compression body-parser

あわせて、compression を使っていれば 1.8.2 以上にします。同じ10月5日に登録された GHSA-vc2v-76pw-4v95(CVE-2026-87776、重大度 High)は、圧縮した応答の送信中に接続を切ると zlib のメモリーが解放されず、繰り返すとメモリーを使い果たす問題で、回避策は「None.」です。compression は Express 本体の依存ではないため、別に確かめます。body-parser は、limit に不正な値を渡すとサイズの制限が黙って無効になる問題(CVE-2026-12590)が 2.3.0 で直っており、1.x 系では 1.20.6 で修正されています。

5.3.0で変わる応答と、更新後の確認

5.3.0 には、応答の中身が変わる変更も入っています。

  1. Content-Disposition の引用符がなくなる。 res.attachment()・res.download()・res.sendFile() で、HTTPのトークンとして有効なファイル名は引用符で囲まれなくなりました。編集部の環境でも res.attachment('report.csv') が 5.2.1 の filename="report.csv" から 5.3.0 で filename=report.csv に変わりました。変更履歴は RFC 6266 上は同じ意味としつつ、ヘッダーの文字列をそのまま比べるテストは期待値の更新が必要だとしています
  2. body-parser の limit が不正だと起動時に止まる。 2.3.0 では express.json({ limit: 'abc' }) が TypeError: option limit "abc" is invalid を投げました(2.2.1 ではエラーなし)。環境変数から読んでいる場合は、本番の値を先に確かめます
  3. res.send() が Content-Type の既存のパラメーターを残したまま charset を付けるようになった

更新を本番に出したら、次の3点を確かめることを編集部では勧めます。

  • プロキシ経由の接続で、req.ip が利用者のアドレスになっているか(ログにロードバランサーのアドレスばかり残っていないか)
  • アプリに直接届く経路があれば、そこから偽の X-Forwarded-For を付けて送り、req.ip が実際の接続元になるか
  • 問題の設定で運用していた期間の、req.ip を根拠にした許可・制限・操作ログをどこまで見直すか

ヘッダーの値を信じた許可設定が穴になる例はログのClaudeBotは本物か — User-Agentを信じた許可設定が穴になるでも整理しています。npm の部品の版を確かめる流れはsharpのAVIF脆弱性 — Astro・Next.jsで0.35.4以上か確かめる、依存部品をまたぐ攻撃への備えは2026年サプライチェーン攻撃まとめで扱っています。

確認した範囲

2026年10月10日に、github/advisory-database の GHSA-jqcg-44mw-7w3h と GHSA-vc2v-76pw-4v95 のJSON、expressjs/express の History.md と各タグの package.json(v5.3.0 は commit cf6722b、v4.22.3 は commit 899b524)、jshttp/proxy-addr の修正コミット 780911d、expressjs/body-parser の HISTORY.md、registry.npmjs.org の公開日時と依存関係を直接取得して確認しました(資料調査)。あわせて、Linux 上の Node.js 22.22.0 で Express 5.2.1(proxy-addr 2.0.7 に固定)と 5.3.0 を動かし、req.ip・Content-Disposition・body-parser の limit の挙動と、npm ls・npm audit・npm update の結果を確かめました(実機検証)。IPv6 での待ち受け、ロードバランサーを挟んだ構成、compression と body-parser の問題そのものの再現、Fastify での挙動は確かめていません。

ロードバランサーの後ろで動く業務システムの依存部品の更新や、IPアドレスによる制限の見直しは、開発・AI・自動化のご相談からお問い合わせください。

参考資料

この記事を共有XFacebook
鈴木 翔

技術の可能性に魅了され、学生時代からプログラミングとデジタルアートの分野に深い関心を持つ

この記事のテーマを、自社の次の一歩へ

自社での進め方を、具体的に。

つくりたい仕組み、既存システム、運用の条件を整理し、実現に向けた次の一歩を考えます。

  • 実現したい仕組み
  • 既存環境との接続
  • 運用の条件
開発・運用の構想を相談する

構想段階からご相談いただけます。この記事の情報を相談フォームに引き継ぎます。

最新記事をメールで受け取る