1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WordPressプラグインのセキュリティ実装、全部まとめ【保存版・申請前チェックリスト】

1
Posted at

WordPress.orgにプラグインを申請したら、セキュリティ面で差し戻されたことがあります。しかも、自分では対策したつもりの箇所でした。

何が足りなかったのかを一つずつ潰していくうちに、「プラグインのセキュリティで見るべき箇所」は、だいたい決まっていると気づきました。散らばって覚えていると抜けるので、1枚のチェックリストにまとめておきます。申請前や、レビュー前に、上から順に見る用です。個別の詳しい実装は、それぞれ別記事へのリンクを置いています。

見るべきは、6つの入り口

プラグインで攻撃が入る(あるいは事故が起きる)のは、だいたい次の6つの入り口でした。

入力の受け取り。DBへのアクセス。出力の表示。権限と認可。預かった秘密の保管。そして、AIを使うなら、その出力の扱い。順に、確認する点を書きます。

1. 入力:受け取った値を、信用しない

フォーム、URLパラメータ、AJAX、REST。外から来る値は、全部サニタイズしてから使います。

// 受け取りは wp_unslash + sanitize、用途に合った関数で
$name  = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : '';
$email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';

そして、状態を変える操作(保存・削除・送信)には、nonce を必ず付けます。CSRF対策です。

// 送信側でnonceを埋め、受け取り側で検証する
check_admin_referer('myplugin_save'); // 失敗すれば処理を止める

2. DB:クエリは prepare を通す

値を文字列連結でSQLに埋めない。$wpdb->prepare に通します。

// ✅ 値はプレースホルダで
$wpdb->get_results($wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_author = %d", $author_id
));

実務でつまずくのは、IN句(配列をプレースホルダの数だけ作る)、LIKE(esc_like を通す)、テーブル名(プレースホルダにできない)あたりです。ここは別記事にまとめました。

3. 出力:表示する直前に、エスケープする

保存時ではなく、表示する瞬間にエスケープします。埋める場所によって関数を変えます。

echo esc_html($text);              // テキスト
echo '<a href="' . esc_url($url) . '">';   // URL
echo '<input value="' . esc_attr($val) . '">'; // 属性値

保存時にきれいに見えても、再表示のときにエスケープが抜けると、そこで事故ります。「保存はそのまま、表示でエスケープ」が原則です。

4. 権限と認可:この操作を、この人がしていいか

ログインしているか、ではなく、その操作をする権限があるか、を見ます。

// AJAX/RESTの機密操作は、権限チェックを必ず入れる
if (!current_user_can('manage_options')) {
    wp_send_json_error('forbidden', 403);
}

とくにWordPress 7.0では、REST APIの permission_callback が省略できなくなりました。公開APIでも __return_true と明示が要ります。ここは差し戻しの定番です。

JSでボタンを隠すのは、認可ではありません。エンドポイントは直接叩けるので、認可は必ずサーバー側に置きます。

5. 秘密:預かった鍵を、平文で置かない

APIキーやトークンを預かるなら、update_option に平文で入れない。DBだけが漏れる経路(他プラグインのSQLi、バックアップの置き忘れ)は現実にあります。

wp-configの salt から鍵を導出して、AES-256-GCMで暗号化して保存する。鍵をコードに直書きしてもDBに置いても意味がないので、ファイル側の salt から導くのが落としどころでした。

6. AIを使うなら:出力も外部入力として扱う

AIチャットボットなどを積むなら、追加で3つ見ます。

LLMの返答を、そのまま innerHTML に入れない。返答は外部入力と同じで、<script> が紛れうる。表示前にエスケープします。

BYOKで請求が青天井にならないよう、1日あたりの利用上限をサーバー側で持つ。呼ぶ前に判定して、超えていたらAIを呼ばずに返す。

根拠がないのに、それらしい嘘を答えさせない。サイト内を検索して、十分な根拠がなければ答えさせない。

申請前に、grep で洗う

最後に、抜けがないか機械的に洗います。

# 受け取り・クエリ・出力・権限・危険な出力先を一気に確認
grep -rn "\$_POST\|\$_GET\|\$_REQUEST\|wpdb->query\|wpdb->get_\|echo\|innerHTML\|current_user_can\|register_rest_route" ./

ヒットした箇所を、上の1〜6と照らし合わせる。サニタイズは通っているか。prepareを経由しているか。エスケープしているか。権限を見ているか。この照合を申請前にやると、差し戻しがだいぶ減りました。

早見でまとめ

  • 入力:wp_unslash + sanitize_*、状態変更には nonce
  • DB:$wpdb->prepare。IN句・LIKE・テーブル名の扱いに注意
  • 出力:表示の直前に esc_html / esc_url / esc_attr。保存はそのまま
  • 認可:current_user_can はサーバー側で。7.0は permission_callback 必須
  • 秘密:APIキーは平文で置かず、saltから鍵を導いて暗号化
  • AI:出力も外部入力。innerHTML直挿し禁止、利用上限、根拠のない回答をさせない
  • 申請前に grep で該当箇所を洗い、1〜6と照合する

セキュリティは、一つひとつは知っていても、抜けるのは「全部を一度に確認していない」ときでした。この1枚を申請前に開いて、上から照合するようにしてから、差し戻しが減りました。役に立ったらストックして、リリース前のチェックに使ってください。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?