※本記事にはアフィリエイトリンクを含みます。リンク経由で申し込みがあった場合、筆者に報酬が入ります。
記事を保存したら、真っ白な画面にこう出ました。
閲覧できません (Forbidden access)

しかも厄介なことに、全部の記事で出るわけではありません。ある1本だけが、何度やっても403で弾かれる。他の記事は普通に保存できる。
原因はサーバーのWAF(ウェブアプリケーションファイアウォール)でした。ただ、調べて出てくる記事はどれも「WAFをOFFにしましょう」で終わっていて、それはやりたくありませんでした。
この記事は、WAFを切らずに、本文の中のどの文字列が原因なのかを特定した記録です。
先に結論
- ConoHaの管理画面にはWAFのログを見る機能と、そこから1件だけ除外する機能がある。全部OFFにする必要はない
- ログを見ても分からないときは、本文を4分割して投げ直すと、10分ほどで犯人の文字列にたどり着ける
- 私の場合、犯人は
<id>という6文字と、DELETEというリクエストの種類そのものだった

症状:まず「全部」か「特定の記事だけ」かを見分ける
同じ403でも、原因が2種類あります。ここを最初に分けないと、間違った対処をして時間を失います。
サイト全体が403になる場合。トップページも管理画面も開けないなら、WAFではなくパーミッションや .htaccess の問題であることが多いです。この記事の対象外です。
特定の記事を保存・更新したときだけ403になる場合。他の記事は保存できるのに、その1本だけ弾かれる。これがWAFの誤検知です。
見分け方は簡単で、本文を全部消して「あ」だけにして保存してみることです。それで保存できるなら、犯人は本文の中にいます。
まず、公式のログを見る(多くの記事が飛ばしている手順)
検索して出てくる記事の多くは、いきなり「WAFをOFFにしましょう」と書いています。でもその前に見るべき画面があります。
ConoHaのコントロールパネルで、こう進みます。
WING → サイト管理 → サイトセキュリティ → WAF
ここで「表示切替」から「ログ」を選ぶと、遮断された内容の一覧が出ます。
そして重要なのが、ログの各行から「除外」を押せることです。押した攻撃パターンだけがWAFの対象から外れます。除外中のものは同じ画面で確認できて、「除外解除」で戻せます。
つまり、WAFを全部切る必要はありません。誤検知している1件だけを外せばいい。ここを知らずにOFFにしている人が多いはずです。
まずはこれを試してください。これで解決するなら、以下は読まなくて大丈夫です。
それでもダメなとき:本文の「どこ」が原因かはログに出ない
私の場合はここで止まりました。
理由は、私が管理画面を使わずにREST API経由で記事を投げていたからです。ブロックエディタも同じですが、記事の保存は「本文まるごと1回のリクエスト」で送られます。そのため、ログを見ても「このリクエストが遮断された」までは分かっても、本文の何万字のうちどの部分が引っかかったのかは分からないのです。
そこで、犯人を自力で絞り込むことにしました。
手順:本文を4分割して、犯人を10分で特定する
考え方は単純です。WAFは本文の中身を見て判定しているので、犯人は必ず本文の中にいます。なら半分ずつ試せば、必ず見つかります。
1. 実験用のダミー記事を1本作る
本番の記事で試すと、失敗するたびに中身が壊れます。空の下書きを1本作って、そこに上書きしていきます。
2. 本文を4分割して、1つずつ投げる
ブラウザの開発者ツールのコンソール(Macは Cmd + Option + J)から実行しています。WordPressの管理画面を開いた状態で使ってください。
// 合鍵(nonce)を取る
const nonce = (await fetch('/wp-admin/admin-ajax.php?action=rest-nonce',
{ credentials: 'include' }).then(r => r.text())).trim();
// ダミー記事のIDを入れる
const TEST_ID = 000;
async function tryBody(text) {
const r = await fetch(`/wp-json/wp/v2/posts/${TEST_ID}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
body: JSON.stringify({ content: text })
});
return r.status; // 200なら通過、403なら犯人がこの中にいる
}
const body = '(ここに問題の本文を丸ごと入れる)';
const n = body.length;
console.log('1/4:', await tryBody(body.slice(0, n / 4)));
console.log('2/4:', await tryBody(body.slice(n / 4, n / 2)));
console.log('3/4:', await tryBody(body.slice(n / 2, n * 3 / 4)));
console.log('4/4:', await tryBody(body.slice(n * 3 / 4)));
403が出たブロックに犯人がいます。私の場合は1つ目のブロックでした。

3. 疑わしい文字列を1個ずつ試す
あとは、そのブロックの中で「いかにも引っかかりそうなもの」を短い本文にして1個ずつ投げます。403が返ったものが犯人です。ここまで10分ほどでした。
実際に見つかった犯人は、これでした

犯人1:<id> という6文字
APIの説明を書くときに、可変部分を <id> と書いていました。この部分です。HTMLとして解釈されないようにエスケープして書いていたのですが、WAFはこれをタグの注入だと判定していました。
面白いのは、同じ書き方でも <head> は通ることです。/wp-admin/ も admin-ajax.php も wp-json/wp/v2/posts も、全部そのまま通りました。WordPress関連の単語が悪いわけでも、エスケープ全般が悪いわけでもありません。ピンポイントでこの並びだけが引っかかっていました。
対処はシンプルで、{id} と書けば通ります。APIドキュメントでは中括弧のほうが一般的な書き方なので、こちらに揃えるのが結果的に正解でした。
ちなみに、この記事を書くときも同じ壁にぶつかりました。犯人の文字列を記事の中で説明しようとすると、当然その文字列が本文に入るので、また403になります。しかも数値文字参照(< のような書き方)に置き換えても駄目でした。WAFは数値文字参照を元に戻してから判定しているようです。最終的に、文字列の途中に空のHTMLタグを挟んで、WAFから見て連続した並びにならないようにして回避しています。
犯人2:DELETEというリクエストの種類そのもの
もうひとつ、記事をゴミ箱に移そうとしたら403になりました。こちらは本文と無関係で、DELETEという操作の種類そのものが弾かれていました。
これには回避策があります。WordPress側が「POSTで送るけれど、中身はDELETEとして扱ってほしい」という指定を受け付けてくれます。
await fetch(`/wp-json/wp/v2/posts/${id}?_method=DELETE`, {
method: 'POST',
credentials: 'include',
headers: { 'X-WP-Nonce': nonce, 'X-HTTP-Method-Override': 'DELETE' }
});
これで通りました。ちなみに { status: 'trash' } を送る方法は使えません。WordPressが受け付ける状態の一覧に trash が入っていないためです。
WAFを切る前に、考えたいこと
ここまで手間をかけた理由を書いておきます。
WAFは、本来なら通してはいけないリクエストを止めるための仕組みです。誤検知は確かに邪魔ですが、邪魔だからといって全部切るのは、火災報知器が鳴りやまないから電池を抜くのに近いと思っています。
ConoHaはログから1件だけ除外できます。それでも解決しないなら、今回のように犯人を特定して、書き方のほうを変える。この順番がいいはずです。
私の場合、結果として書き方を1か所変えただけで済みました。WAFは切っていません。
使っているサーバーの話
このブログは ConoHa WING で動いています。
正直に言うと、今回のWAFの件は面倒でした。ただ、調べていて分かったのは、ログを見て1件ずつ除外できるサーバーは、意外と親切な部類だということです。切るか切らないかの2択しかない環境もあります。
料金やキャンペーンの条件は変わります。申し込む時点の表示を必ず確認してください。
まとめ
- 特定の記事だけ403になるなら、犯人は本文の中にいる
- まずConoHaのWAFログを見る。全部OFFにする前に、1件だけ除外できる
- ログで分からないときは、本文を4分割して投げ直す。10分で見つかる
- 私の犯人は
<id>の6文字と、DELETEという操作そのものだった
同じところで止まっている人の役に立てば嬉しいです。
アフィリエイト作業で似たような「なぜか動かない」に当たった話は、こちらにも書いています。

コメント