2026年6月25日

2026年6月25日

WordPressのファイルアップロード脆弱性を防ぐ方法:サーバー設定とコードの対策

WordPress のファイルアップロード脆弱性を防ぐ要点は、「アップロード先で PHP を実行させない」「許可するファイル形式を必要なものだけに絞る」「独自のアップロード処理では権限・nonce・形式の確認を WordPress の関数に任せる」の 3 つです。この記事では、攻撃がどう成立するかを押さえたうえで、サーバー設定、WordPress の設定、自作コードの書き方、侵入されていないかの確認方法までを順に説明します。

ファイルアップロード脆弱性で何が起きるか

ファイルアップロードの脆弱性は、本来は画像や PDF だけを受け付けるはずの機能から、攻撃者が任意のファイルを置けてしまう状態を指します。最も深刻なのは、PHP ファイル(いわゆる Web シェル)をアップロードされ、ブラウザからそのファイルを開くだけでサーバー上で任意のコードを実行される場合です。こうなると、データベースの読み出し、管理者ユーザーの追加、他のファイルの改ざんまで行えてしまいます。

WordPress 本体のメディアアップロードは、拡張子とファイルの中身をある程度照合する仕組みを持っています。実際の被害の多くは、次のような場所から起きています。

  • プラグインの独自アップロード機能:問い合わせフォームの添付、会員のプロフィール画像、インポート機能などで、拡張子の確認が甘い、ログインしていない人でも呼び出せる、といった不備があるもの
  • 拡張子の偽装:image.php.jpg のような二重拡張子や、大文字の .PHP、.phtml .phar など、チェックをすり抜けやすい拡張子
  • SVG ファイル:SVG は XML なので JavaScript を埋め込めます。そのまま公開すると、開いた人のブラウザでスクリプトが動く XSS の原因になります

つまり、アップロード機能そのものを無くすことはできなくても、「置かれても実行されない」「そもそも置けない」の二重の対策を取ることで、被害の大部分は防げます。

対策1:アップロード先で PHP を実行させない

最初に行うべきなのは、wp-content/uploads 以下で PHP が実行されないようにするサーバー設定です。仮にチェックをすり抜けて PHP ファイルが置かれても、実行されなければ Web シェルとして機能しません。メディアとして PHP ファイルを配信する正当な理由はないので、副作用もほとんどありません。

Apache の場合

wp-content/uploads/.htaccess を作成し、次の内容を書きます(Apache 2.4 の書式です)。

# uploads 以下の PHP 系ファイルへのアクセスを拒否する
<FilesMatch "(?i)\.(php[0-9]?|phtml|phar)$">
    Require all denied
</FilesMatch>

この設定が効くには、サーバー側で .htaccess による上書き(AllowOverride)が許可されている必要があります。レンタルサーバーでは多くの場合有効ですが、VPS で自分で構築した環境では無効になっていることがあります。

Nginx の場合

Nginx は .htaccess を読まないので、サーバー設定ファイルに書きます。

# uploads 以下の PHP 系ファイルは実行させずに拒否する
location ~* ^/wp-content/uploads/.*\.(php[0-9]?|phtml|phar)$ {
    deny all;
}

Nginx の正規表現 location は、設定ファイルに書かれた順に評価され、最初に一致したものが使われます。PHP-FPM に渡す location ~ \.php$ より前に書かないと効きません。設定後は nginx -t で構文を確認してから再読み込みします。

効いているかの確認

テスト用に wp-content/uploads/test.php を置き(中身は <?php echo 'ok'; 程度で十分です)、ブラウザで開きます。403 Forbidden になれば成功、ok と表示されたら設定が効いていません。確認が終わったらテスト用ファイルは削除してください。

対策2:許可するファイル形式を絞る

WordPress は、upload_mimes フィルターで許可する拡張子と MIME タイプの一覧を決めています。標準では Office 文書、音声、動画など多くの形式が許可されています。サイトで使わない形式は外しておくと、プラグインの不備があっても置けるファイルの種類が減ります。

// 画像と PDF だけをアップロード可能にする
add_filter( 'upload_mimes', function ( $mimes ) {
    return array(
        'jpg|jpeg|jpe' => 'image/jpeg',
        'png'          => 'image/png',
        'gif'          => 'image/gif',
        'webp'         => 'image/webp',
        'pdf'          => 'application/pdf',
    );
} );

この設定は管理者を含むすべてのユーザーのメディアアップロードに影響します。動画や Excel ファイルを扱う運用がある場合は、その形式を一覧に加えてください。

また、wp-config.php に ALLOW_UNFILTERED_UPLOADS という定数を true で定義すると、管理者が形式の確認なしにアップロードできるようになります。「アップロードできないファイルがある」という理由で追加されがちですが、管理者アカウントが乗っ取られたときに任意のファイルを置かれる原因になるため、定義しないでください。必要な形式は upload_mimes で個別に追加します。

SVG の扱い

