記事一覧に戻る
guide2026年8月12日29分で読めます

無料で守るWordPress — Aikido Zenを入れてみた

WordPressに無料のランタイム保護 Aikido Zen を本番導入した記録。deb 1つで入るSQLi/RCE対策、mu-plugin 1枚で効かせるwp-login.phpのレートリミット、PATH_INFOの落とし穴、そして「Attacks 0」の正しい読み方まで、すべて実測ベースで解説する。

WordPressAikidoZenRASPWAFレートリミットランタイム保護無料セキュリティ

無料で守るWordPress — Aikido Zenを入れてみた

Zen — in-app firewall for WordPress

WordPressの問題は尽きない

つい先日も、BdThemes系プラグイン(Element Pack Lite、Prime Slider Lite等の7種)を狙ったサプライチェーン攻撃が報告された。プラグイン本体のコードは無改変のまま、プラグインが読み込むバナー配信APIの応答が汚染され、管理者がwp-adminを開くだけで攻撃が発火。不正な管理者アカウントの作成、偽プラグインとWebシェルの設置まで進む。

WordPressはコア・テーマ・プラグインという構造上、自分が書いていないコードを大量に動かすことになる。プラグインの脆弱性(SQLインジェクション、パストラバーサル、任意ファイルアップロード等)は毎週のように公表されており、さらにwp-login.phpへのパスワード総当たり、xmlrpc.phpの悪用、/wp-json/経由のユーザー列挙と、WordPressであるというだけで受ける攻撃は多い。「全部を追いかけて塞ぐ」のは現実的でない。

そこで、無料でできる防御の一枚として、Aikido Zenを紹介したい。筆者は自身のWordPress(本サイト含む)で実運用している。

Zenとは — WordPressに何をしてくれるか

Zen(Aikido Security)は、アプリケーションの内側で動くランタイム保護(in-appファイアウォール)だ。PHP版はPHP拡張としてサーバーに入るため、WordPress本体・テーマ・プラグインのコードには一切手を入れない。オープンソースで、導入自体は無料。Aikidoの無料プランに接続すれば、ダッシュボードでの攻撃検知の可視化まで使える(無料プランは月25万リクエストまで保護対象)。

WordPressの文脈で効くのは:

  • プラグイン脆弱性を突く攻撃の検知・ブロック — SQLインジェクション、コマンドインジェクション、パストラバーサル、SSRF。シグネチャのパターンマッチではなく、「入力が実際にクエリやコマンドとしてどう使われたか」をPHP実行の内側で見て判断する。プラグイン名やCVEごとのルール更新は不要で、未知の脆弱性にも効く
  • ボット・脅威IPの制御 — 「誰が来ているか」の監視は無料。ブロックは対象で分かれ、既知脅威アクターのうちBruteforce Attackers(約10万IP)とPublic Internet Scannersの2リストは無料プランでもブロック可。ボットカテゴリ(AIスクレイパー等)・Tor・追加リスト(HTTP DoS/Exploit、Proxy/VPN、WordPress Attackers等)のブロックは有料プラン。なおFirewallタブのブロックはdetect/blockingモードと独立で、有効化すれば常時強制される
  • レートリミット — wp-login.phpの総当たり対策など。ルート単位で設定でき、IPだけでなくログインユーザー単位でも制限できる(後述、WordPressでは小さなmu-pluginが1つ必要)

正直な整理:冒頭のBdThemes攻撃をZenは止められたか

宣伝記事だからこそ、ここは正直に書いておく。

BdThemes攻撃の初動は、管理者ブラウザ内で実行されるXSS(汚染されたAPI応答由来)だ。これはクライアント側で起きるため、サーバー側ランタイム保護であるZenの守備範囲ではない。不正管理者の作成も「認証済み管理者のセッション」による正規のAPI呼び出しとして行われるため、これ単体をZenが弾くわけでもない。

