CVE-2026-51385 のアドバイザリです。GRAPHIFY がこのアドバイザリを認識も公開もしていなかったため、公開する必要がありました。MITRE は CVE-2026-51385 を割り当てており、これがそのためのアドバイザリです。
ES: これは私の最初の CVE でした。正直に言うと、ANTI SSRF 関数のロジックを破る方法を自分で見つけました。そして、本当に最初の CVE を公開したかったのです。 そこで、セキュリティにどのように影響するかを考えて影響を示そうと思いました。実行は複雑ですが (実際のインスタンスで発生するのは難しいですが、可能なので complexity high になっています)。MITRE に送ったところ受理されました。これがアドバイザリです。
EN: これは私の最初の CVE でした。正直に言うと、アンチ SSRF 関数のロジックを破る方法を自分で見つけました。そして、本当に CVE を公開したかったのです。 そこで、それがセキュリティにどのように影響しうるかを考え、正当な理由を見つけて MITRE に送り、受理されました。これがアドバイザリです。
このレポートは部分的に AI によって作成されましたが、人間が監督したことに留意してください
graphify における DNS リバインディング (TOCTOU) による SSRF。
パッケージ: graphify (PyPI: graphifyy)、リポジトリ Graphify-Labs/graphify
影響を受けるコンポーネント: URL 取り込みパス graphify add <url>
影響を受けるバージョン: >=0.3.2, <=0.4.29
修正済みバージョン: 0.5.4 (コミット dd86271、PR #591 / #592)
CVSS 3.1: 8.3 (High)、CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CWE: CWE-918 (SSRF)、CWE-367 (TOCTOU レース)
報告者: Arturo Melgarejo Galindo、独立系セキュリティ研究者 (@Arturo0x90)
graphify add <url> は、多くのフェッチャーと同様の方法で SSRF から自身を保護しようとします。ホスト名を解決し、結果の IP をブロックリスト (ループバック、RFC1918、リンクローカル、予約済み) と照合し、そのチェックに合格した場合にのみフェッチします。
問題は、検証した IP が実際に接続する IP ではないことです。検証では DNS ルックアップを 1 回実行し、その後 requests.get(hostname) が 2 回目の独立したルックアップを実行します。ホスト名の DNS ゾーンを所有し、非常に低い TTL で 2 つの A レコード (1 つはパブリック、もう 1 つは内部) を返す場合、リゾルバはクエリごとに異なる回答を合法的に返すことができます。最初の回答はブロックリストを通過します。2 番目の回答が実際にソケットが接続する先です。これがバグの全容です。「IP をチェックしてから名前で接続する」ロジックに対する古典的なリバインディング回避であり、修正方法は検証済みの IP を接続に固定することです。0.5.4 が行っているのはこれです。
これについては正直に述べたいと思います。単体では、実際よりも弱く見えるからです。
人間が座って、すでに信頼している URL を graphify add に入力する場合、これはほぼ無価値です。その人物は、望めば既に graphify を自分の 127.0.0.1 に向けることができます。リバインディングのトリックは必要なく、そのシナリオには攻撃者はいません。
graphify がオペレーターが選択していない URL に対して実行された瞬間、これは本当の脆弱性になります。そして、このツールにとってそれはコーナーケースではなく、通常の使用方法です。graphify は AI アシスタントに消費されるナレッジグラフを構築するため、現実的なフローは、エージェント、CI ジョブ、README の URL リストを走査するスクリプト、または別のモデルの出力から直接得られた URL など、何らかのプロセスがあなたの代わりに graphify add を呼び出すものです。これらのすべてにおいて、攻撃者は入力文字列を制御し、人間がそれを検査することはありません。
それが重要となるケースです。入力が信頼できないものになり、リバインディングのレースに勝利すると、フェッチは検証したパブリックアドレスではなく内部アドレスに到達します。具体的には、以下に到達できます:
127.0.0.1 およびループバックにバインドされたすべてのもの、169.254.169.254 (クラウドインスタンスメタデータ)、100.64.0.0/10。これはレースさえ不要で、そこを指す単純な A レコードで十分です。そして、これらのアドレスに到達することが損害につながるのは、そこに何が存在するかによります。多くの内部サービスや開発サービスは、データを返すか、素の GET で状態を変更する GET エンドポイントを公開しています。したがって、間違ったパスとクエリ文字列でそれらの 1 つに到達する graphify フェッチは、単なる読み取りではありません。内部ターゲットが Jenkins の scriptText、--debug の Flask/Django デバッガー、管理パネル、またはメタデータエンドポイントである場合、その不正な GET は境界の内側から攻撃者が行動していることを意味します。graphify は、彼らの代わりにリクエストを行う代理人 (deputy) です。
これが RCE を提供するとは主張しません。単体ではそうではありません。しかし、自動化された取り込みパスからループバック、IMDS、RFC1918、CGN に向けられる完全な SSRF は、まさにそれらの攻撃が構築されるプリミティブであり、それが報告のポイントです。
私は Tavis Ormandy の公開ハーネス rbndr.us を使用しました。これは、解決のたびに 2 つの IP 間で切り替わるホスト名を提供します。7f000001.08080808.rbndr.us は 8.8.8.8 と 127.0.0.1 を交互に返します。
内部ターゲット (ここではデモ用にループバック) で何かを起動します:
sudo python3 -m http.server 80
次に、取り込みをトリガーし、レースが成立するまで再試行します。4〜5 回の試行に 1 回程度の確率で成立し、単純なループで 99% をはるかに超えます:
for i in {1..20}; do
graphify add http://7f000001.08080808.rbndr.us/ && break
sleep 1
done
成功した試行では、ホスト名が最初に IP ブロックリストを通過したにもかかわらず、graphify は 127.0.0.1:80 のローカルサーバーが返したものを取り込みます。
0.5.4 (dd86271) で修正がコミットされる。Arturo Melgarejo Galindo、独立系セキュリティ研究者。