SVG を使いたい場合は、upload_mimes に追加するだけでは不十分です。埋め込まれたスクリプトを取り除くサニタイズ処理が必要なので、Safe SVG のようなサニタイズ機能を持つプラグインを使うか、アップロードできるユーザーを管理者だけに限ります。

対策3:独自のアップロード処理を安全に書く

テーマやプラグインで独自のアップロード機能を作る場合は、move_uploaded_file() で自前で保存するのではなく、WordPress の wp_handle_upload() に処理を任せます。この関数は、ファイル名の無害化、拡張子と中身の照合(wp_check_filetype_and_ext())、保存先の決定を行います。そのうえで、呼び出し前に権限と nonce を確認します。

add_action( 'admin_post_myplugin_upload', 'myplugin_handle_upload' );

function myplugin_handle_upload() {
    // 1. 権限の確認(ログインしていても権限が無ければ拒否)
    if ( ! current_user_can( 'upload_files' ) ) {
        wp_die( esc_html__( '権限がありません。', 'myplugin' ), '', array( 'response' => 403 ) );
    }

    // 2. nonce の確認(フォーム側で wp_nonce_field( 'myplugin_upload', 'myplugin_nonce' ) を出力しておく)
    check_admin_referer( 'myplugin_upload', 'myplugin_nonce' );

    if ( empty( $_FILES['myplugin_file'] ) ) {
        wp_die( esc_html__( 'ファイルが選択されていません。', 'myplugin' ) );
    }

    // 3. サイズの上限(ここでは 5MB)
    if ( $_FILES['myplugin_file']['size'] > 5 * MB_IN_BYTES ) {
        wp_die( esc_html__( 'ファイルが大きすぎます。', 'myplugin' ) );
    }

    require_once ABSPATH . 'wp-admin/includes/file.php';

    // 4. 許可する形式をこの機能用に限定して保存
    $result = wp_handle_upload(
        $_FILES['myplugin_file'],
        array(
            'test_form' => false,
            'mimes'     => array(
                'jpg|jpeg' => 'image/jpeg',
                'png'      => 'image/png',
                'pdf'      => 'application/pdf',
            ),
        )
    );

    if ( isset( $result['error'] ) ) {
        wp_die( esc_html( $result['error'] ) );
    }

    wp_safe_redirect( admin_url( 'admin.php?page=myplugin&uploaded=1' ) );
    exit;
}

書くときにつまずきやすい点は次のとおりです。

  • **$_FILES['type'] を信用しない**:ブラウザが送る MIME タイプは、送信側で自由に書き換えられます。形式の判定は wp_handle_upload() に渡す mimes と、関数内部の中身の照合に任せます。
  • 未ログインでも呼べるフックに注意:admin_post_nopriv_ や wp_ajax_nopriv_ で始まるフックは、ログインしていない人からも呼べます。公開フォームでアップロードを受け付ける場合は、形式をさらに狭め、保存先を公開ディレクトリ以外にすることも検討します。
  • nonce は権限の代わりにならない:nonce は「本人が意図して送ったか」を確かめる仕組みで、「その人に操作の権限があるか」は current_user_can() で別に確認します。

対策4:権限設定と更新

ファイルのパーミッションは、ディレクトリ 755、ファイル 644 を基本にし、uploads を 777 にするのは避けます。777 にすると、同じサーバーの別ユーザーやプロセスからも書き込めてしまいます。

そして、実際の被害で最も多いのは、修正版が出ているのに更新されていないプラグインです。アップロード機能を持つプラグイン(フォーム、会員、インポート系)は特に、更新を後回しにしないようにします。使っていないプラグインは停止ではなく削除しておくと、ファイル自体が残らないので安全です。

侵入されていないかを確認する

すでに不正なファイルが置かれていないかは、SSH で次のように探せます。

# uploads 以下にある PHP 系の拡張子を持つファイルを一覧する
find wp-content/uploads -type f \( -iname '*.php*' -o -iname '*.phtml' -o -iname '*.phar' \)

通常の運用では、uploads 以下に PHP ファイルが置かれることはありません(一部のプラグインが空の index.php を置く場合はあります)。見覚えのないファイルが見つかったら、すぐに削除する前に、更新日時やアクセスログを控えて侵入経路を調べます。あわせて、WP-CLI の wp core verify-checksums と wp plugin verify-checksums --all で、本体と公式ディレクトリのプラグインが改ざんされていないかを確認できます。

まとめ

  • 最優先は、uploads 以下で PHP を実行させないサーバー設定で、置かれても動かない状態にすることです。
  • upload_mimes で許可する形式を必要なものだけにし、ALLOW_UNFILTERED_UPLOADS は定義しません。
  • SVG はサニタイズしてから扱うか、アップロードできる人を限定します。
  • 独自のアップロード処理は、権限・nonce を確認したうえで wp_handle_upload() に任せます。
  • プラグインの更新と、uploads 内の不審なファイルの定期確認を続けます。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