Zenが効くのは、その前後だ。WordPressを日常的に叩いている攻撃の大半——プラグイン脆弱性を突くSQLi、パストラバーサル経由のファイル窃取、設置されたWebシェルからのコマンド実行——はZenの検知・ブロック対象になる。境界(CDN/WAF)、露出削減(ポート・アクセス制御)、そしてアプリ内側(Zen)。どれか一枚で全部を止める銀の弾丸はなく、Zenは「アプリの内側」という、無料では他に埋めにくい層を埋めるものだと理解してほしい。

導入手順(WordPress / Docker環境)

筆者の環境は公式wordpressイメージベースのDocker構成。手順は4ステップ。

1. AikidoでZenアプリを作成しトークン取得

Aikidoにログイン → Zen → Add app → アプリ名を付けて Generate token。AIK_RUNTIME_... 形式のトークンをコピーする。Aikidoアカウントは無料で作成できる。

Zenアプリ作成画面

Zenトークン発行とスタック選択(トークンはマスク済み)

2. PHP版Zenのインストール(Dockerfile)

PHP版はdebパッケージ1つで入る。Composerも、WordPress側のコード変更も不要。PHP 7.2〜8.5対応なので、公式wordpressイメージならどのPHPタグでも入る。

既存構成が「imageを直接指定」の場合、Zenを焼き込むために image → build に切り替える。ディレクトリ構成はこうなる:

~/homepages/
├── docker-compose.yml
├── .env
└── zen/
    └── Dockerfile

zen/Dockerfile の全文:

# 今使っているwordpressイメージのタグに合わせる
FROM wordpress:php8.1-apache

# Zen(PHP版)をdebで導入
RUN curl -L -O https://github.com/AikidoSec/firewall-php/releases/latest/download/aikido-php-firewall.x86_64.deb \
    && dpkg -i -E ./aikido-php-firewall.x86_64.deb \
    && rm ./aikido-php-firewall.x86_64.deb

(ARM/Graviton環境なら aikido-php-firewall.aarch64.deb に読み替え)

ベースイメージのタグは、現行composeの image: wordpress:xxx と同じものを指定する。ここを変えるとPHPバージョンごと変わってしまうので注意。

3. トークンの設定(docker-compose.yml)

既存のwordpressサービスを次のように変更する。image: 行を build: に置き換え、環境変数を2つ足すだけで、他(volumes、DB接続等)は現行のまま:

services:
  wordpress:
    # image: wordpress:php8.1-apache   ← これを削除して
    build: ./zen                       # ← buildに置き換え
    environment:
      # (既存のWORDPRESS_DB_*等はそのまま残す)
      AIKIDO_TOKEN: ${AIKIDO_TOKEN}
      AIKIDO_BLOCK: "false"   # まずはdetect(検知のみ)
    # volumes等、既存の定義はそのまま

トークンはcompose直書きせず.envへ:

# .env(gitに入れない)
AIKIDO_TOKEN=AIK_RUNTIME_xxxxxxxxxxxxxxxx

ビルドと反映:

cd ~/homepages
docker compose build --no-cache wordpress
docker compose up -d --force-recreate wordpress

--no-cache を付けるのは、releases/latest を取る書き方のため。キャッシュが効くと古いdebのまま再ビルドされてしまう。

導入確認:

docker compose exec wordpress dpkg -l | grep aikido
# ii  aikido-php-firewall  1.5.x-1 ...  と出ればOK

4. Detect → Blocking

最初は AIKIDO_BLOCK: "false"(detectモード)で運用し、ダッシュボードで検知内容と誤検知の有無を確認する。公式は誤検知回避のため2週間程度のdryモード運用を推奨している。問題なければ "true" に切り替えてブロッキング有効化。筆者のWordPressはBlockingで運用中で、体感できる遅延はない。

Aikido側での稼働確認は、ダッシュボードのEventsに「Application started」イベントが上がっていること、Routesにサイトのルートが自動発見されてくること、Instancesにインスタンス数が出ていることを見る。

レートリミットの設定(wp-login.php総当たり対策)

ここはWordPressならではの一手間があるので、詳しく書く。

仕組み

