【図解】WAFとは?APIの入口で危ないリクエストを止めるしくみと誤検知の直し方を初心者向けに解説

前回のレート制限の記事では、入口で「誰が・何回」を数えて止めるしくみを見ました。でも、回数が少なくても、リクエストの中身そのものが危ない形をしていることがあります。

そこで出てくるのが、HTTP の中身を見て危ない形のリクエストを止める WAF(ウェブアプリケーションファイアウォール)です。この記事では、空港の手荷物検査のたとえを使いながら、OWASP CRS と Microsoft のドキュメントで確かめ、テスト用の環境で nginx に ModSecurity と OWASP CRS を組み込んで実際に動かした結果と一緒に、一歩ずつ図にしました。

この記事でわかること
  1. WAF とは何か、ファイアウォールや API ゲートウェイとどう違うのか
  2. ルール集(OWASP CRS)と、点数(アノマリスコア)で止めるしくみ
  3. 記録だけのモードから始めて、止めるモードに切り替える進め方
  4. nginx + ModSecurity で、止まる・通る・誤検知を直すまでを試す方法
  5. Azure での WAF の置き場所(Front Door・Application Gateway)と、WAF だけでは守れないこと

先に覚えておく3つの言葉

最初に、いちばん大事な3つの言葉を、空港の手荷物検査にたとえて押さえておきます。

WAFたとえ:空港の手荷物検査HTTP の中身を見て、危ない形のものを止めるルール(CRS)たとえ:持ち込み禁止品のリスト危ない形を集めた決まり。合うと点数が足される誤検知と除外たとえ:毎回止まる水筒正しいのに止められること。その場所だけ見逃す設定をするこの3つが分かれば、この記事の話はほぼ追える
図1 先に覚えておく3つの言葉

ほかにも、この記事にはいくつか専門の言葉が出てきます。先に、ざっくりした意味を並べておきます。読んでいて迷ったら、ここに戻ってきてください。

言葉ざっくりした意味空港でたとえると
ModSecurityオープンソースの WAF のエンジン。nginx などに組み込んで使う検査の機械
OWASP CRSModSecurity などで使う、攻撃の形を集めたルール集持ち込み禁止品のリスト
アノマリスコア合ったルールの点数の合計。しきい値以上で止める怪しさの点数
しきい値止めるかどうかを分ける点数。CRS の初期値は 5「この点数から別室へ」
誤検知正しいリクエストなのに、ルールに合って止められることただの水筒で止められる
除外決めた場所・欄・ルールだけ、照らさないようにする設定「この水筒は確認済み」

この記事の地図

ひとことで言うと

WAF は入口でリクエストの中身を照らし、危ない形のものは奥に届く前に 403 で止めます。

先に全体の地図を見ておくと、迷いにくくなります。

外アプリふつうのリクエスト怪しい通信危ない形の中身WAF① 中身(URL・本文)を見る② ルールと照らす③ 合ったら点数を足す④ しきい値以上なら 403API ゲートウェイ誰か・回数(前の記事)バックエンド API権限・業務の確認403 Forbidden危ない形のリクエストは、奥に届く前に止まるWAF は入口でリクエストの「中身の形」を見る係
図2 この記事の地図

WAF は、URL や本文などの中身をルールと照らし、合ったら点数を足します。合計がしきい値以上なら 403 を返し、奥には送りません。通ったリクエストは、API ゲートウェイで「誰か・回数」を確かめられ、最後に奥の API で「この人がこのデータを扱ってよいか」を確かめられます。WAF は、その前に立つ係です。

まずはたとえで:空港の手荷物検査

ひとことで言うと

WAF は、API の入口でする「手荷物検査」です。

手荷物検査がない空港を思い浮かべてください。危ない物もそのまま機内に持ち込まれ、乗務員が1人ずつ確かめるしかありません。見落としたら、機内で困ります。

手荷物検査なし危ない物もそのまま機内へ乗務員が1人ずつ確かめるしかない見落としたら、機内で困るアプリ(乗務員)が全部の入力を1つずつ確かめる手荷物検査あり禁止品のリストと照らして止める乗る前に、入口でまとめて確かめるリストは専門家が更新してくれるただし、搭乗券(権限)の確認は別の係(認証・認可)の仕事WAF は、API の入口でする「手荷物検査」
図3 たとえで見る WAF

手荷物検査がある空港では、乗る前に入口で、持ち込み禁止品のリストと照らして止めます。リストは専門家が更新してくれます。ただし、搭乗券を見て「この便に乗ってよいか」を確かめるのは別の係です。WAF も同じで、中身の形は見ますが、「この人がこの操作をしてよいか」は見ません。

WAF とは何か、ほかの係との違い

ひとことで言うと

ファイアウォールは「どこから来たか」、WAF は「中身の形」、API ゲートウェイは「誰か・回数」を見ます。

