
Penpotのリモート画像インポート機能により、認証済みファイル編集者が通常のメディア便利機能をバックエンド発信のSSRFに変えることができました。攻撃者が制御するURLがリダイレクト追従型のサーバーフェッチパスに渡され、宛先フィルタリングが行われなかったためです。
この問題は、Penpot(オープンソースのデザイン・コード共同作業プラットフォーム)をレビューしているときに発見しました。特定の質問を念頭に置いていました。
共同作業デザインツールが、あるユーザーにバックエンドへリモート画像URLをフェッチさせる場合、何が起こるのか?
この場合、その質問が実際のバグに繋がりました。
Penpotのリモート画像インポートフローは、ユーザーが制御するURLを受け入れ、バックエンドがそれをサーバーネットワークコンテキストからフェッチすることを引き起こしましたが、ループバックやプライベートネットワークターゲットに対する宛先制限が適用されていませんでした。また、共有HTTPクライアントは自動的にリダイレクトを追跡していました。
これにより、通常のメディア便利機能が認証済みバックエンド発信のSSRFプリミティブに変わり、最終的にCVE-2026-45806になりました。
Penpot: Penpot on GitHub
CVE: CVE-2026-45806
CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
影響を受けたのはPenpotです。公式サイトとメディアキットでは、Penpotは**+100万人以上の成長中のユーザーベースを持ち、数万の組織が使用しているとしています。その中にはBlender**、Mozilla、Fedora、NTT Data、MIT、Société Générale、Cisco、Fujitsu、Indra、ByteDanceが含まれます。
認証済みファイル編集者 -> 攻撃者が制御するリモート画像URL -> create-file-media-object-from-url -> バックエンドのdownload-imageがリダイレクト有効でフェッチ -> 最終リクエストが内部専用イメージエンドポイントに到達 -> バックエンド発信のSSRF / 内部到達可能性
Penpotはオープンソースのデザイン・コード共同作業プラットフォームです。
以下のような機能を扱います。
つまり、メディアインポートパスは実際の信頼境界に位置しています。
ここで重要な問いは、Penpotがリモート画像のインポートをサポートしているかどうかではありませんでした。
本当の問いは次の通りです。
ユーザーがリモート画像をインポートするとき、Penpotはバックエンドがどこに接続することを許可されているかを制限しているか?
この場合、制限していませんでした。
多くの人はリモートインポート機能を過小評価しています。
それは誤りです。
アプリケーションが以下の条件を満たす瞬間、
実際の発信信頼境界が生まれます。
それが今回の問題でした。
このバグは画像レンダリングの問題ではありませんでした。 ファイルストレージの問題でもありませんでした。 ファイル編集の通常のパーミッションチェックの問題でもありませんでした。
これは古典的なサーバーサイドの信頼障害でした。
それだけで実際の脆弱性が生まれます。
Penpotに対して、最初にRPCメソッドを盲目的にファジングしたり、クラッシュを探したりするアプローチは取りませんでした。
より強力なアプローチは、最も有望なセキュリティ境界を特定することでした。
Penpotにとって、それはリモートメディアインポートでした。
なぜなら、この機能は次の要素を組み合わせているからです。
これが正しい調査すべき境界でした。
そして、まさにそこにバグがありました。
バグは小さな信頼チェーンに還元されます。
フロントエンドでは:
(defn upload-media-url
[name file-id url]
(rp/cmd!
:create-file-media-object-from-url
{:name name
:file-id file-id
:url url
:is-local true}))
ユーザーが制御するurlが直接RPC呼び出しに送られます。
次にバックエンドでは:
(sv/defmethod ::create-file-media-object-from-url
...
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(files/check-edition-permissions! pool profile-id file-id)
...
(let [_ (files/get-minimal-file cfg file-id)
mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))])
そして:
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(let [content (media/download-image cfg url)
バックエンドは呼び出し元が対象ファイルを編集できることを確認した後、攻撃者が制御するURLをmedia/download-imageに渡します。
フェッチの実装は次の通りです:
(defn download-image
"Download an image from the provided URI and return the media input object"
[{:keys [::http/client]} uri]
...
(http/req! client
{:method :get :uri uri}
{:response-type :input-stream})
そして共有HTTPクライアントは次のように設定されています:
(http/build-client {:connect-timeout 30000
:follow-redirects :always}))
これが脆弱性の全体像です:
攻撃者に必要なのは次のみだからです:
攻撃チェーンは単純です:
これがバグの全体像です。
重要な違いはリクエストがどこで発生するかです。
問題は次のようなものではありません:
「PenpotはURLから画像をインポートできますか?」
本当の問題は次の通りです:
「認証済みユーザーが、アプリケーションを通じてアクセスできないはずの内部宛先にPenpotのバックエンドを接続させることができるか?」
この場合、その答えは「はい」でした。
それが重要な理由は、以下の間に実際の違いがあるからです:
画像検証はその違いを除去しません。
一部の直接的なデータ流出ケースを狭めるだけであり、SSRF条件やネットワーク境界の突破を除去するわけではありません。
この問題を、レビューしたPenpotコードパスに直接関連する制御されたローカル証明で検証しました。
目標はサードパーティのインフラストラクチャに影響を与えることではありません。 目標は正確なセキュリティ特性を証明することでした:
関連する動作を反映した自己完結型のJavaバリデーターを構築しました:
content-typeとcontent-lengthに基づく画像受け入れチェック2つのケースを検証しました。
バリデーターは以下をリクエストしました:
http://127.0.0.1:7790/internal.png
観測された結果:
http://127.0.0.1:7790/internal.pnghttp://127.0.0.1:7790/internal.png200image/pngこれにより、インポート形式のフェッチロジックが内部専用イメージエンドポイントを直接受け入れることが証明されました。
次にバリデーターは以下をリクエストしました:
http://localhost:7791/redirect-to-internal
そのエンドポイントは以下へのHTTPリダイレクトを返しました:
http://127.0.0.1:7790/internal.png
観測された結果:
http://localhost:7791/redirect-to-internalhttp://127.0.0.1:7790/internal.png200image/png内部専用リスナーがリダイレクトされたリクエストをログに記録しました。
これにより、より重要な主張が証明されました:
ここでのペイロードは意図的に単純でした:
それが重要な理由は、Penpotが任意のバイトをフェッチして停止するだけではないからです。 リクエスト後にメディア指向の検証を実行します。
したがって、正しい証明は次のようなものではありません:
「バックエンドがどこかに接続しようとすることができる」
より強い証明は次の通りでした:
「バックエンドを内部のどこかに接続させ、機能が期待する同じ画像形式の制約の下でリクエストを正常に完了させることができる」
それがまさに検証で示されたことです。
このようなSSRFバグに対する一般的な反応は次の通りです:
「ターゲットは画像を返さなければならない」
その観察は真実ですが、不完全です。
脆弱性を除去するわけではありません。
どの内部ターゲットが最も直接的に有用かを示すだけです。
この問題は依然として以下を可能にします:
それでも実際のセキュリティ境界の突破です。
特に自己ホスト環境では、内部サービスはしばしばその境界の背後に存在します。
この問題は最終的にHighのCVSS深刻度に割り当てられました:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
その分類は妥当です。
主張は、認証されていない攻撃者が何もない状態からすべてのPenpotデプロイメントを即座に侵害できるということではありません。
主張は、通常の認証済みファイル編集者がPenpotを内部宛先に対するバックエンドリクエストプリミティブに変えることができるということです。これにはリダイレクト支援によるループバックやプライベートネットワークターゲットへのアクセスも含まれます。
開示プロセス中にいくつかの深刻度に関する議論がありました。主に以下に関するものです:
これらは議論すべき妥当な制約です。
しかし、核心的な問題を除去するわけではありません:
それは現実的で防御可能なSSRF脆弱性です。
ここで重要な修正は、より厳格なMIME処理ではありません。
本当の修正は送信先ポリシーです。
この種のバグに対する正しい修正には以下が必要です:
httpとhttpsのみを許可するlocalhostこれが正しい修正方向です。なぜなら、これは画像解析のバグではなく、ネットワーク信頼境界のバグだったからです。
この問題は、GitHubのセキュリティ報告フローを通じて非公開で報告されました。
報告には以下が含まれていました:
メンテナーは問題を確認し、解決に取り組み始めました。
その後、問題は次のように割り当てられました:
CVE-2026-45806
重要な教訓は単純です:
リモートメディアインポートは発信信頼境界であり、単なる便利機能ではない
多くの開発者は次のように考えます:
これらは実装の詳細です。
本当のセキュリティ上の問いは次の通りです:
ユーザーに代わってバックエンドがどこに接続することを許可されているか?
その問いが明示的に答えられない場合、リモートインポートのような機能はデフォルトでSSRFの表面になります。
このバグは、SSRFレビューに関する重要なことを再確認させます:
それが本当の教訓です。
この脆弱性は派手なペイロードに関するものではありませんでした。
正しい信頼境界の問いを立てることに関するものでした。
Penpotは認証済みファイル編集者にリモート画像URLを提供することを許可し、バックエンドはそのURLを本来以上に信頼しました。 リダイレクト処理が残りの部分を行いました。
それがこれがCVE-2026-45806になった理由です。