---
title: WordPress のセキュリティ
sidebar:
  label: セキュリティ
description: テーマのコードで守るセキュリティのルール。出力時のエスケープ、入力のサニタイズ、nonce と権限の確認、SQL のプレースホルダー
---

テーマのコードで脆弱性を作らないためのルールです。
HTTPS や CSP など、WordPress に限らない方針は [Frontend のセキュリティ](/frontend/quality/security)を、管理画面のアカウントや本体の更新は[運用と保守](/wordpress/operations)を参照してください。

基本の考え方は、**入力は受け取った時点でサニタイズし、出力は出力する直前にエスケープする**ことです。
どちらか一方だけでは足りません。
データベースに入った値は、他のプラグインや管理画面の操作で書き換えられる可能性があるため、サニタイズ済みでも出力時にエスケープします。

## 出力時のエスケープ

変数を HTML に出力するときは、出力先に合ったエスケープ関数を通します。
エスケープは出力の直前で行い、変数に代入する時点では行いません。
代入時にエスケープすると、その変数が別の場所（属性値や URL）で使われたときに、正しいエスケープかどうかを確かめられなくなるためです。

| 出力先 | 関数 |
| --- | --- |
| HTML の本文 | `esc_html()` |
| HTML の属性値 | `esc_attr()` |
| `href`・`src` などの URL | `esc_url()` |
| `<textarea>` の中身 | `esc_textarea()` |
| 投稿本文のように、一部の HTML タグを残したい値 | `wp_kses_post()` |
| JavaScript に渡す値 | `wp_json_encode()` |

```php
<a href="<?php echo esc_url($link_url); ?>" class="<?php echo esc_attr($class_name); ?>">
  <?php echo esc_html($label); ?>
</a>
```

翻訳関数の戻り値も同様です。
`esc_html__()` や `esc_attr_e()` のように、エスケープと翻訳をまとめた関数を使います。

:::note[WordPress の関数から取得した値もエスケープする]
`get_the_title()`、`get_post_meta()`、`get_field()` のように値を返す関数の戻り値も、`echo` するときにエスケープします。
管理画面から入力された値でも、エスケープを省略する理由にはなりません。
:::

## 入力のサニタイズ

`$_GET`・`$_POST` などの値は、`wp_unslash()` でスラッシュを取り除いてから、データの種類に合ったサニタイズ関数を通します。

| データの種類 | 関数 |
| --- | --- |
| 1 行のテキスト | `sanitize_text_field()` |
| 複数行のテキスト | `sanitize_textarea_field()` |
| メールアドレス | `sanitize_email()` |
| スラッグ・キー | `sanitize_key()`、`sanitize_title()` |
| 0 以上の整数 | `absint()` |
| URL（保存する場合） | `esc_url_raw()` |

```php
$keyword = isset($_GET["keyword"]) ? sanitize_text_field(wp_unslash($_GET["keyword"])) : "";
```

選択肢が決まっている値は、サニタイズではなく許可リストと照合します。

```php
$order = in_array($_GET["order"] ?? "", ["asc", "desc"], true) ? $_GET["order"] : "desc";
```

## nonce と権限の確認

データを保存・変更する処理（フォームの送信、Ajax、管理画面の設定）では、次の 2 つを両方確認します。

- **nonce**：リクエストが自分のサイトのフォームから送られたことを確かめる。CSRF（クロスサイトリクエストフォージェリ）を防ぐ
- **権限**：操作しているユーザーに、その操作をする権限があることを確かめる

nonce は操作した人の権限を確かめないため、nonce だけでは権限の確認になりません。

```php
// フォームの出力
wp_nonce_field("theme_save_settings", "theme_settings_nonce");
```

```php
// 送信の処理
if (!isset($_POST["theme_settings_nonce"]) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST["theme_settings_nonce"])), "theme_save_settings")) {
  wp_die("不正なリクエストです。");
}

if (!current_user_can("manage_options")) {
  wp_die("この操作を行う権限がありません。");
}
```

管理画面のフォームでは、`check_admin_referer()` で nonce の確認をまとめて行えます。
Ajax では `check_ajax_referer()` を使います。

## データベースの操作

データの取得には、`WP_Query`・`get_posts()`・`get_post_meta()` など WordPress の関数を使います。
これらの関数は SQL を組み立てる際に値を適切に処理します。

`$wpdb` で SQL を直接書く場合は、値を文字列連結で埋め込まず、必ず `$wpdb->prepare()` のプレースホルダーを使います。
連結すると SQL インジェクションの原因になります。

```php
$results = $wpdb->get_results(
  $wpdb->prepare("SELECT ID, post_title FROM {$wpdb->posts} WHERE post_type = %s AND post_author = %d", $post_type, $author_id)
);
```

| プレースホルダー | 値の種類 |
| --- | --- |
| `%s` | 文字列 |
| `%d` | 整数 |
| `%f` | 浮動小数点数 |
| `%i` | テーブル名・カラム名などの識別子 |

## リダイレクト

ユーザーの入力をもとにリダイレクトするときは、`wp_redirect()` ではなく `wp_safe_redirect()` を使い、直後に `exit` します。
`wp_safe_redirect()` は外部のドメインへのリダイレクトを許可しないため、オープンリダイレクトを防げます。

```php
wp_safe_redirect(home_url("/thanks/"));
exit;
```