Microsoft のドキュメントは、Azure Web Application Firewall を、よく知られた攻撃や脆弱性からウェブアプリケーションを1か所でまとめて守るもの、と説明しています。そして、SQL インジェクションやクロスサイトスクリプティングは、もっともよくある攻撃の1つだとしています。また、IP アドレスやポートなどネットワークの層を見る Azure Firewall と違い、WAF は HTTP の層だけで動き、ヘッダ・クエリ・本文・Cookie の意味を理解して照らす、と書き分けています。

係見るもの確かめることファイアウォールIP アドレス・ポートこの住所・この入口から来てよいかWAFHTTP の中身(URL・ヘッダ・本文)危ない形の文字列が入っていないかAPI ゲートウェイキー・トークン・回数誰か、使いすぎていないかアプリ(API)業務のデータと権限この人がこのデータを見てよいか係ごとに見るものが違う。重ねて置くと、すき間が減る
図4 何を見て止めるかの違い
係止めるもの空港でたとえると
ファイアウォール来てはいけない住所・入口からの通信空港の門の警備
WAF中身に危ない形が入ったリクエスト手荷物検査
API ゲートウェイキーのない相手、使いすぎの相手搭乗券の確認・人数の整理
アプリ(API)権限のない操作機内での席の確認

WAF が照らす場所

HTTP リクエストリクエスト行POST /qa/posts?lang=jaヘッダContent-Type: application/jsonCookiesession=…本文(JSON など){“title”:”…”,”body”:”…”}URL・クエリヘッダの値Cookie の値本文の各項目WAF が照らす場所URL だけでなく、ヘッダや本文の中身まで照らして確かめる
図5 WAF が見る場所

WAF は、URL だけでなく、ヘッダや Cookie、本文の中身まで照らします。API では本文が JSON のことが多いので、WAF が JSON を項目ごとに読めるかどうかが大事になります(STEP 4 で確かめます)。また、暗号化された HTTPS のままでは中身が読めないので、WAF の手前か WAF 自身で TLS をほどく(終端する)必要があります。Microsoft のドキュメントも、Front Door の WAF は TLS をほどいたあとのリクエストを照らす、と説明しています。

ルール集(OWASP CRS)

ひとことで言うと

CRS は、よくある攻撃の形をグループごとにまとめた、無料で使えるルール集です。

OWASP CRS(Core Rule Set)は、OWASP が公開している「ModSecurity や互換の WAF で使う、汎用の攻撃検知ルール集」です。SQL インジェクション、クロスサイトスクリプティング、ファイルの読み取りなどから守ります。ルールには6桁の番号が付いていて、上の3桁でグループが分かります。

ねらい(攻撃の種類)ルール番号どんな形か(ざっくり)SQL インジェクション942xxxデータベースへの命令をまぎれ込ませるクロスサイトスクリプティング(XSS)941xxx画面に動くスクリプトをまぎれ込ませるファイルの読み取り(LFI)930xxxサーバの中のファイルを読もうとする命令の実行(RCE)932xxxサーバでコマンドを動かそうとするプロトコル違反920xxxHTTP の決まりから外れた形スキャナの検知913xxx脆弱性を探す道具らしい通信CRS は、よくある攻撃の形をグループごとにまとめたルール集
図6 OWASP CRS の主なルールのグループ

Azure の WAF のマネージドルール(DRS)も、この OWASP CRS をもとにしています。そのため、Azure の WAF のログでも、同じ 942100 や 949110 といった番号を目にします。

なお、ModSecurity は長くトラストウェーブ社が管理してきましたが、2024年1月に OWASP に引き継がれました。

点数で止めるしくみ(アノマリスコア)

ひとことで言うと

1つのルールに合っただけでは止めず、合計の点数がしきい値以上なら止めます。

ルールの重さ(severity)と足される点数CRITICAL+5ERROR+4WARNING+3NOTICE+2例:テスト用の文字列を送ったときSQL インジェクションの形942100 +5合計 5XSS の形941100 +5941110 +5941160 +5合計 15合計が しきい値(初期値 5)以上 → 949110 が 403 で止める。5 未満なら通す1つのルールでは止めず、合計の点数で止めるかを決める
図7 点数(アノマリスコア)のしくみ

CRS のドキュメントによると、ルールに合っても、その場では止めません。合ったルールの重さに応じて点数を足していき、リクエストの本文まで照らし終わったところで、合計がしきい値以上かを見て止めるかを決めます(そこで止めるのが 949110 というルールです)。しきい値の初期値は、リクエストが 5、レスポンスが 4 です。

重さ(severity)足される点数どういうことか
CRITICAL51つ合うだけで、しきい値 5 に届く
ERROR4ほかのルールと合わせて届く
WARNING3ほかのルールと合わせて届く
NOTICE2単独では届かない

こうすると、少し怪しいだけのリクエストは通し、怪しい点がいくつも重なったリクエストだけを止められます。空港でいえば、「ベルトのバックルが鳴っただけでは止めないが、いくつも重なったら別室で確かめる」ような考え方です。

