n8n CVE-2026-72765 検査した式と実行する式がずれる
ワークフロー自動化ツールの n8n に、ワークフローを編集できる利用者がホスト上で OS コマンドを実行できる脆弱性が見つかっている。CVE-2026-72765 である。CVSS は 9.9(Critical)で、2.31.5 と 2.32.1 で修正されている。
NVD の記述で目を引くのは原因の書き方だ。アロー関数の本体(arrow-function body)を使った式でサンドボックスを回避できる、とある。
この記事では、なぜ「アロー関数の本体」という限定が付くのかを仕組みから読み解き、最後に Node.js で同じ形を再現して、検査をどこに置けば止まるのかを確かめるスクリプトを置く。n8n を立てる必要はない。
何が起きるのか
n8n のワークフローでは、ノードのパラメータに {{ ... }} の形で JavaScript の式を書ける。「前のノードの出力からこの項目を取り出す」といった処理を、コードを書かずに済ませるための機能だ。
この式は本来サンドボックスの中で評価される。ファイルにもプロセスにも触れない、という前提になっている。今回の欠陥は、その前提がアロー関数の本体で破れるというものだった。
抜けた先はホストの Node.js プロセスである。そこから先はシステムコマンドの実行に届く。
なぜ「アロー関数の本体」なのか
ここが仕組みの核心だ。式のサンドボックスには、素朴に作るとほぼ必ず踏む落とし穴がある。
検査するタイミングと、実行するタイミングがずれることだ。
式を安全にしたい実装者は、たいてい次のように考える。危ないものを弾けばいい、と。禁止したい識別子(process、require、constructor など)を並べて、式の文字列にそれが含まれていないかを見る。含まれていなければ評価する。
素直な発想だが、この検査には前提がある。検査した文字列が、そのまま実行される式のすべてであるという前提だ。
アロー関数は、この前提を壊す。
1 | |
x => x + 1 の部分は、式が評価された時点ではまだ実行されていない。 実行されるのは map が呼び出したときで、その呼び出し元はサンドボックスの外にある関数である。
つまり、アロー関数の本体は「式の一部として検査される」のに「サンドボックスの外側の文脈から呼ばれる」という二重の性質を持つ。検査と実行のあいだに、値が関数として外へ渡る隙間がある。
素朴な文字列検査は、ここで2つの意味で無力になる。
- 文字列を見る検査は、書き方の揺れに追随できない。
constructorを弾いても["cons"+"tructor"]は通る。動的なプロパティアクセスがある言語で、識別子の禁止リストは原理的に閉じない。 - 関数として外へ渡った本体は、渡した先の権限で走る。 検査が見ていたのは「式を評価する瞬間」であって、「あとから本体が呼ばれる瞬間」ではない。
CVSS のベクタが S:C(スコープ変更あり)なのはこのためだ。サンドボックスという境界の内側で起きたことが、境界の外側の権限に届いている。9.9 という値の大半はここから来ている。
「認証が要る」を安全側に読み違えない
ベクタは PR:L(低い権限が必要)である。必要なのは「ワークフローを作成または変更できるアカウント」だ。
これを「管理者しか触れないから大丈夫」と読むと危ない。n8n の運用では、ワークフローを編集できることは管理者権限ではなく、ふつうの業務権限として配られている。 自動化の設定を各チームに任せている組織なら、編集者は十数人から数十人になる。
そして n8n は、各種サービスの認証情報を保管して外部へ接続するための道具である。ホストでのコマンド実行は、そのまま保管された資格情報と接続先への到達可能性を意味する。n8n 単体の被害では終わらない。
Node.js で同じ形を再現する
以下は n8n の再現ではない。式サンドボックスという設計に共通する落とし穴を、最小の形で確かめるものだ。実行しても外部へは何も出さない。
1 | |
3 番目が今回の型である。x => x + 1 そのものは無害だが、重要なのはこの本体が「あとで、別の場所から」呼ばれるという一点だ。検査は式を評価する瞬間にしか立っていない。本体が呼ばれる瞬間には、もうどんな検査も残っていない。
2 番目は、文字列による禁止リストが原理的に閉じないことを示している。この 2 つが重なると、禁止リストは飾りになる。
検査をどこに置くか
では、どこに置けば止まるのか。3 つの層がある。
第一に、文字列ではなく構文木を見る。 式をパースして AST にし、許可した形(プロパティアクセス、算術、許可済み関数の呼び出し)だけを通す。禁止リストではなく許可リストにする。["cons"+"tructor"] は「計算されたプロパティアクセス」という形として弾ける。書き方の揺れに追随する必要がなくなるのが、許可リストの本質的な利点である。
第二に、関数値を外へ出さない。 式の評価結果が関数だったら、そこで止める。あるいは高階関数に渡す本体も同じサンドボックスの中で呼ぶ。検査した文脈と実行する文脈を一致させる、というのがこの層の目的だ。
第三に、抜けられる前提で権限を絞る。 サンドボックスは破られうるものとして扱い、破られた先のプロセスに何ができるかを制限する。n8n の場合で言えば、実行ユーザの権限、コンテナの capability、外向き通信の制限がここにあたる。
この 3 つは代替ではなく重なりだ。第一と第二を頑張っても、パーサの実装差やエンジンの更新で穴は開く。第三があれば、開いた穴の被害が小さくなる。
使っているなら何をするか
対応は n8n の更新である。2.31 系なら 2.31.5、2.32 系なら 2.32.1 へ上げる。どちらの系統に乗っているかで上げ先が違う点に注意する。対象は自己ホストの n8n なので、Docker ならイメージのタグ、npm なら n8n --version を確認する。
更新までのあいだの手当ては、ワークフローの編集権限の棚卸しだ。成立条件がその権限なので、持つアカウントを減らせば経路が減る。退職者・異動者のアカウント、検証用の共有アカウント、外部協力者に一時的に渡した権限。この 3 つを見れば大半は片づく。緩和であって修正ではないが、時間は稼げる。
管理画面をインターネットに直接出しているなら、その必要性も再検討する価値がある。ベクタは AV:N で、認証情報が漏れた瞬間にホストでのコマンド実行まで直結する構造になっている。VPN の内側に入れるか IP 制限を掛けるだけで、必要な前提条件が 1 段増える。
侵害の有無を見るなら、n8n プロセスの子プロセスが手掛かりになる。式の評価からシステムコマンドが起動されるので、シェルや curl のような想定外の子プロセスが生えていないかを見る。あわせて、身に覚えのないワークフローの作成・更新がないかを監査ログで確認する。更新しても、更新前に踏まれていた可能性は消えない。疑いがあるなら暗号化キーと保管中の資格情報の再発行を検討する。
まとめ
この脆弱性が教えているのは、サンドボックスの強さではなく検査の位置の話だ。
式を文字列のまま検査して、あとから中身を実行する。この順序である限り、検査を通った式と実際に走る式は別物になりうる。アロー関数はその「あとから」を言語機能として提供しているだけで、悪いのはアロー関数ではない。
自分で式やテンプレートを評価する機能を持っているなら、上のスクリプトの 3 番目を自分の実装で走らせてみるとよい。関数値が外へ出ていくなら、その先に検査は届いていない。
本記事は 2026-09-02 時点で、NVD の CVE-2026-72765 と、影響版・修正版・原因が一致する n8n の GitHub セキュリティアドバイザリ GHSA-gv7g-jm28-cr3m を確認して書いている。なお GHSA 側のページには CVE 番号の記載がないため、両者を同一と断定はせず、記載内容が一致している事実のみを根拠にしている。
このバージョンはその範囲に入るのか
依存の話でいちばん間違えやすいのは ^1.2.3 や ~1.2.3 がどこまでを許すかです(^0.2.3 のようにメジャーが 0 のときは規則が変わります)。範囲とバージョンを貼ると、下限・上限に展開したうえで一致・不一致とその理由を表示するsemver 範囲判定ツールを置いています。ブラウザの中だけで動き、入力はどこにも送信しません。