SQLi等のコア保護はdebを入れるだけで動くが、レートリミットとユーザーブロックだけは仕組みが違う。アプリ側からZenに「このリクエストをブロックすべきか」を問い合わせ、その結果に応じてアプリ自身が429を返す、という作りになっている。PHP版では \aikido\should_block_request() という関数がその窓口だ。

つまりWordPressの場合、functions.phpかmu-pluginへのフック実装が必要になり、ここだけ一手間かかる。実装先は次の理由でmu-pluginを推奨する:

  • functions.phpはテーマに属するため、テーマの更新や切り替えで実装が消える
  • mu-plugin(wp-content/mu-plugins/)は自動読み込みで、管理画面から無効化できない。攻撃者が管理画面を触れてもレートリミットを外せない

なお制限値そのものはコードに書かないので、実装は一度置けば終わり。以降の運用はすべてダッシュボード側で完結する。

制限値そのものはコードに書かず、Aikidoダッシュボードで管理する。ルート単位(ワイルドカード可)でリクエスト数と時間枠を指定でき、判定はスライディングウィンドウ方式(直近60秒の移動集計。固定窓のように窓の境目で瞬間的に倍のリクエストが通る、という穴がない)。制限はグループ→ユーザー→IPの優先順で1つだけ適用される。

mu-pluginの設置

置き場所はWordPressルート直下の wp-content/mu-plugins/(コンテナ内では通常 /var/www/html/wp-content/mu-plugins/)。Docker環境の場合、ホスト側のどこに置くかはvolumeマウントに依存するので、先に確認する:

docker inspect $(docker compose ps -q wordpress) \
  --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
  • -> /var/www/html のマウントがあれば、そのホスト側パスの wp-content/mu-plugins/ 配下
  • -> /var/www/html/wp-content なら、そのホスト側パスの mu-plugins/ 配下

ディレクトリがなければ mkdir -p で作ってよい。ホスト側のwp-contentがwww-data所有になっている環境では、sudoで作成し、置いたファイルも chown www-data:www-data で所有者を揃えておく。以下を aikido-zen.php として置く。mu-pluginは置くだけで自動読み込みされ(有効化操作なし)、管理画面から無効化できないプラグインなので、この用途に向いている。攻撃者に管理画面を取られてもレートリミットを外されない、という意味でもある。

<?php
/**
 * Plugin Name: Aikido Zen Middleware
 * Description: Zenのレートリミット/ユーザーブロック判定をWordPressに接続する
 */

if (!extension_loaded('aikido')) {
    return; // Zen未導入環境では何もしない
}

add_action('init', function () {
    // ログイン済みならユーザーをZenに通知(ユーザー単位のrate limitに使われる)
    if (is_user_logged_in()) {
        $user = wp_get_current_user();
        \aikido\set_user((string) $user->ID, $user->user_login);
    }

    // ダッシュボード設定に基づくブロック判定
    $decision = \aikido\should_block_request();
    if ($decision->block) {
        if ($decision->type === 'ratelimited') {
            status_header(429);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'リクエストが多すぎます。しばらく待ってから再試行してください。';
        } else {
            status_header(403);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'アクセスが拒否されました。';
        }
        exit;
    }
}, 0); // 優先度0で他の処理より先に判定
?>

設置後、コンテナ内から見えているかを確認する:

docker compose exec wordpress ls -la /var/www/html/wp-content/mu-plugins/

wp-login.phpもWordPress本体を読み込んで init を発火させるため、このフックでログイン画面への総当たりも捕捉できる。

※このコードは公式middleware例のWordPress移植で、筆者の本番サイトで実測済み(後述のテストで設定どおり200×5→429×2を確認)。設置したらサイトを開く前に構文チェックを。mu-pluginは構文エラー1つでサイト全体が白画面になる:

docker compose exec wordpress php -l /var/www/html/wp-content/mu-plugins/aikido-zen.php
# "No syntax errors detected" を確認してからサイトにアクセス