厳しさの段階(パラノイアレベル)

PL1基本誤検知がほとんどないPL2実データを扱う誤検知の調整が要るPL3銀行なみ誤検知が多いPL4とても厳しい誤検知がとても多い初期値ゆるいきびしい上げるほど見逃しは減るが、正しい通信も止まりやすいまずは PL1 で始め、必要なところだけ上げる
図8 パラノイアレベル

CRS には、どこまで厳しく照らすかを決めるパラノイアレベル(PL)が4段階あります。CRS のドキュメントは、上げるほど見逃しは減るが、正しい通信まで止める誤検知が増える、と説明しています。テスト用の環境の CRS でも、初期値は PL1 でした(STEP 1 で確かめます)。

動かし方(モード)

ModSecurityAzure WAF動き使いどころOff―何もしない動かしていないのと同じDetectionOnlyDetection記録だけ(止めない)最初に入れるとき・調整するときOnPrevention記録して、止める調整が終わった本番まず「記録だけ」で入れ、誤検知を直してから「止める」に切り替える
図9 WAF の動かし方(モード)

WAF には、止めずに記録だけするモードと、記録して止めるモードがあります。テスト用の環境に入った ModSecurity の推奨設定ファイルにも、「導入直後の混乱を減らすため、最初は記録だけ(DetectionOnly)で始めるように」と書かれていました。

全体の流れ

ここからは、注文 API の前に WAF を置く例で、流れを順に見ます。準備、通るとき、止めるときの3つです。

準備:記録だけで始めて、誤検知を直してから止める

あらすじ:管理者はルール集を入れ、まず記録だけで動かします。記録を読んで誤検知だけを狭く除外し、それから止めるモードに切り替えます。

管理者WAFAPI(バックエンド)① ルール集(CRS)を入れる② 記録だけで動かすDetectionOnly③ 本番の通信を照らして記録止めずに通すそのまま転送④ 記録を読む正しい通信が引っかかっていないか⑤ 誤検知だけ除外するこの場所・この欄・このルール⑥ 止めるに切り替えるOn(Prevention)いきなり止めず、記録で確かめてから止める
図10 準備の流れ(①〜⑥)
手順誰が・どこで何をするかルール・記録はどうなるか
①管理者 → WAFルール集(CRS)を入れる攻撃の形のリストが入る
②管理者 → WAF記録だけで動かすDetectionOnly(Azure では Detection)
③WAF本番の通信を照らして記録する止めずに奥へ通す
④管理者記録を読む正しい通信が引っかかっていないかを見る
⑤管理者 → WAF誤検知だけを除外する場所・欄・ルールを絞る
⑥管理者 → WAF止めるモードに切り替えるOn(Azure では Prevention)

通るとき:照らしても点数がたまらない

あらすじ:ふつうのリクエストは、ヘッダも本文も照らされますが、合うルールがないので点数がたまらず、そのまま奥に届きます。

利用者のアプリWAFバックエンドAPI⑦ POST /orders/new{“item”:”apple”}⑧ ヘッダを照らすプロトコル違反などを確かめる⑨ 本文を読み、照らすJSON の各項目を確かめる⑩ 点数を合計する合計 0 → しきい値 5 未満⑪ 転送⑫ 仕事をする⑬ レスポンス⑭ 200ふつうのリクエストは、照らしても点数がたまらないので、そのまま通る
図11 通るときの流れ(⑦〜⑭)
手順誰が・どこで何をするか点数はどうなるか
⑦アプリ → WAFJSON の本文を付けて送る―
⑧WAFヘッダを照らすプロトコル違反などがなければ 0
⑨WAF本文を読み、項目ごとに照らす危ない形がなければ 0
⑩WAF点数を合計する0 → しきい値 5 未満
⑪WAF → API転送する―
⑫バックエンド API仕事をする―
⑬⑭API → WAF → アプリレスポンスを返す200

止めるとき:点数がしきい値に届いたら 403

あらすじ:危ない形の文字列が入っていると、合ったルールの点数が足されます。合計がしきい値に届くと、WAF は奥に送らずに 403 を返し、記録を残します。

怪しい通信WAFバックエンドAPI❶ 危ない形の文字列クエリや本文に入っている❷ ルール 942100 に合う+5 点❸ 合計 5 ≥ しきい値 5949110 が止めると決める奥には届かない❹ 403 Forbidden❺ 記録するルール ID・点数・URL❻ 何度送っても同じ形なら 403危ない形は入口で止まり、奥の API はそのリクエストを見ない
図12 止めるときの流れ(❶〜❻)
手順最初の図ではこの図では
❶⑦ ふつうの本文危ない形の文字列が入っている
❷⑨ 合うルールがない942100 に合い、+5 点
❸⑩ 合計 0合計 5 ≥ しきい値 5。949110 が止めると決める
❹⑪〜⑭ 転送して 200奥へは送らずに 403
❺―ルール ID・点数・URL を記録する

