2026年10月26日
2026年10月26日
WordPressのパスワードポリシーを強化する方法
はじめに
「password123」や「wordpress」のような簡単なパスワードは数秒で解読されます。WordPressには標準でパスワード強度チェックがありますが、強力なパスワードを「強制」する機能はありません。独自のパスワードポリシーを実装することで、組織全体のセキュリティレベルを底上げできます。
症状・原因
- ユーザーが簡単なパスワードを設定して不正ログインされた
- パスワードを何年も変更していないユーザーがいる
- 過去に使ったパスワードを再設定している
- 漏洩したパスワードデータベースに載っているパスワードが使われている
解決手順
ステップ1:現在のパスワード状態を確認する
# パスワードが変更されていないユーザーを確認
wp user list --fields=ID,user_login,user_email,user_registered \
--format=table
# 管理者アカウントのパスワード最終変更日を確認
wp eval "
\$users = get_users(['role' => 'administrator']);
foreach (\$users as \$user) {
\$last = get_user_meta(\$user->ID, 'password_last_changed', true);
\$days = \$last ? round((time() - \$last) / DAY_IN_SECONDS) : '未記録';
echo \$user->user_login . ': ' . \$days . ' 日前' . PHP_EOL;
}
"
# よく使われるパスワードのリスト(確認用)
# https://haveibeenpwned.com/Passwords でチェック可能
ステップ2:パスワード強度要件を実装する
// functions.php: パスワードポリシーの強制
/**
* パスワードが強度要件を満たすかチェック
*/
function validate_password_strength(string $password): array {
$errors = [];
if (strlen($password) < 12) {
$errors[] = 'パスワードは12文字以上にしてください。';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = '大文字を1文字以上含めてください。';
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = '小文字を1文字以上含めてください。';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = '数字を1文字以上含めてください。';
}
if (!preg_match('/[!@#$%^&*()\-_=+\[\]{}|;:,.<>?\/]/', $password)) {
$errors[] = '記号(!@#$ など)を1文字以上含めてください。';
}
return $errors;
}
// ユーザープロフィール更新時にパスワードポリシーを検証
add_action('user_profile_update_errors', function(\WP_Error $errors, bool $update, \WP_User $user): void {
if (empty($_POST['pass1'])) return;
$password = $_POST['pass1'];
$strength_errors = validate_password_strength($password);
foreach ($strength_errors as $error) {
$errors->add('weak_password', $error);
}
}, 10, 3);
// 新規ユーザー登録時にも適用
add_filter('registration_errors', function(\WP_Error $errors, string $sanitized_user_login, string $user_email): \WP_Error {
if (empty($_POST['user_pass'])) return $errors;
$strength_errors = validate_password_strength($_POST['user_pass']);
foreach ($strength_errors as $error) {
$errors->add('weak_password', $error);
}
return $errors;
}, 10, 3);
ステップ3:パスワード有効期限を設定する
// functions.php: パスワード有効期限の管理
// パスワード変更時に最終変更日を記録
add_action('password_reset', function(\WP_User $user): void {
update_user_meta($user->ID, 'password_last_changed', time());
}, 10, 1);
add_action('profile_update', function(int $user_id, \WP_User $old_user_data): void {
if (empty($_POST['pass1'])) return;
update_user_meta($user_id, 'password_last_changed', time());
}, 10, 2);
// ログイン時に有効期限をチェック
add_action('wp_login', function(string $user_login, \WP_User $user): void {
// 管理者・編集者のみ有効期限を適用
$target_roles = ['administrator', 'editor'];
if (empty(array_intersect((array) $user->roles, $target_roles))) return;
$last_changed = (int) get_user_meta($user->ID, 'password_last_changed', true);
// 記録がない場合は現在時刻を記録して通知
if (!$last_changed) {
update_user_meta($user->ID, 'password_last_changed', time());
return;
}
$days_since_change = (time() - $last_changed) / DAY_IN_SECONDS;
$expiry_days = 90;
if ($days_since_change > $expiry_days) {
// パスワード有効期限切れ → 変更を強制
update_user_meta($user->ID, 'password_expired', true);
wp_redirect(admin_url('profile.php?password_expired=1'));
exit;
}
// 期限切れ7日前に警告
if ($days_since_change > ($expiry_days - 7)) {
$remaining = $expiry_days - (int) $days_since_change;
set_transient('password_expiry_warning_' . $user->ID, $remaining, DAY_IN_SECONDS);
}
}, 10, 2);
// 有効期限切れ警告を管理画面に表示
add_action('admin_notices', function(): void {
$user_id = get_current_user_id();
$remaining = get_transient('password_expiry_warning_' . $user_id);
if ($remaining === false) return;
echo '<div class="notice notice-warning is-dismissible">';
echo "<p><strong>パスワードの有効期限まで{$remaining}日です。</strong>";
echo ' <a href="' . admin_url('profile.php#password') . '">今すぐ変更</a></p>';
echo '</div>';
});
ステップ4:漏洩パスワードチェック(Have I Been Pwned)
// functions.php: Have I Been Pwned API でパスワード漏洩を確認
function is_password_pwned(string $password): bool {
// SHA-1 ハッシュの先頭5文字で k-Anonymity 検索
$sha1 = strtoupper(sha1($password));
$prefix = substr($sha1, 0, 5);
$suffix = substr($sha1, 5);
$response = wp_remote_get(
"https://api.pwnedpasswords.com/range/{$prefix}",
[
'timeout' => 5,
'headers' => ['Add-Padding' => 'true'],
]
);
if (is_wp_error($response)) return false;
$body = wp_remote_retrieve_body($response);
$hashes = explode("\r\n", $body);
foreach ($hashes as $hash_line) {
[$hash_suffix, $count] = array_pad(explode(':', $hash_line), 2, 0);
if (strtoupper(trim($hash_suffix)) === $suffix) {
return (int) $count > 0;
}
}
return false;
}
// パスワード設定時に漏洩チェックを実行
add_action('user_profile_update_errors', function(\WP_Error $errors, bool $update, \WP_User $user): void {
if (empty($_POST['pass1'])) return;
if (!empty($errors->errors)) return; // 既にエラーがある場合はスキップ
if (is_password_pwned($_POST['pass1'])) {
$errors->add(
'pwned_password',
'このパスワードは過去のデータ漏洩で確認されています。別のパスワードを選択してください。'
);
}
}, 20, 3);
ステップ5:パスワードポリシー違反ユーザーを一括通知する
# WP-CLI で全ユーザーのパスワード最終変更日を確認
wp eval "
\$users = get_users();
\$now = time();
foreach (\$users as \$user) {
\$last = get_user_meta(\$user->ID, 'password_last_changed', true);
if (!empty(\$last) && (\$now - \$last) > 90 * DAY_IN_SECONDS) {
echo \$user->user_login . ' (' . \$user->user_email . '): パスワード期限切れ' . PHP_EOL;
}
}
"
注意事項
- Have I Been Pwned APIは外部サービスです。パスワード全体ではなくSHA-1ハッシュの先頭5文字のみを送信する「k-Anonymity」方式なので、パスワード自体は外部に漏れません
- パスワード有効期限を短くしすぎると、ユーザーが末尾の数字を変えるだけの「有効期限回避パスワード」を使うようになります。90日程度が現実的です
- WordPress管理画面のパスワード変更ページ(プロフィールページ)のURLを記載したパスワード変更案内メールを送ると、ユーザーが対応しやすくなります
まとめ
パスワードポリシーの強化は①user_profile_update_errorsフックで12文字以上・大小文字・数字・記号を強制、②password_reset・profile_updateフックでpassword_last_changedメタを記録、③ログイン時に90日以上経過した管理者・編集者を強制変更ページにリダイレクト、④Have I Been Pwned APIのk-Anonymity方式でSHA-1先頭5文字を送信して漏洩パスワードをブロック、⑤WP-CLIで期限切れユーザーを抽出して一括通知します。