2026年9月12日
2026年9月12日
WordPressのオープンリダイレクト脆弱性のテスト方法と修正手順(wp_safe_redirectで直す)
WordPress のオープンリダイレクトは、URL パラメータで受け取った移動先をそのまま wp_redirect() や header('Location: ...') に渡しているコードで起こります。修正の基本は、移動先を wp_safe_redirect() か wp_validate_redirect() で検証し、外部へ出す必要があるドメインだけを allowed_redirect_hosts フィルターで許可することです。この記事では、自分のサイトで脆弱な箇所を見つけてテストする方法、典型的な修正コード、修正後の確認までを順に説明します。
テストは自分が管理しているサイト、できればステージング環境だけで行ってください。他人のサイトに対して同じリクエストを送ることは、利用規約や法律に触れる可能性があります。
オープンリダイレクトとは何が問題なのか
オープンリダイレクトは、https://example.com/?redirect_to=https://evil.example/ のように、信頼されているドメインの URL から第三者のサイトへ自動で飛ばせてしまう状態を指します。サーバーに侵入されるわけではありませんが、次のような悪用につながります。
- フィッシングの踏み台: メールや SNS に載ったリンクのドメイン部分は本物なので、受け取った人が警戒しにくくなります。ログイン画面そっくりの偽サイトへ誘導されると、パスワードを入力してしまう人が出ます。
- OAuth や SSO のトークン漏えい: ソーシャルログインなどで、認証後の戻り先 URL の検証が甘いと、認可コードやトークンが外部に渡る経路になり得ます。
- URL フィルターの回避: 「自社ドメインへのリンクは許可」という社内フィルターやスパム判定をすり抜ける目的で使われることがあります。
WordPress 本体のログイン画面(wp-login.php)の redirect_to は、内部で wp_safe_redirect() を使っているため、通常は外部ドメインへ飛びません。問題になりやすいのは、テーマやプラグイン、独自に書いた functions.php のコードです。
脆弱な箇所を探す
まずはコードを検索して、リダイレクトを行っている場所を洗い出します。対象は wp-content 配下の自作テーマ・子テーマ・mu-plugins・自作プラグインです。
# wp-content 配下でリダイレクト処理を探す(サーバーにSSHで入れる場合)
grep -rnE "wp_redirect\(|header\(\s*['\"]Location" wp-content/themes wp-content/plugins wp-content/mu-plugins
ヒットした行のうち、次の条件に当てはまるものが要注意です。
- 移動先に
$_GET、$_POST、$_REQUEST、$_SERVER['HTTP_REFERER']などの外部入力がそのまま入っている wp_redirect()を使っている(この関数は移動先のホストを検証しません)wp_get_raw_referer()の戻り値を使っている(wp_get_referer()と違い、検証されていない値です)
典型的な脆弱なコードは次のような形です。
// 悪い例: 受け取ったURLをそのまま使っている
add_action( 'template_redirect', function () {
if ( isset( $_GET['go'] ) ) {
wp_redirect( $_GET['go'] );
exit;
}
} );
JavaScript 側の location.href = 受け取った値 も同じ問題を起こします。PHP だけでなく、テーマの JS ファイルも location や window.open で検索しておくと見落としが減ります。
自分のサイトでテストする方法
見つけた箇所に、外部ドメインを渡してリダイレクト先を確認します。ブラウザで試すと実際に移動してしまうので、curl でレスポンスヘッダーの Location だけを見る方法が安全で確認もしやすいです。テスト用の外部ドメインには、予約済みで実在サイトにならない example.org や .example を使います。
# Location ヘッダーとステータスコードだけを表示(-L を付けないので追従しない)
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" \
"https://staging.example.com/?go=https://attacker.example/"
結果が 302 https://attacker.example/ なら外部へ飛ばせる状態、自サイトの URL や管理画面の URL になっていれば検証が効いています。
ホストの判定をすり抜ける書き方もいくつか試します。自作の判定コード(文字列の先頭一致など)を使っている場合に、ここで穴が見つかることがよくあります。
//attacker.example/ プロトコル相対URL
/\attacker.example/ バックスラッシュを混ぜた形
https://example.com@attacker.example/ @ の前はユーザー名として扱われる
https://example.com.attacker.example/ 先頭一致だけの判定をすり抜ける
https:attacker.example スキームの後のスラッシュ省略
%2F%2Fattacker.example URLエンコードした形
パラメータ名は redirect_to、redirect、return、next、url、go などが使われがちです。ログイン後・ログアウト後・フォーム送信後・言語切り替えなど、画面遷移を伴う機能を中心に確認してください。
WPScan や Patchstack などの脆弱性データベースには、プラグインのオープンリダイレクトも登録されることがあります。導入しているプラグインに該当がないか定期的に確認し、修正版が出ていれば更新します。自分で見つけたプラグインの脆弱性は、公開の場に書き込む前に開発者や各データベースの報告窓口へ連絡するのが一般的な手順です。
修正方法1: wp_safe_redirect() に置き換える
もっとも手軽な修正は、wp_redirect() を wp_safe_redirect() に置き換えることです。wp_safe_redirect() は内部で wp_validate_redirect() を呼び、移動先のホストが自サイト(home_url() のホスト)か許可リストに含まれる場合だけリダイレクトします。許可されていなければ、既定では管理画面の URL(admin_url())に飛びます。
// 良い例: 検証付きリダイレクトに置き換え、フォールバック先も指定する
add_action( 'template_redirect', function () {
if ( ! isset( $_GET['go'] ) ) {
return;
}
$to = wp_unslash( $_GET['go'] );
$to = wp_validate_redirect( $to, home_url( '/' ) );
wp_safe_redirect( $to );
exit;
} );
ポイントは次の3つです。
- **
exitを付ける**: リダイレクト関数はヘッダーを送るだけなので、その後の処理が続いてしまいます。 - フォールバック先を決める: フロント側の機能なのに不正な値で管理画面へ飛ぶのは不自然なので、
wp_validate_redirect()の第2引数でトップページなどを指定しておくと分かりやすくなります。 - 相対パスも想定する:
/members/のような自サイト内のパスは許可されます。逆に「自サイト内であっても飛ばしたくない場所」がある場合は、別途チェックが必要です。
修正方法2: 外部ドメインは allowed_redirect_hosts で許可する
決済サービスや別ドメインの自社サイトなど、外部へのリダイレクトが仕様上必要な場合は、allowed_redirect_hosts フィルターで許可するホストを追加します。ホスト名を文字列どおりに照合する仕組みなので、サブドメインは個別に書きます。
// mu-plugins または子テーマの functions.php に追加
add_filter( 'allowed_redirect_hosts', function ( $hosts ) {
$hosts[] = 'shop.example.com';
$hosts[] = 'docs.example.com';
return $hosts;
} );
ここに書いたホストは wp_safe_redirect() を使うすべての処理で許可されます。必要最小限にとどめ、ユーザーが自由にコンテンツを置ける外部サービスのドメイン(ファイル共有や短縮 URL など)は入れないでください。そこが新たな踏み台になります。
修正方法3: URL ではなくキーを受け取る
もっとも安全なのは、そもそも URL をパラメータで受け取らない設計に変えることです。移動先の候補が決まっているなら、キーと URL の対応表をサーバー側に持ち、受け取ったキーで引きます。
add_action( 'template_redirect', function () {
if ( ! isset( $_GET['go'] ) ) {
return;
}
$map = array(
'docs' => 'https://docs.example.com/',
'contact' => home_url( '/contact/' ),
);
$key = sanitize_key( wp_unslash( $_GET['go'] ) );
if ( ! isset( $map[ $key ] ) ) {
return; // 未知のキーは何もしない
}
wp_safe_redirect( $map[ $key ] );
exit;
} );
この形なら、外部ドメインを許可リストに登録していなくても wp_redirect() で動きますが、置き換え漏れを防ぐ意味で wp_safe_redirect() に統一し、allowed_redirect_hosts にも登録しておくほうが管理しやすくなります。
ログイン後の移動先を変えたい場合は、$_GET を自分で読むのではなく、login_redirect フィルターで受け取った値を加工するのが WordPress の流儀です。このフィルターの戻り値も最終的には wp_safe_redirect() に渡されます。
修正後の確認
修正を本番に反映する前に、ステージングで次の点を確かめます。作業前にはファイルのバックアップを取り、子テーマか mu-plugin に書いて、親テーマの更新で消えないようにしておきます。
- 先ほどの curl のテストをすべてのパターンでもう一度実行し、
Locationが自サイトかフォールバック先になっていることを確認する - 正規の使い方(自サイト内の移動、許可した外部ドメインへの移動)が引き続き動くことを確認する
- ログイン・ログアウト・パスワードリセット後の遷移が想定どおりかを確認する
- キャッシュプラグインや CDN を使っている場合は、リダイレクトのレスポンスがキャッシュされていないかを確認する
オープンリダイレクトの修正は小さな変更で済むことが多い一方、自作の判定ロジックで直したつもりになっているケースが目立ちます。ホストの判定は自分で書かず、コアの関数に任せるのが安全です。各関数の挙動は WordPress のバージョンで細かく改善されることがあるので、詳細は Developer Resources の関数リファレンス(2026年10月現在)で確認してください。
まとめ
- オープンリダイレクトは、外部入力の URL をそのまま
wp_redirect()やLocationヘッダーに渡すコードで起きる - 探すときは
wp_redirect(・header('Location・wp_get_raw_referer()・JS のlocationを検索する - テストは自分のサイトのステージングで、curl で
Locationヘッダーを見る。//・@・バックスラッシュなどの迂回パターンも試す - 修正は
wp_safe_redirect()(+exit)への置き換えが基本。外部ドメインはallowed_redirect_hostsで最小限だけ許可する - 可能なら URL ではなくキーを受け取る設計にすると、判定ミスの余地をほぼなくせる