誤検知と除外

ひとことで言うと

誤検知は、ルールごと止めずに「場所・欄・ルール」を絞って除外します。

除外する前Q&A の投稿(正しい通信)body: “SELECT name FROM …”942100 に合う → 合計 5403SQL の質問なのに止められる(誤検知)狭く除外した後場所/qa/posts だけ欄body だけルール942100 だけbody の SQL の質問200title に危ない形403ほかの欄・ほかのルールは見たまま誤検知は「場所・欄・ルール」を絞って除外する。ルールごと止めない
図13 誤検知と除外

CRS のドキュメントは、正しいリクエストがルールに合ってしまうことを誤検知(false positive)と呼び、ルールの除外を書いて直すよう案内しています。CRS のルールファイルそのものを書き換えると、ルールを更新したときに消えてしまうので勧めていません。

直し方どういうものか気をつけること
実行時の除外ctl:ruleRemoveTargetById などで、決めたリクエストのときだけ、決めた欄をそのルールから外すCRS より前に書く
起動時の除外SecRuleRemoveById などで、起動したときからルールを外すすべてのリクエストに効く
ルールを止めるそのルールを使わないその攻撃の形を、どこでも見なくなる

除外は、狭ければ狭いほど安全です。この記事の「試してみる」では、「Q&A の投稿 API(/qa/posts)の、本文の body 欄だけ、ルール 942100 だけ」を外します。

WAF だけでは守れないこと

ひとことで言うと

WAF は「形」を見る係です。「この人がこのデータを扱ってよいか」は見ません。

WAF が得意なことよく知られた攻撃の形を止めるアプリを直す前の応急の守り全部の API の入口をまとめて守る記録から、狙われ方が分かるWAF だけでは守れないことこの人がこのデータを見てよいか→ 認可(次の記事)正しい形のまま悪用される→ 業務の確認中身が読めない通信→ TLS を WAF で終端するアプリそのものの弱さ→ コードを直すWAF は守りの1枚。認証・認可やアプリの修正と重ねて使う
図14 WAF が得意なことと、WAF だけでは守れないこと

Microsoft のドキュメントは、攻撃をアプリのコードの中だけで防ぐのは難しく、WAF なら1か所でまとめて、よく知られた脆弱性への手当てを早くできる、と説明しています。一方で、WAF はリクエストの形を照らすものなので、正しい形のまま他人の注文番号を指定するような使い方は、形だけでは見分けられません。そうした「認可」の弱さは、次の記事の OWASP API Security Top 10 で扱います。WAF は、認証・認可やアプリの修正と重ねて使う守りの1枚です。

試してみる(nginx + ModSecurity + OWASP CRS)

ここからは、テスト用の環境で、nginx 1.24.0 に ModSecurity を組み込み、OWASP CRS を読み込ませて動かした結果を見ていきます。奥の API には、API ゲートウェイの記事で使ったものに POST を受ける処理を足した、Python の小さなサーバを使います。

使った版

Ubuntu 24.04 のパッケージで入れた、ModSecurity 3.0.12、nginx 用のコネクタ 1.0.3、OWASP CRS 3.3.5 です。2026年11月の時点で、CRS の最新は 4 系(OWASP のプロジェクトページでは 4.29.0)なので、本番で使うときは新しい版を確かめてください。

試験で送る 1' OR '1'='1 や <script>alert(1)</script> は、WAF が動いているかを確かめるときによく使われる、SQL インジェクションや XSS の形をまねたテスト用の文字列です。自分で管理していない環境には送らないでください。

WAF を組み込んで動かす(図10の①)

確かめること:nginx に ModSecurity が組み込まれ、CRS のルールが読み込まれること。

やっていること:入口に検査の機械を置き、禁止品のリストを読み込ませます。