ダッシュボードでの制限値設定

  1. Zenの対象アプリを開き、Routes タブへ
  2. サイトにアクセスすると、ルートは自動発見されて一覧に増えていく。POST /wp-login.php が現れたら、行のアクションメニューから Setup rate limiting
  3. 例:5リクエスト/分 に設定して保存。エージェントの設定取得は約60秒周期なので、反映まで1分待つこと。筆者は保存直後にテストして7連発すべて素通り(200×7)し、一瞬「動いていない」と思ったが、1分後の再テストでは設定どおりに429が返った
  4. まとめて制限したい場合は Add Route からワイルドカード(例:POST /wp-json/*)を追加して同様に設定できる。完全一致のルートが優先され、ワイルドカードは最も低い制限値が適用される

レートリミット設定ダイアログ(画面はGET /wp-login.phpの例。POST側にも同様に設定する)

Routes一覧でwp-login.phpに5 requests per minuteが付いた状態

設定候補の例:

ルート制限例ねらい
POST /wp-login.php5回/分パスワード総当たり対策
POST /wp-login.php/*5回/分PATH_INFO亜種の抜け道封じ(次節参照)
POST /xmlrpc.php5回/分xmlrpc悪用(system.multicall等)対策
POST /wp-json/*30回/分REST API乱用の抑制

動作確認

制限を設定したルートに連続アクセスしてみる。

for i in $(seq 1 7); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST https://example.com/wp-login.php
done

6回目あたりから 429 が返れば成功。ダッシュボード側にもrate limitedイベントが記録される。

落とし穴:同じwp-login.phpが「複数」現れる(PATH_INFO)

しばらく運用すると、Routes一覧に POST /wp-login.php のほかに POST /wp-login.php/xmlrpc.php のような合成パスが現れることがある。これはPATH_INFO形式(存在するPHPファイルの後ろにパスを付け足す記法)で、攻撃ツールが境界型WAFのパスマッチをすり抜ける目的でよく使う。

問題は、Zenのルートは自動発見されたパスごとに別エントリ=別カウントだという点だ。完全一致の POST /wp-login.php にだけ制限を掛けても、攻撃者が /wp-login.php/a、/wp-login.php/b とパスを回転させれば、毎回新しいルート=新しいカウントになり、実質すり抜けられる。

対策は、完全一致の設定に加えて Add Routeからワイルドカード POST /wp-login.php/* を1本追加しておくこと。これでPATH_INFO亜種の全体が1つの制限下にまとまる(完全一致が優先され、ワイルドカードがそれ以外を受ける)。筆者のサイトでも実際に POST /wp-login.php/xmlrpc.php が自動発見されており、机上の話ではない。

PATH_INFO合成パスが別ルートとして自動発見され、個別に制限が付いている

注意点:プロキシ配下のIP判定

CDN(CloudFront/Cloudflare等)やリバースプロキシの背後にいる場合、ZenはX-Forwarded-Forヘッダーからクライアント IPを判定する。逆にプロキシなしで直接公開しているサーバーでは AIKIDO_TRUST_PROXY=false を設定すること。これを怠ると、攻撃者が偽のX-Forwarded-Forを付けてIPを詐称し、IPベースのレートリミットをすり抜けられる。

入れた後の画面の読み方 — 「Attacks 0」は効いていないのか

導入して数日経つと、たぶんこう思う。「Requestsは数千あるのに、Attacksが0。効いてるのか?」筆者も思った。結論から言うと、0は「攻撃されていない」ではない。Zenの画面は攻撃を複数の層で別々に見せているので、読み方を知っておく必要がある。

層1:Routes一覧はスキャンの足跡帳

Routes一覧を眺めると、/nmaplowercheck17824...(nmapのプローブ)、/includesh1.php や /kuj8rl.php(Webシェル探し)、ルート / へのUNLOCK・PURGE・PROPFIND・LOCK(WebDAVプローブ)といった、自分では作った覚えのないルートが並んでいるはずだ。これは実際にサイトへ飛んできた偵察スキャンの痕跡が、ルートとして自動記録されたもの。WordPressは公開した瞬間からこれだけ叩かれている、という事実がそのまま見える。

Routes一覧に残るスキャン痕跡 — nmap、Confluence、GeoServer探査まで

層2:Attack Wave — 探査の「波」の検知とIPブロック

Zenは、1つのIPが短時間に不審なリクエストを連発する(目安:1分に15回)、.envやwp-config.phpのような機微ファイルを取りに来る、といった振る舞いパターンを「Attack Wave」として検知し、BlockingモードならそのIPを遮断する。個々のリクエストが404で終わる無害なGETでも、連打の仕方が怪しければWaveになる。筆者のサイトでは米・欧の複数IPからの波が数日おきに検知・ブロックされている。

筆者のサイトに来た波の実例を1つ。User-AgentはCCBot——Common Crawlの行儀の良いクローラを名乗っているが、中身は .git/config、.aws/credentials、.env(無印・.example・.production・.old・api/・admin/・config/ 配下まで)を順に漁っていく認証情報狩りだった。UAは自己申告でしかなく信用ならない、ということも実データで同時にわかる。

Attack Waveイベントの一覧

CCBotを名乗る認証情報狩りの実例(.git/.aws/.envを順に探査)

なお、名前が紛らわしいが、Attack WaveはAttacksグラフとは別の集計だ。「Detected Attack Wave」のイベントが並んでいてもAttacksが0、というのは矛盾ではない。

層3:Attacksグラフ — ペイロードがシンクに届いた攻撃

統計ページのAttacksグラフが数えるのは、SQLインジェクションやコマンドインジェクションのような悪性ペイロードが、アプリ内部の監視ポイント(SQL実行・シェル実行・ファイルアクセス等)まで実際に届いて検知されたものだ。存在しないファイルへの探査は404で終わり、ペイロードがどこにも届かないので、ここには数字が付かない。

つまり「Requests数千、Attack Wave多数、Attacks 0」は、偵察は大量に来て波としてブロックされているが、ペイロード型はまだシンクに到達していないという、健全な状態の読み方になる。逆にAttacksに数字が付き始めたら、それは実エンドポイントに向けて中身のある攻撃が届いているということだ。

アプリ概要 — 7日間で3.4Kリクエスト、Attacksは0

※補足:本稿執筆時点で、アプリ概要一覧の「Attacks」列の集計についてはAttack Wave件数が混ざるケースが無いか開発チームが確認中。数字に違和感があるときは、統計ページのAttacksグラフを正として読むのがよい。

まとめ

  • WordPressのプラグイン起点の攻撃は構造的に尽きない。全脆弱性を追うより、層を足す
  • Zenは無料で入れられる「アプリ内側」の層。SQLi・コマンドインジェクション・パストラバーサル・SSRFを、コード変更なし・ルール更新なしで検知・ブロックする
  • 導入はdeb+環境変数のみ。detectで様子を見てからblockingへ
  • レートリミットはmu-plugin1枚で有効化でき、制限値はダッシュボードから運用できる。wp-login.php(完全一致+ワイルドカード)・xmlrpc.php・REST APIを設定しておく
  • 「Attacks 0」は「攻撃されていない」ではない。Routes=スキャンの足跡、Attack Wave=探査の波の検知とIPブロック、Attacks=ペイロードが内部に届いた攻撃、の3層で読む
  • 万能ではない。冒頭のBdThemesのようなクライアント側XSSは守備範囲外。境界(CDN/WAF)・露出削減・アプリ内側の多層で考える

導入自体は30分もかからない。「問題が尽きない」ことを嘆く前に、無料で足せる一枚から。

キーワード

  • WordPress
  • Aikido
  • Zen
  • RASP
  • WAF
  • レートリミット
  • ランタイム保護
  • 無料セキュリティ
A

安藤 典久

公開日: 2026/8/12

最終更新: 2026/8/12

シェア:

ニュースを購読

セキュリティの最新情報をお届けします。

Aikido Community Japan からのお知らせの受信に同意します。詳しくはプライバシーポリシーをご確認ください。