仮想パッチは、脆弱なコードはそのままに、手前のWAF(Web Application Firewall)で「その脆弱性を突くリクエストの形」だけを止める方法です。2026年10月14日(公表日)に Next.js の脆弱性3件が公開され、うち2件は修正版の 15.5.28/16.4.1 に上げる以外の回避策がありませんでした。こうした緊急パッチが出た日から、依存関係の検証とデプロイを終えるまでの数日を、WAFでどこまで稼げるのか。そしてWAFでは原理的に守れないものは何か。OWASP の WAF エンジン Coraza とルールセット CRS を手元で動かして確かめました。
本記事は連載「事故から逆算するWebセキュリティ」の第15回です。セキュリティ全体の地図は「サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説」にまとめています。
WHAT A WAF STOPS通したもの・止めたもの
WAFは知っている形だけを止める
Coraza+CRS(PL1)
- SQLi1' OR '1'='1 403(スコア5)検索の q に入れる
- XSS二重エンコード 403(スコア15)検索の q にscriptタグ
- B64base64に包んだXSS 200で通過アプリが復号する data
- IDOR他人の注文ID 200で通過/api/orders/1002
- VPテンプレート注入 仮想パッチで403tpl に {{7*7}}
30秒でわかる
- ・仮想パッチとは:既知の脆弱性を突くリクエストをWAFで止め、修正版までの時間を稼ぐ。脆弱性そのものは消えない
- ・CRSの仕組み:ルールが当たるたびに点を足し、合計がしきい値(既定5)以上なら遮断。パラノイアレベルを上げると誤検知が増える
- ・実測:SQLi・XSSは403。base64に包んだXSSとIDORは200で通過。無害な入力9件の誤検知はPL1で0件、PL2で4件
- ・書き方:そのURLのその引数だけを許可リストで狭く書き、ログのみ → 遮断 → 本修正のあと見直す
- ・守れないもの:認可の欠陥・業務ロジック・正規の認証情報での不正ログイン・HTTP以外の経路(カーネルの欠陥など)
仮想パッチとは|修正版までの数日をWAFで稼ぐ
OWASP の Virtual Patching Cheat Sheet は、仮想パッチを「脆弱なコードを変えずに、既知の脆弱性への攻撃の試みを検知して止めるセキュリティポリシーの実施層」と定義しています。同じ箇所で「検査する層が見ている通信と、ルールが認識できる攻撃の形に対してだけ露出を減らす。根本の脆弱性は取り除かない」とも書いています(GitHub の CheatSheetSeries の原文で確認)。
コードを直せば済む話に見えますが、チートシートは直せない事情として、開発者の手が空いていない、第三者製のソフトで手を入れられない、外注で変更に新しい契約が要る、を挙げています。緊急パッチの日に起きるのは、依存関係を上げた版の検証とデプロイに数日かかる、という時間の問題です。
10月の2件で考える
- ・Next.js(10月14日公表):HTTPで届く脆弱性なので、WAFの守備範囲に入る。ただしアドバイザリは攻撃のリクエストの形を書いておらず、狭い仮想パッチを書く材料が無い
- ・PwnEscape(CVE-2026-99999):仮想マシンのゲストが送るパケットをホストのカーネルが処理する欠陥。HTTPを通らないので、WAFは何もできない
それぞれの経緯は「Next.js CVE-2026-96889|SVG経由のRCEと10月の3件、sharpが古いまま残る落とし穴」と「CVE-2026-99999(PwnEscape)とは|さくらが新規受付を止めたLinuxカーネル脆弱性と確認手順」で扱いました。Next.js のアドバイザリが攻撃のリクエストの形を書いていないことは、前者を調べた範囲での確認です。
WAFの置き場所は大きく3つです。CDN やロードバランサーの手前に置くもの、Webサーバーのモジュール(ModSecurity)、アプリに組み込むライブラリ(今回使った Coraza の Go ミドルウェア)です。どれもTLSを終端したあとのHTTPリクエスト(URL・引数・ヘッダー・ボディ)とレスポンスを読みます。逆に言えば、HTTP以外の通信と、暗号化されたまま通り過ぎる通信は見えません。
WAFは何を見ているか——CRSのアノマリスコアとパラノイアレベル
OWASP CRS(Core Rule Set)は、ModSecurity 系の WAF で動く汎用のルールセットです。1本のルールが当たっただけでは止めず、点を足していくのが特徴で、公式ドキュメントはこれをアノマリスコア(collaborative detection)と呼びます。リクエストを調べるルールをすべて走らせたあと、合計がしきい値以上なら遮断します。
| 項目 | CRS 4.25.0 の既定値 | 確認の方法 |
|---|---|---|
| 1回当たったときの点 | critical 5・error 4・warning 3・notice 2 | 配布物の REQUEST-901 を読んだ |
| 遮断のしきい値(受信) | 5 | 同上 |
| パラノイアレベル | 1 | 同上 |
| Coraza 推奨設定のエンジン | DetectionOnly(記録のみで止めない) | coraza.conf-recommended を読んだ |
しきい値が5なので、critical のルールが1本当たれば止まります。パラノイアレベル(PL)は有効にするルールの範囲で、公式ドキュメントは次のように位置づけています。
・PL1:誤検知がほとんど出ない基準。インターネットでHTTPサーバーを動かす全員向け
・PL2:実ユーザーのデータを扱う場合。誤検知が出る前提で調整を覚える
・PL3:オンラインバンキング級。誤検知は「受け入れ、想定する」
・PL4:最重要資産向け。大量の誤検知を覚悟して自己責任で
ドキュメントは「パラノイアレベルとしきい値は直接関係のない別物」と強調しています。前者は動かすルールの数、後者は何点で止めるかです。また Cloudflare の OWASP ルールセット(同社ドキュメントでは CRS 3.3.0 の実装)は、しきい値を 25・40・60(既定40)から選ぶ方式です。同じ「スコア」でも製品ごとに尺度が違うので、数字をそのまま持ち込めません。
Coraza+CRSを手元で動かして確かめた
Go 1.24.7 で、Coraza v3.8.1 と、CRS 4.25.0 を同梱した coraza-coreruleset v4.25.0 を使いました。小さなアプリ(検索・注文API・架空の脆弱エンドポイント)の前に Coraza のミドルウェアを挟み、次の設定を読み込ませています。推奨設定は DetectionOnly なので、遮断させるには SecRuleEngine On を後から書きます。
Include @coraza.conf-recommended
SecRuleEngine On
Include @crs-setup.conf.example
SecAction "id:900000,phase:1,pass,nolog,setvar:tx.blocking_paranoia_level=1"
Include @owasp_crs/*.conf
リクエストは curl で送りました。Host を IP のままにすると「Host ヘッダーが数値のIP」(920350、3点)が全リクエストに乗るので、Host: shop.example を付けています。
B=http://127.0.0.1:8089; H='Host: shop.example'
curl -s -o /dev/null -w '%{http_code}\n' -H "$H" "$B/search?q=1%27%20OR%20%271%27%3D%271"
curl -s -o /dev/null -w '%{http_code}\n' -H "$H" "$B/search?q=%253Cscript%253Ealert(1)%253C%252Fscript%253E"
curl -s -o /dev/null -w '%{http_code}\n' -H "$H" -H 'Cookie: session=user-1001' "$B/api/orders/1002"
結果です(2026年10月17日、括弧内は合計スコア)。
| リクエスト | PL1 | PL2 | 何が当たったか |
|---|---|---|---|
| 普通の検索 | 200 | 200 | なし |
SQLi 1' OR '1'='1 |
403(5) | 403(40) | PL1は libinjection の1本(942100)だけ |
XSS <script> |
403(20) | 403(30) | 941100・941110 ほか |
| 同じXSSを二重URLエンコード | 403(15) | 403(23) | PL2では「多重URLエンコード」(920230)も |
| base64に包んだXSS(アプリが復号) | 200 | 403(5) | PL2の403は「SQL演算子」(942120)で、XSSとしてではない |
| 他人の注文ID(IDOR) | 200 | 200 | なし。PL3・PL4でも200 |
| テンプレート注入 `` | 200 | 403(5) | PL2の AngularJS 向けルール(941380) |
この表は手元の Coraza+CRS(既定の設定)で送った7種類の結果で、自社の WAF が同じように止めるとは限りません。403 が返っても、別の符号化や受け口からの攻撃まで止まるかはこのコマンドだけでは判断できないので、自社の環境で同じリクエストを送って確かめてください。
二重エンコードは、CRS が復号してから照合するので止まりました。一方、アプリが独自に base64 で受ける値は、WAF にとってはただの英数字です。PL2 で止まったのは偶然で、同じ 942120 は無害な「monthly sales report 2026」の base64 も止めました。
FALSE POSITIVES誤検知の実測
パラノイアレベルを上げると無害な入力も止まる
誤検知も測りました。O'Reilly books・Tom & Jerry -- best episodes・price<100 & rating>4 など、引用符・記号を含む無害な文6件と、無害な文の base64 3件です。PL2 で4件が止まり、-- は「SQLコメント」(942440)として扱われました。PL4 では O'Reilly も、最初の「普通の検索」も「厳しい文字集合の外」(920273)で403です。公式ドキュメントは、PL4 の誤検知を潰すのに「何週間もかかりうる」と書いています。
仮想パッチの書き方——狭いルールをログのみから遮断へ
VIRTUAL PATCH LIFECYCLE仮想パッチの一生
狭く書きログだけで流してから止める
-
特定して狭く書く
壊れるURL・引数・値を特定し、正しい値の形を許可リストで書く
-
ログだけで流す
正規の利用者が引っかからないかを見る
-
遮断に切り替える
符号化や別の受け口(JSONなど)でも止まるか確かめる
-
本修正のあと見直す
ルールIDをチケットで追い、外すか残すかを決める
テンプレート注入は PL1 では素通りでした。そこで架空の脆弱エンドポイント /legacy/report の引数 tpl に仮想パッチを当てます。OWASP のチートシートは、攻撃の形を並べる拒否リストより、正しい値の形を書く許可リスト型を推奨しています。報告に載った攻撃文字列だけを止める「エクスプロイト専用の仮想パッチ」は避けるよう明記しています。
SecRule REQUEST_FILENAME "@streq /legacy/report" \
"id:1000001,phase:2,pass,log,auditlog,t:none,msg:'VP: legacy report tpl',chain"
SecRule ARGS:tpl|ARGS:json.tpl "!@rx ^[a-z0-9_]{1,32}$" "t:none"
まず pass,log で流すと、は200のまま記録だけが残りました。`deny,status:403` に替えると、正しい `monthly_sales` は200、 は403です。同じ名前の引数を2つ並べた場合、パスの一部を %72 に符号化した場合、フォーム送信の場合も403で、/search など他の道には影響しませんでした。
最初の版は ARGS:tpl だけで、JSON で送ると200で素通りしました。Coraza の推奨設定は JSON のボディを解析し、引数名を json.tpl にするためです。チートシートが「別の符号化・影響するエンドポイント・検査層を迂回する経路を試してから守れたと言え。1つの攻撃文字列で通ったテストは完全な防御を示さない」と書くのは、こういう穴のことです。
手順の要点は次の4つです。
・特定:アドバイザリやPoCから、どのURLのどの引数にどんな値が来ると壊れるかを書き出す
・ログのみ:誤検知と、止めたときに元へ戻す条件を、遮断の前に決めておく
・遮断:符号化・重複・別の受け口で迂回できないか、自分で試す
・後始末:ルールIDをチケットに残し、本修正のあと外すか残すかを見直す
WAFが守れないもの——IDOR・業務ロジック・HTTPの外
WAF が見ているのは「リクエストが攻撃の形をしているか」だけです。形が正常なまま悪用される欠陥は見えません。OWASP のチートシート自身が「仮想パッチはインジェクション型の欠陥に最も向き、他の種類では十分に攻撃面を減らせないことがある」と書いています。
| 脆弱性の種類 | WAFの効き | 補足 |
|---|---|---|
| SQLi・XSS・コマンド注入 | 効きやすい | CRSの得意分野。実測でも止まった。ただし迂回の研究は続いている |
| 既知のCVE(攻撃の形が公開済み) | 効く | 狭い仮想パッチを書ける。形が非公開なら粗いルールしか書けない |
| アプリ独自の符号化(base64など) | 効きにくい | WAFは復号のしかたを知らない。実測でPL1は素通り |
| IDOR・認可の欠陥 | 効かない | 正しい形のリクエスト。実測でPL1〜4すべて200 |
| 業務ロジック(割引の重ね掛けなど) | 効かない | 値は正常。何が不正かはアプリしか知らない |
| 正規の認証情報での不正ログイン | ほぼ効かない | 形は正常なログイン。回数制限や多要素認証の領域 |
| HTTP以外(カーネル・SSH・DBポート) | 効かない | PwnEscapeはゲストからホストへのパケットで、WAFを通らない |
Cloudflare も自社ドキュメントで、OWASP Top 10 のうち暗号の失敗・安全でない設計・識別と認証の失敗などは「アプリの作りや監視の仕組みに依存する」としています。WAF の守備範囲の外にあるものは、アプリかインフラで直すしかありません。
対策——仮想パッチを「期限つきの借り」として運用する
緊急パッチの日に慌てないための準備と、仮想パッチを入れたあとの扱いです。チートシートは準備段階として、WAFを事前に配置しておくこと、承認の手順を先に決めておくことを挙げています。
・WAFを先に置いておく:導入の承認を事故の日に取りに行かない。普段は記録のみでもよい
・本修正を止めない:仮想パッチは時間稼ぎ。依存関係の更新とデプロイは並行して進める
・ログに秘密を残さない:WAFの記録にはパスワードや決済情報が入りうる。チートシートは記録する項目を絞るよう求めている
・ルールセットも古くなる:今回の coraza-coreruleset v4.25.0 が同梱する CRS は3月28日の 4.25.0。上流は10月2日に 4.30.0 が出ている
・上げるなら検知だけ先に:CRS の detection paranoia level で上のレベルを記録だけ動かし、誤検知を潰してから遮断のレベルを上げる
影響範囲の見極めも WAF の外の仕事です。Next.js の件なら npm ls next sharp で実際の版を確かめ、PwnEscape なら事業者のホストのメンテナンスを待ちます。WAF のログに何も出ていないことは、攻撃が無かったことの証明になりません。
参照ソース
- OWASP Virtual Patching Cheat Sheet(原文は GitHub の CheatSheetSeries で確認)
- OWASP CRS ドキュメント:Anomaly Scoring・Paranoia Levels(GitHub の documentation リポジトリ で確認)
- coreruleset/coreruleset(v4.25.0 と v4.30.0 のタグの日付を確認)
- corazawaf/coraza(v3.8.1)・corazawaf/coraza-coreruleset(v4.25.0)
- Cloudflare WAF:OWASP ruleset concepts(GitHub の cloudflare-docs で確認)