waf.confnginx の設定modsecurity onmodsecurity_rules_file main.confmain.conf読み込む順番1. modsecurity.confエンジンの設定(On・本文を読む・記録)2. crs-setup.confCRS の設定(しきい値など)3. exclusions.conf除外ルール(STEP 6 で書く)4. rules/*.confCRS のルール本体nginx に ModSecurity を組み込み、CRS のルールを読み込ませる
図15 STEP 1 の設定の組み立て

waf.conf(nginx の設定)

load_module /usr/lib/nginx/modules/ngx_http_modsecurity_module.so;
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {}

http {
    access_log logs/access.log;

    server {
        listen 127.0.0.1:8080;
        modsecurity on;
        modsecurity_rules_file /srv/waf/modsec/main.conf;

        location /orders/ {
            proxy_pass http://127.0.0.1:9001/;
        }

        location /qa/ {
            proxy_pass http://127.0.0.1:9002/;
        }

        location /detect/ {
            modsecurity_rules 'SecRuleEngine DetectionOnly';
            proxy_pass http://127.0.0.1:9001/;
        }
    }
}
  • load_module:ModSecurity を nginx に組み込む
  • modsecurity on:この server で WAF を使う
  • modsecurity_rules_file:読み込むルールのファイル(main.conf)
  • /orders/・/qa/:注文 API と Q&A の投稿 API に渡す
  • /detect/:この場所だけ記録だけのモードにする(STEP 5)

modsec/main.conf(読み込む順番)

Include /srv/waf/modsec/modsecurity.conf
Include /etc/modsecurity/crs/crs-setup.conf
Include /srv/waf/modsec/exclusions.conf
Include /usr/share/modsecurity-crs/rules/*.conf

除外ルールの exclusions.conf は、CRS のドキュメントのとおり、CRS のルール本体より前に読み込ませています。この時点では中身は空です。

奥の API(注文 API 9001番、Q&A 投稿 API 9002番)を動かす

python3 backend.py orders 9001 &
python3 backend.py qa 9002 &

エンジンの主な設定を確かめる

grep -E "^SecRuleEngine|^SecRequestBodyAccess|^SecAuditEngine|^SecAuditLog " modsec/modsecurity.conf
SecRuleEngine On
SecRequestBodyAccess On
SecAuditEngine RelevantOnly
SecAuditLog /srv/waf/logs/audit.log

CRS の初期値(しきい値・パラノイアレベル)を確かめる

grep -n "threshold=\|tx.paranoia_level=1\|critical_anomaly_score=" /usr/share/modsecurity-crs/rules/REQUEST-901-INITIALIZATION.conf
81:    setvar:'tx.inbound_anomaly_score_threshold=5'"
90:    setvar:'tx.outbound_anomaly_score_threshold=4'"
99:    setvar:'tx.paranoia_level=1'"
126:    setvar:'tx.critical_anomaly_score=5'"

設定を確かめて、動かす

nginx -t -p /srv/waf/ -c waf.conf
2026/11/02 15:35:58 [notice] 1121#1121: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/921/0)
nginx: the configuration file /srv/waf/waf.conf syntax is ok
nginx: configuration file /srv/waf/waf.conf test is successful
nginx -p /srv/waf/ -c waf.conf
2026/11/02 15:35:58 [notice] 1122#1122: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/921/0)

結果の読み方:

  • SecRuleEngine On は止めるモード、SecRequestBodyAccess On は本文も読む設定です。記録(監査ログ)は、引っかかったリクエストだけを残す設定(RelevantOnly)です。
  • CRS の初期値は、リクエストのしきい値が 5、レスポンスが 4、パラノイアレベルが 1、CRITICAL のルールが +5 点でした。
  • rules loaded … 0/921/0 は、ファイルから 921 個のルールを読み込んだ、という意味です。

ふつうのリクエストは通る(図11の⑦〜⑭)

確かめること:WAF を入れても、ふつうのリクエストはこれまでどおり通ること。

やっていること:ふつうの荷物を持った人が、手荷物検査を通ります。

GET(クエリ)/orders/list?item=apple200通るPOST(JSON の本文){“item”:”apple”,”qty”:2}200通るふつうのリクエストは、WAF を入れても今までどおり通る
図16 STEP 2 の様子

クエリ付きの GET

curl -s "http://localhost:8080/orders/list?item=apple"
{"service": "orders", "method": "GET", "path": "/list?item=apple"}

JSON の本文付きの POST

curl -s -H "Content-Type: application/json" -d '{"item":"apple","qty":2}' http://localhost:8080/orders/new
{"service": "orders", "method": "POST", "path": "/new", "received_bytes": 24}

結果の読み方:どちらも奥の注文 API まで届き、返事が返ってきました。照らしても合うルールがなかったので、点数がたまらなかったということです。

危ない形は止まる(図12の❶〜❺)

確かめること:危ない形の文字列が入ると 403 で止まり、どのルールに合ったかが記録に残ること。

やっていること:禁止品の形をした荷物を持って、検査を通ろうとします。

SQL インジェクションの形(テスト用)?item=1′ OR ‘1’=’1403合計 5XSS の形(テスト用)?item=<script>…403合計 15記録:942100(+5)→ 949110 / 941100・941110・941160(+5 ×3)→ 949110危ない形が入ると、合ったルールの点数が足され、5 以上で 403
図17 STEP 3 の様子

テスト用の文字列をクエリに入れて送る(番号だけ表示)

curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:8080/orders/list?item=1'%20OR%20'1'%3D'1"
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:8080/orders/list?item=%3Cscript%3Ealert(1)%3C/script%3E"
403
403

監査ログで、合ったルールを見る

grep -oE '\[id "[0-9]+"\] \[rev "[^"]*"\] \[msg "[^"]*"\]' logs/audit.log
[id "942100"] [rev ""] [msg "SQL Injection Attack Detected via libinjection"]
[id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"]
[id "941100"] [rev ""] [msg "XSS Attack Detected via libinjection"]
[id "941110"] [rev ""] [msg "XSS Filter - Category 1: Script Tag Vector"]
[id "941160"] [rev ""] [msg "NoScript XSS InjectionChecker: HTML Injection"]
[id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 15)"]

エラーログで、止めた記録を見る

tail -n 1 logs/error.log | grep -oE 'Access denied with code 403 \(phase 2\)|\[msg "[^"]*"\]|\[uri "[^"]*"\]'
Access denied with code 403 (phase 2)
[msg "Inbound Anomaly Score Exceeded (Total Score: 15)"]
[uri "/orders/list"]

結果の読み方:

  • どちらも 403 でした。奥の注文 API には届いていません。
  • SQL インジェクションの形は、942100(libinjection という部品で SQL インジェクションを見つけるルール)に合い、合計 5 点で 949110 が止めました。
  • XSS の形は、941100・941110・941160 の3つに合い、合計 15 点でした。1つでもしきい値に届きますが、合ったルールの点数はすべて足されます。
  • エラーログには、止めた理由が 949110 の「Inbound Anomaly Score Exceeded(点数の合計がしきい値を超えた)」として残ります。本当の原因(942100 など)は監査ログで見ます。

JSON の本文の中も照らす(図11の⑨)

確かめること:URL だけでなく、JSON の本文の中に危ない形があっても止まること。

やっていること:禁止品を、かばんの奥(本文)に入れて通ろうとします。

POST(JSON の本文に危ない形){“item”:”1′ OR ‘1’=’1″}403本文も見るSecRequestBodyAccess On + JSON の本文を読む設定(200001)があるので、項目ごとに照らせるURL だけでなく、JSON の本文の中も照らしている
図18 STEP 4 の様子

JSON の本文にテスト用の文字列を入れて送る

curl -s -o /dev/null -w "%{http_code}\n" -H "Content-Type: application/json" -d "{\"item\":\"1' OR '1'='1\"}" http://localhost:8080/orders/new
403

結果の読み方:本文の中でも 403 で止まりました。推奨設定ファイルには、Content-Type が application/json のときに本文を JSON として読む設定(ルール 200001)が入っていて、JSON の項目を1つずつ照らせます。API の前に WAF を置くときは、本文を読む設定が入っているか、送ってくる Content-Type が正しいかを確かめておきます。

記録だけのモード(図10の②③)

確かめること:DetectionOnly では止めずに通しながら、止めるはずだったことを記録すること。

やっていること:検査の機械は鳴らすけれど、通してしまい、鳴ったことだけを記録します。

/detect/(DetectionOnly)に同じ形?item=1′ OR ‘1’=’1200止めない記録:ModSecurity: Warning. detected SQLi using libinjection. Inbound Anomaly Score Exceeded (Total Score: 5)記録だけのモードでは、止めるはずだったものを Warning として残す
図19 STEP 5 の様子

記録だけの /detect/ に、同じテスト用の文字列を送る

curl -s -w "%{http_code}\n" "http://localhost:8080/detect/list?item=1'%20OR%20'1'%3D'1"
{"service": "orders", "method": "GET", "path": "/list?item=1'%20OR%20'1'%3D'1"}
200

監査ログの最後を見る

grep -oE 'ModSecurity: (Warning|Access denied)[^[]{0,40}|\[msg "[^"]*"\]' logs/audit.log | tail -n 4
ModSecurity: Warning. detected SQLi using libinjection. 
[msg "SQL Injection Attack Detected via libinjection"]
ModSecurity: Warning. Matched "Operator `Ge' with parameter 
[msg "Inbound Anomaly Score Exceeded (Total Score: 5)"]

結果の読み方:STEP 3 と同じ形なのに、今度は 200 で奥まで届きました。それでも監査ログには、Warning(警告)として 942100 と、合計 5 点だったことが残っています。止めるモードに切り替える前に、この記録を読んで、正しい通信が引っかかっていないかを確かめます。

誤検知を、狭く除外して直す(図10の④⑤)

確かめること:正しいリクエストが止められたとき、場所・欄・ルールを絞って除外すれば通り、ほかは守られたままになること。

やっていること:「この人のこの水筒は確認済み」と登録し、ほかの荷物はこれまでどおり検査します。

除外する前Q&A の投稿(body に SQL の文){“title”:”How to write SQL”,”body”:”SELECT …”}403誤検知exclusions.conf を書いて reload した後同じ投稿{“title”:”How to write SQL”,”body”:”SELECT …”}200通るtitle に危ない形{“title”:”1′ OR ‘1’=’1″,”body”:”hello”}403止まる除外は「/qa/posts の body の 942100」だけ。ほかはこれまでどおり守る
図20 STEP 6 の様子

社内の Q&A 掲示板の API に、「SQL の書き方」の質問を投稿するとします。本文に SQL の文が入るのは、この API では正しい使い方です。

SQL の文を本文(body 欄)に入れて、Q&A に投稿する

curl -s -w "%{http_code}\n" -H "Content-Type: application/json" -d '{"title":"How to write SQL","body":"SELECT name FROM products WHERE price < 100"}' http://localhost:8080/qa/posts
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
403

監査ログで、合ったルールを見る

grep -oE '\[id "[0-9]+"\] \[rev "[^"]*"\] \[msg "[^"]*"\]' logs/audit.log | tail -n 2
[id "942100"] [rev ""] [msg "SQL Injection Attack Detected via libinjection"]
[id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"]

正しい質問なのに、942100 に合って止められてしまいました。誤検知です。そこで、除外ルールを書きます。

modsec/exclusions.conf(除外ルール)

SecRule REQUEST_URI "@beginsWith /qa/posts" \
    "id:10001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:json.body"

「URL が /qa/posts で始まるときだけ、ルール 942100 から、JSON の body 欄(ARGS:json.body)を外す」という意味です。欄の名前は、監査ログの中の ARGS:json.body という表示で確かめました。

設定を読み直す

nginx -s reload -p /srv/waf/ -c waf.conf
2026/11/02 15:35:58 [notice] 1142#1142: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/922/0)
2026/11/02 15:35:58 [notice] 1142#1142: signal process started

同じ投稿をもう一度送る

curl -s -w "%{http_code}\n" -H "Content-Type: application/json" -d '{"title":"How to write SQL","body":"SELECT name FROM products WHERE price < 100"}' http://localhost:8080/qa/posts
{"service": "qa", "method": "POST", "path": "/posts", "received_bytes": 81}
200

今度は title 欄にテスト用の文字列を入れて送る

curl -s -o /dev/null -w "%{http_code}\n" -H "Content-Type: application/json" -d "{\"title\":\"1' OR '1'='1\",\"body\":\"hello\"}" http://localhost:8080/qa/posts
403

結果の読み方:

  • 読み込んだルールが 921 個から 922 個に増えました。除外ルールが1つ足されたからです。
  • 同じ質問の投稿が、今度は 200 で Q&A の API まで届きました。
  • title 欄に危ない形を入れると、同じ /qa/posts でも 403 で止まりました。外したのは body 欄の 942100 だけなので、ほかの欄はこれまでどおり守られています。

ここからは Microsoft の場合です

Azure での WAF の置き場所

ひとことで言うと

Azure では、WAF は Front Door(世界の入口)か Application Gateway(リージョンの入口)に付けます。

利用者Front Door+ WAF世界の入口(エッジ)複数リージョン向けどちらか一方、または両方を重ねて置くApplicationGateway + WAFリージョンの入口仮想ネットワークの前API ManagementバックエンドWAF ポリシー:マネージドルール(DRS)・カスタムルール(先に評価)・除外・Detection/PreventionMicrosoft の設計ガイドでは、API Management の前に Application Gateway の WAF を置く構成を紹介Azure では、WAF は Front Door か Application Gateway に付ける
図21 Azure での WAF の置き場所

Microsoft のドキュメントによると、Azure Web Application Firewall は、Application Gateway、Application Gateway for Containers、Front Door などと組み合わせて使います。

置き場所特徴向いているとき
Front Door の WAF世界中のエッジで照らす。仮想ネットワークに入る前に止める複数のリージョンに置いたアプリ
Application Gateway の WAFリージョンごとの入口。仮想ネットワークの前に置く1つのリージョンのアプリ・API Management の前
両方を重ねるFront Door で広く守り、Application Gateway でリージョンごとに調整するリージョンごとに違う守り方が要るとき

Microsoft のアーキテクチャガイド「Protect APIs by using Application Gateway and API Management」では、外からの通信を Application Gateway の WAF で照らしてから、仮想ネットワークの中の API Management に渡す構成を紹介しています。Front Door の WAF は、Premium ではすべての機能が使えますが、Standard ではカスタムルールだけです。

WAF ポリシーの中身

項目内容この記事の試験でいうと
マネージドルールDRS(Default Rule Set)など。新しく作るなら DRS 2.2 が勧められていて、OWASP CRS 3.3.4 に Microsoft の脅威インテリジェンスのルールを足したもの。点数で止める方式OWASP CRS 3.3.5
カスタムルール自分で書くルール。マネージドルールより先に評価され、合えばそこで決まる―
除外ルールセット・グループ・ルールの単位で、決めた欄を外すexclusions.conf
モードDetection(記録だけ)と Prevention(止める)DetectionOnly と On

Application Gateway の WAF でも、点数のしくみは CRS と同じです。重さごとの点数は CRITICAL 5・ERROR 4・WARNING 3・NOTICE 2 で、合計が 5 以上なら、Prevention モードでは止め、Detection モードでは記録だけします。ログでは、点数を足したルールが Matched、止めると決めたルールが Blocked または Detected と表示されます。パラノイアレベルは、Azure の WAF では PL1 と PL2 までで、PL3・PL4 は使えません。

Microsoft のドキュメントは、マネージドルールは WAF ポリシーで Detection モードのまま有効になるのが初期値で、切り替えのときはまず Detection で正しい通信を確かめてから止めるようにと案内しています。一方で、本番を Detection のまま置いておかないように、とも注意しています。

誤検知の実例として、Microsoft の API Management のドキュメントは、Application Gateway の WAF のルール 942200 が、開発者ポータルからの OpenAPI の定義のダウンロードを止めてしまうことがあると注意しています。API の前に WAF を置くときは、この記事の STEP 6 のような調整が要る、ということです。

よくある間違い

間違いどうするか
いきなり止めるモードで入れる記録だけで始め、記録を読んで誤検知を直してから止める
記録だけのモードのまま本番を続ける調整が終わったら止めるモードに切り替える
誤検知のたびに、ルールごと止める場所・欄・ルールを絞って除外する
949110 を除外する949110 は合計で止めるだけのルール。その前に合ったルール(942100 など)を見る
CRS のルールファイルを直接書き換える除外は別のファイルに書く。更新で消えない
WAF を置いたので、アプリの入力の確認をやめるWAF は守りの1枚。アプリ側でも入力と権限を確かめる
WAF を通らずに奥へ届く道が残っている奥の API は WAF(Front Door など)からの通信だけを受ける

949110 については、Microsoft のドキュメントも、点数で止めるルールではなく、点数を足した元のルールを除外するようにと注意しています。また、Front Door の WAF を使うときは、奥のサーバが Front Door からの通信だけを受けるようにしておかないと、WAF を飛ばして直接届いてしまう、と説明しています。

うまくいかないとき

WAF がうまくいかないときは、まず記録で「どのルールに合ったか」と「どの欄か」を確かめます。

困っていることまず確かめること正しいリクエストが 403記録のルール ID と欄(ARGS:…)949110 だけが見えるその前に合ったルール(942100 など)危ない形なのに通るDetectionOnly のまま・本文を読む設定JSON の本文が見られていないContent-Type と本文の読み方の設定WAF を飛ばして届く奥の API が WAF からだけ受けるか記録に秘密が残るログの項目を伏せる設定403 の理由は、949110 ではなく、その前に合ったルールにある
図22 WAF がうまくいかないとき
困っていることよくある原因確認すること
正しいリクエストが 403誤検知監査ログ(Azure では WAF のログ)のルール ID と欄の名前(ARGS:…)
949110 だけが見える合計で止めたルールだけを見ている同じリクエストで、その前に合ったルール
危ない形なのに通る記録だけのモードのまま。本文を読んでいないSecRuleEngine(Azure ではモード)と SecRequestBodyAccess
JSON の本文が照らされていないContent-Type が JSON になっていない送ってくる Content-Type と、本文を読む設定
WAF を飛ばして届く奥の API が外から直接受けている奥の API の受け付け元の制限
記録に秘密が残る本文やヘッダがそのまま記録されるログの項目を伏せる設定(Azure ではログのスクラブ)

WAF の記録には、リクエストの中身がそのまま残ることがあります。Microsoft のドキュメントも、WAF のログには Authorization ヘッダや Cookie、本文などが含まれうるので、伏せる設定(スクラブ)をするよう勧めています。この記事の試験の監査ログにも、送った本文がそのまま残っていました。

まとめ

項目内容空港でたとえると
WAFHTTP の中身を照らし、危ない形のリクエストを入口で止める手荷物検査
ルール集OWASP CRS。Azure の DRS もこれがもと持ち込み禁止品のリスト
止め方合ったルールの点数を合計し、しきい値(5)以上なら 403怪しさが重なったら別室へ
進め方記録だけで始め、誤検知を直してから止める試行期間のあと本運用
誤検知場所・欄・ルールを絞って除外する「この水筒は確認済み」
AzureFront Door か Application Gateway に WAF ポリシーを付ける空港の入口か、ターミナルの入口
限界権限や業務の正しさは見ない。認証・認可と重ねる搭乗券の確認は別の係

参考にした資料

この記事は、次の公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。

あとがき

実際に動かしてみて印象に残ったのは、エラーログには 949110(合計で止めた)しか出ず、本当の理由の 942100 は監査ログを見ないと分からないことでした。誤検知を直すときに 949110 を外したくなる気持ちも分かりますが、それでは WAF が何も止めなくなってしまいます。また、SQL の質問が止められたように、「API によっては正しい入力」が必ず出てくるので、記録だけのモードで様子を見る期間は省けないと感じました。次の記事では、WAF だけでは守れない「認可」の弱さも含めて、OWASP API Security Top 10 を見ていきます。

コメント