はじめに

六回目となった今シリーズですが、今回でシリーズはラストとなりました。

最後を締めくくるのはWordPressのセキュリティ設定についてです。

WordPressは自由度の高いウェブサイト構築を助ける便利なツールですが、その反面、脆弱性だらけで多くのセキュリティリスクを有しているとも言われています。

しかし、それは利用者自身の使い方にも依存しているため、リスクは良くも悪くも変化します(インストール当初からある程度固めておくのが良いとは思います)。

つまり、ある程度の対策を行っておくだけで、その危険性を大きく排除することが可能です。

WordPressの公式ドキュメントにも対策方法が掲載されているので、閲覧するだけでなく一度目を通して可能な設定はやっておくことをおすすめします。

この記事ではWordPressを使う際にやっておくべきセキュリティ対策をいくつか紹介していきます。

また、できるだけWordPressを使用していないように見せかける方法を採用しています。

これまでのシリーズ一覧は以下のとおりです。

なお、このシリーズではホスティングにVultrを使っています。

Vultr

サーバのスペックおよび構成は以下の通りです。

  • $ cat /etc/lsb-release

    • DISTRIB_ID=Ubuntu
    • DISTRIB_RELEASE=20.04
    • DISTRIB_CODENAME=focal
    • DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
  • Vultr : High Frequency Server

    • 32GB MVMe
    • 1CPU
    • 1GB Memory
    • Additional Features
      • Virtual Private Clouds: ON

インストールするルートを変更する

WordPressでは公開ディレクトリ直下にインストールされるケースがありますが、セキュリティ的にはよくありません。

理由として、WordPressの関連ファイルに機械的なアクセスを行いやすいからです。

例えばexample.com直下にWordPressをインストールした場合、example.com/wp-config.phpというように、WordPress関連のディレクトリ(wp-adminwp-contentwp-includes)やファイル(wp-config.phpwp-login.phpなど)にすぐアクセスすることができます。

しかし、example.com/wp/wp-contentのようにディレクトリを一つずらしてインストールしたならば、上記のようなアクセスを弾くことができます。

とはいえ、必要のないファイルにわざわざアクセスする者は単純ではないので、これで大きくセキュリティ対策ができるわけではありませんが、少しでも脅威を排除するためにやっておいて損は無いでしょう。

公開ディレクトリではなくサブディレクトリにWordPressを新規インストールする手順は、前回の記事で公開しています。

上記記事ではドメイン名/apps/にWordPressをインストールしつつ、表示上はドメイン名のみで行えるようにしています。

この設定では`/apps/ディレクトリは存在しているため、直接アクセスするとステータスコードは200を返します。

そのため、/apps/ディレクトリへアクセスして欲しくない場合、これまでのシリーズと同様にNginxで設定を行って制御を行います。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定ファイルを編集します。

## 省略 ##

# /apps(/)にアクセス時、"/?access=apps"へ内部リダイレクトする
location ~ /apps/?$ {
    rewrite ^/  /?access=apps? last;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

これにより、/apps/ディレクトリにアクセスされても他の404ページと同様の動きが行われます。

ブログ上の表示名を変更する

WordPressでウェブサイトを新規に作成する際、ユーザ名とパスワードの入力を行ったのを覚えているでしょうか。

この時に登録したユーザ名は、セキュリティ上の理由から別のニックネームへと変更しなければなりません。

ユーザ名は管理ページへのログインの際に使用される非常に重要な情報であり、ユーザ名が特定されてしまえば後はパスワードだけでログインすることが可能です。

対策していないサイトでのユーザ名の特定は非常に簡単で、ウェブサイトURLの末尾に?author=1というようなクエリ文字列を付けるだけで簡単に判明します。

そのクエリ文字列を付けて移動すると、URLが自動的に記事の投稿者ページに移動しますが、ユーザ名を変更していない場合はそれがログイン用のユーザ名となっています。

これを変更するにはログイン後、メニュー一覧のユーザーからプロフィールを選択し、対応するユーザを選択して新しいニックネームを付けることです。

ニックネームの追加後、ブログ上の表示名に追加した名前がタブに増えているので、それに変更して保存します。

これにより、投稿者名が別の名前に変更されたため、ユーザ名を特定することができなくなりました。

複数のユーザを登録している場合も同様ですので、すべて変更しておくことを強くおすすめします。

ただし、ユーザ名がadminloginroot等の分かりやすい名前にするのはまったくおすすめしません。

なお、他にもfunctions.phpに下記コードを追加することでauthorに関連するURLを無効化することができます。

functions.php

/** 省略 **/

// 投稿者アーカイブを無効化してWordPressのユーザ名を隠す方法
// https://blog.webcontent.jp/entry/no-author-archive

// 投稿者アーカイブを無効化
add_filter( 'author_rewrite_rules', '__return_empty_array' );

// "?author=値"もしくは"/author/文字列"を含んだURLの場合、404ページに内部リダイレクトする
function disable_author_archive() {
	  if( $_GET['author'] || preg_match('#/author/.+#', $_SERVER['REQUEST_URI']) ){

        // "/author/文字列"を含んだURLの場合
        if(preg_match('#/author/.+#', $_SERVER['REQUEST_URI'])){
            // ステータスコードを404に変更する
            status_header( '404' );
        }

    // トップページにリダイレクトさせる
    // wp_safe_redirect( home_url() );

    // テンプレート内の404ページにリダイレクトする
    include( get_query_template( '404' ) );

    // 処理を停止する
    exit();
    }
}
// "init"に対してアクションフックを行う
add_action('init', 'disable_author_archive');

/** 省略 **/

上記方法では投稿者アーカイブを無効にし、?author=値もしくは/author/文字列を含むURLにアクセスがあった場合、テンプレートの404ページ(もしくはトップページ)にリダイレクトさせます(/author/文字列では更にステータスコードを404に変更)。

しかしながらクエリ文字列の場合、通常はページに変化が無いものなので、特定URLにアクセスした時にトップページへリダイレクトしたり404ページに移動するような仕様は、相手からするとWordPressを使っていることが丸わかりです。

そのため、より自然な動作を演出するには、サーバ側で特殊なリダイレクト処理を挟む必要があります。

当シリーズではNginxを使用しているため、$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定ファイルを編集します。

ドメイン名.conf

## 省略 ##

## if文はlocationの外に設置(意図しない動作をするため)##

    # "?author=値"にアクセス時、"/?access=author"へ内部リダイレクトする
    if ($args ~ "author=.*") {
       rewrite ^/  /?access=author? last;
    }

    # "/author/"を含むURLにアクセス時、"/?access=author"へ内部リダイレクトする
    location ~ /author/ {
        rewrite ^/  /?access=author? last;
    }

## 省略 ##

上記コードを用いた場合、?author=1を加えたURLにアクセスすると、URL表示上では変化がありませんが、内部ではホスト名?access=authorに変換しています(example.com/?access=author)。

2つ目のコードも同様に、/author/を含むURLにアクセスすると、URL上は変化が無いまま、内部ではホスト名?access=authorとして処理されています。

URL表示上は変化させずに内部リダイレクトを行うことにより、できるだけ違和感無くアクセスを弾くことができます。

また、上記Nginxのコードを用いれば、先程functions.phpに記述したauthor用のコードは必要ありません(add_filter( 'author_rewrite_rules', '__return_empty_array' );は必要です)。

なお、アクセス時のヘッダを調べたい場合は、curl 調べたいURL -I -vを使うことで各種ヘッダ、curlがアクセスした際のhttpsの処理およびヘッダを見ることができます。

Nginxでは内部リダイレクトした場合、クライアントにはその情報が基本的に知らされないため、curlを用いたチェックでも、表示上のURLと同じ内容を返すようになっています。

wp-config.phpを移動する

WordPressにとって非常に大切な設定ファイルであるwp-config.phpは、セキュリティ上でも最上位レベルの重要さを持っています。

このファイルはデータベースのユーザ名やパスワード等のコアとなる設定が書き込まれているため、これを悪意のある第三者が利用できた場合、あなたのウェブサイトは完全に破壊されてしまいます。

初期設定ではWordPressのインストールディレクトリ直下にwp-config.phpが設置されています。

ありがたいことにwp-config.phpはインストールディレクトリ直下だけでなく、その一つ前のディレクトリに設置しても機能することが可能です。

なお、最初に説明したインストールディレクトリの変更を行っている場合、一つ前に移動しても公開ディレクトリ直下になるだけなので、この方法を使用することはできません。

ただ、例として公開ディレクトリ直下にWordPressをインストールした場合、現在の場所をフルパスで表現すると/var/www/example.com/wp-config.phpですが、これを/var/www/wp-config.phpに移動することができます。

これにより公開ディレクトリにはwp-config.phpが設置されていないため、ブラウザでのアクセスなど、外部からはこのファイルを認識することができなくなります。

やり方も簡単で、FTPで移動できるならそのまま前のディレクトリに移動すれば良いだけですが、パーミッションによる影響などですぐに移動できない場合は$ sudo mv /var/www/example.com/wp-config.php /var/www/wp-config.phpを実行することでファイルを移動できます。

ファイル移動後にウェブサイトを閲覧できる状態ならば、これで作業は完了です。

今シリーズでの設定の様に、公開ディレクトリ直下にWordPressをインストールしていない場合は、ファイルのパーミッションを制御することで対策します。

パーミッションによる制御

WordPressではとても多くのファイルを扱っているため、これらファイルのパーミッション(アクセス権限)を誤ってしまうと何らかの被害が出る可能性があります。

そのため、正しいパーミッション設定を行って不正アクセス対策をしておく必要があります。

パーミッションの基本

基本的な事として、あらゆるファイル(およびディレクトリ)には、所有者(user:group)と権限(user:group:other)が存在しています。

所有者はそのファイルに対しての所有権を持ったユーザであり、usergroupの2つに分けられます。

WordPressでの設定では、userとしてApatchやNginxなどのウェブサーバを実行するユーザ、groupには(S)FTPを使うユーザなど、複数の利用者が使用するための共通グループが割り当てられます。

権限はそのファイル(およびディレクトリ)に対し、読み取り(Read)書き込み(Write)実行(Execute)権限を0から1を使った数字で表し、usergroupotherの順にそれぞれ割り当てます(otherusergroup以外のユーザ)。

数字は権限を加えるごとに足していく仕組みになっており、読み取りでは4、書き込みでは2、実行では1となっています。

例えば755の場合、userには7が割り当てられ、これは読み取り、書き込み、実行の権限が与えられています。

一方、groupotherには5が割り当てられていますが、これは読み書きが可能なものの、実行(Execute)はできません。

ちなみに権限が自由に割り当て可能だとしても、誰もが操作可能となる777を設定することはセキュリティ上全くおすすめしません。

なお、権限はこれら以外にも、親ディレクトリの権限を継承するSet User ID(4000)およびSet Group ID(2000)というシステムや、スティッキービット(1000)と呼ばれる所有者とrootユーザのみが削除を行える特殊な権限があります。

パーミッションの設定

では実際のパーミッションですが、これまでのシリーズでは所有者にnginx:SFTPユーザ名が設定されています。これは使用者の環境によって変更してください。

コマンドで実行するには、$ sudo chown -R nginx:SFTPユーザ名 /var/www/ドメイン名を使用します。ドメイン名の箇所はウェブサーバの公開ディレクトリを指定します。

そして権限にはディレクトリが775、ファイルに664が割り当てられます。

ディレクトリに対する権限設定をコマンドで実行するには$ sudo find /var/www/ドメイン名 -type d -exec chmod 2775 {} \; を使用します。

  • 上記コマンドが使用できない場合は$ sudo find /var/www/ドメイン名 -type d -print | xargs chmod 2775を使用してください。

権限に2000を足すことでSet Group IDが適用され、そのディレクトリ内で生成されるファイルおよびディレクトリの権限は同一グループに継承されます。

ファイルに対する権限設定をコマンドを実行するには$ sudo find /var/www/ドメイン名 -type f -exec chmod 664 {} \;を使用します。

  • こちらも上記コマンドが使用できない場合は$ sudo find /var/www/ドメイン名 -type f -print | xargs chmod 664を使用してください。

ただし、これら権限はSFTPユーザも読み書き可能にするという柔軟さを重視したものであって、SFTPユーザは閲覧のみで編集させる必要が無い場合は、ディレクトリが755、ファイルは644を割り当てます。

もしSFTPユーザに一切権限を与えない(SFTPを使わない)場合には、所有者をnginx:nginxに変更し、権限はディレクトリが705、ファイルには604を割り当てます。

なお、一部レンタルサーバなどの共用サーバでは、他の利用者とグループが同じに設定されているケースもあるため、この場合は上記と同じくディレクトリに705、ファイルに604を割り当てる必要があります。

重要ファイルのパーミッション

前のセクションにてパーミッション設定を行いましたが、これらとは異なるパーミッションを設定した方が良いファイルもあります。

それはwp-config.phpです。

シリーズではNginxを主に扱っていますが、念の為にApatchでは.htaccess(このファイルはApatchのみ)も重要とされるファイルです。

これらはWordPressにとって非常に大切なファイルであり、より厳格にパーミッションを設定した方が望ましいです。

wp-config.phpでは権限に600、より厳格にするならば400に設定します。

外部からのアクセスを完全に排除したいならば、サーバ側でアクセス禁止を設定する方法もあります。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで編集を行います。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-config.php"へのアクセスを拒否する
location ~ wp-config\.php$ {
# すべての外部アクセスを拒否する
    deny all;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

.htaccessで権限を設定する場合は606、パーマリンク設定が不要ならば604に設定します。

そしてファイルそのもののアクセスを排除する場合は以下を記述します。

.htaccess

<Files ".ht*">
Require all denied
</Files>

なお、Apache2.4からはAllowDenyOrderSatisfyディレクティブは非推奨となっています。

ログインURLを変更する

未対策のWordPressでは、wp-login.phpにアクセスすると誰でもログインページに移動することができます。

それだけでなく、下記のコードによるとWordPressを使用しているサイト内でwp-admindashboardadminloginという文字列をURLに加えただけでも、自動的にログインページへリダイレクトされます。

誰でもログインページを見ることができるのは当然良いとは言えず、推測での自動リダイレクトは便利な側面があるものの、セキュリティで考えるとよろしくありません。

悪意のある者の目に触れさせないためにも、ログインページという大変重要な場所は、できる限り隠蔽することが望ましいです。

今回はログイン用のファイルを新しく作成し、functions.phpにそれを読み込ませる設定を行います。

例としてtest-example.phpというログイン用のファイルを作成します。なお、今回設置する場所は/var/www/ドメイン名/apps/となっています。

ファイル名は自由に決めてもらって構いませんが、できるだけ相手から推測されないためにも$ openssl rand -hex 16等の比較的信頼できる暗号強度の高い文字列を使った名前が良いでしょう。

test-example.php

<?php

// 第4章 WordPressの登録・認証系の秘匿化
// https://ka2.org/part4-conceal-of-registration-or-authentication-on-wordpress

// WordPress管理画面のログインURLを変更する
// https://gray-code.com/blog/wordpress/change-the-url-of-admin-page/

// 許可するIPリスト
$allow_ips = [
    // ログイン画面にアクセスを許可するIPアドレスを定義(未定義の場合は全ての接続元を許可する)
    // コメントアウトの行は空欄に該当する
    // cf. '123.123.123.123', ...

];
// ログインページの表示が許可された場合の処理
if ( empty( $allow_ips ) || ( ! empty( $allow_ips ) && in_array( $_SERVER['REMOTE_ADDR'], $allow_ips, true ) ) ) {
    define( 'LOGIN_PAGE_FILE', basename( $_SERVER['SCRIPT_FILENAME'] ) );
    define( 'LOGIN_CREDENTIAL', hash( 'sha512', $_SERVER['HTTP_HOST'] .':'. $_SERVER['DOCUMENT_ROOT'] ) );;

    // wp-login.phpを読み込む
    require_once dirname( __FILE__ ) .'/wp-login.php';

    // このファイルの設置場所を"/apps/"から公開ディレクトリ直下に変更する場合
    // functions.phpでも設定が必要
    // require_once dirname( __FILE__ ) .'/apps/wp-login.php';

} else {
// ログインページの表示が許可されない場合の処理

    // WordPressの情報を取得するためにwp-load.phpを読み込む
    require_once dirname( __FILE__ ) . '/wp-load.php';

    // このファイルの設置場所を"/apps/"から公開ディレクトリ直下に変更する場合
    // functions.phpでも設定が必要
    // require_once dirname( __FILE__ ) . '/apps/wp-load.php';

    // ステータスコードを404に変更する
    status_header( '404' );

    // テンプレート内の404ページにリダイレクトする
    include( get_query_template( '404' ) );

    // 処理を停止する
    exit();
}

ログイン用ファイルの作成が終われば、次にfunctions.php内に追加のコードを加えます。

functions.php

/** 省略 **/

// 第4章 WordPressの登録・認証系の秘匿化
// https://ka2.org/part4-conceal-of-registration-or-authentication-on-wordpress

// WordPress管理画面のログインURLを変更する
// https://gray-code.com/blog/wordpress/change-the-url-of-admin-page/

// WordPressインストールディレクトリの設定
$_wp_install_dir = trim( str_replace( $_SERVER['DOCUMENT_ROOT'], '', ABSPATH ), '/apps/' );

// ログイン用ファイルを"/apps/"ではなく、公開ディレクトリ直下に設置する場合
// ログイン用ファイルにも設定が必要
//$_wp_install_dir = trim( str_replace( $_SERVER['DOCUMENT_ROOT'], '', ABSPATH ), '/' );

// インストールディレクトリの再設定および定義
$_wp_install_dir .= ! empty( $_wp_install_dir ) ? '/' : '';
defined( 'WP_INSTALL_DIR' ) or define( 'WP_INSTALL_DIR', $_wp_install_dir );

// 新しいログインページのファイル名を定義する
defined( 'LOGIN_PAGE_FILE' ) or define( 'LOGIN_PAGE_FILE', 'test-example.php' );

// ログインURLの処理
add_filter( 'login_url', function( $login_url, $redirect, $force_reauth ) {
    $login_url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $login_url );
    return $login_url;
}, 10, 3 );

// ログアウトURLの処理
add_filter( 'logout_url', function( $logout_url, $redirect ) {
    $logout_url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $logout_url );
    return $logout_url;
}, 10, 2 );

// 新規登録URLの処理
add_filter( 'register_url', function( $url ) {
    $url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $url );
    return $url;
}, 10 );

// パスワード再発行URLの処理
add_filter( 'lostpassword_url', function( $url ) {
    $url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $url );
    return $url;
}, 10 );

// ログインページ内におけるサイトURLの置換処理
add_filter( 'site_url', function( $url, $path, $orig_scheme, $blog_id ) {
	if( ($path == 'wp-login.php' || preg_match( '/wp-login\.php\?action=\w+/', $path) ) && (is_user_logged_in() || strpos( $_SERVER['REQUEST_URI'], LOGIN_PAGE_FILE) !== false) ) {
		$url = str_replace( 'wp-login.php', LOGIN_PAGE_FILE, $url);
    }
    return $url;
}, 10, 4 );

// リダイレクト処理
add_filter( 'wp_redirect', function( $location, $status ) {
    if ( strpos( $_SERVER['REQUEST_URI'], LOGIN_PAGE_FILE ) !== false ) {
        $location = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $location );
    }
    return $location;
}, 10, 2 );

// ログインページの処理
add_action( 'login_init', function() {
    // ログインページへのアクセスが拒否された場合
    if ( ! defined( 'LOGIN_CREDENTIAL' ) || hash( 'sha512', $_SERVER['HTTP_HOST'] .':'. $_SERVER['DOCUMENT_ROOT'] ) !== LOGIN_CREDENTIAL ) {

    // ステータスコードを404に変更する
    status_header( '404' );

    // テンプレート内の404ページにリダイレクトする
    include( get_query_template( '404' ) );

    // 処理を停止する
    exit();
    }
});

// "admin"や"login"、"dashboard"などの管理ページ自動リダイレクト機能を止める
remove_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 );

// stop redirection on /wp-admin call to /wp-login
// https://wordpress.stackexchange.com/questions/257913/stop-redirection-on-wp-admin-call-to-wp-login
// "/wp-admin/"にアクセスした際のログインページリダイレクトを無効化
add_filter('auth_redirect_scheme', 'stop_redirect', 9999);
function stop_redirect($scheme) {

    // 認証クッキーの判定処理
    if ( $user_id = wp_validate_auth_cookie( '',  $scheme) ) {
        return $scheme;
    }

    // ステータスコードを404に変更する
    status_header( '404' );

    // テンプレート内の404ページにリダイレクトする
    include( get_query_template( '404' ) );

    // 処理を停止する
    exit();
}

/** 省略 **/

上記コードにより、wp-login.phpが行う処理は新しく作成したログイン用ページが担うことになりました。

また、上記処理ではwp-login.phpへの直接アクセスを無効化および特定文字列によるリダイレクトを阻止し、同時にステータスコードを404に変更してします。

本来はログイン画面を変更してもwp-login.phpは存在しているので、ステータスコードは200で返却されていますが、ステータスコードも偽装することでそのファイルが存在していないように振る舞います。

しかし、ログインページへのアクセスを弾くにはこれでも不十分で、上記コードでは/wp-admin/へアクセスした場合にも404を返却しますが、サーバの設定もしくはWordPressの機能によって/wp-adminにアクセス時、/wp-admin/へとリダイレクトするため、ディレクトリの存在が分かってしまいます(WordPressを使っていることが判明してしまう)。

Nginxでこれを防ぐには、rewriteで内部リダイレクトを行います。

ドメイン名.conf

## 省略 ##

# "/wp-admin(/)"にアクセス時、"/?access=wp-admin"へ内部リダイレクトする
location ~ /wp-admin/?$ {
     rewrite ^/  /?access=wp-admin? last;
}

## 省略 ##

上記設定はauthorでの方法とほぼ同じであり、できるだけ自然な挙動にするためにURLはそのままにしつつ、内部リダイレクトするようにしています。

WordPressテンプレートの404ページが用意されていればそれが表示されるため、他の存在しないファイルやディレクトリにアクセスした場合と同じ挙動の様に演出してくれます。

これにより、特定ファイルにアクセスした時だけ異なる404ページが表示されたり、URL変更を伴ったリダイレクトが行われる、といった違和感を発生させることはありません。

FastCGI Cacheを使用している場合

もしNginxでFastCGI Cacheを有効にしている場合は正しい表示ができなくなるため、ログイン用ファイルをキャッシュしない設定が必要です。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで編集を行います。

## 省略 ##

# 設置する位置によっては"$skip_cache"が上書きされてしまうので注意
# ログイン用ファイルにはFastCGI Cacheを使わない
if ($request_uri ~ "/test-example\.php$") {
    set $skip_cache 1;
}

## 省略 ##

編集後は$ sudo service nginx restartで再起動します。

IP制限ができない環境の場合

ログイン用ファイルにIP制限が利用できない環境にある場合は、作成したログインページにBasic認証を使う方法もあります。

まず$ openssl passwdでBasic認証に使うパスワードを入力します。

パスワード確認後、ランダムな文字列が生成されるので、こちらもメモしておきます。

次に$ sudo vim /etc/nginx/wp_passでBasic認証に使うユーザ名と先程のパスワードの『暗号化された文字列』を入力します。

wp_pass

Basic認証に使うユーザ名(自由):暗号化後のパスフレーズ

保存後は$ sudo vim /etc/nginx/conf.d/ドメイン名.confでログイン用ファイルへアクセス時の設定を作成します。

ドメイン名.conf

## 省略 ##

# ログイン用ファイルにアクセスした時の設定
location ~ /ログイン用ファイル名\.php$ {

    # Nginx側でIP制限を行う場合
    # allow ここに許可するIPアドレスを入れる;
    # allow 複数のIPを許可する場合は一行ずつ追加

    # 許可されたIP以外全て拒否する
    # deny all;

    # Basic認証を設定
    auth_basic      "login";

    # Basic認証のユーザファイルを指定
    auth_basic_user_file    /etc/nginx/wp_pass;

    # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
    try_files       $uri $uri/ /index.php?$args;

    # PHPに渡すパラメータファイルを指定
    include fastcgi_params;

    # FASTCGIサーバーへのキープアライブ接続をON
    fastcgi_keep_conn on;

    # スラッシュで終わるURIの後にFASTCGIが要求するファイル名
    fastcgi_index   index.php;

    # fastcgi_path_info変数の値をキャプチャする正規表現の定義
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;

    # FastCGIサーバーに渡すパラメータを指定
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # コード300以上のレスポンスをerror_pageディレクティブで処理
    fastcgi_intercept_errors on;

    # FastCGIサーバーのアドレスをUnixドメインソケットパスに指定
    fastcgi_pass unix:/run/php/php8.1-fpm.sock;

}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

これにより、ログイン用ファイルにアクセス時、Basic認証が行われるようになりました。

headに自動出力される要素を削除する

WordPressサイトは初期状態のままだと、多くの要素が自動的に<head>内に出力されます。

例えばWordPressのバージョン情報や、絵文字を使用するためのコード、短縮URLなど、様々な要素が挿入されています。

けれども、これらは必ず必要というものではなく、特にバージョン情報の表記はセキュリティリスクにも関わることから、必要でないものは非表示にした方が良いと思われます。

他にもWordPressを使用していることを一目で分かってしまったり、サイトのパフォーマンス低下を招くため、不要なものを消しておくことはサイト構造の強化に繋がります 。

また、これらはすべてfunctions.phpで除外指定を行うことで取り除くことが可能です。

functions.php

/** 省略 **/

// wp_head()で出力される不要なタグを除去
// https://miyachi-web.com/remove-tags-in-wp_head/
// RSSフィードのURLを除去
remove_action('wp_head', 'feed_links', 2);
remove_action('wp_head', 'feed_links_extra', 3);

// 絵文字機能を停止
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
// "dns-prefetch"の"s.w.org"を削除する(絵文字関連)
add_filter( 'emoji_svg_url', '__return_false' );

// 前後ページのURL表示を停止
remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' );

// 埋め込み機能(Embed)の停止
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
remove_action( 'wp_head', 'wp_oembed_add_host_js' );

// REST APIのURL表示を除去
remove_action('wp_head', 'rest_output_link_wp_head');

// REST APIに関するレスポンスヘッダを削除
remove_action('template_redirect', 'rest_output_link_header', 11 );

// shortlinkに関するレスポンスヘッダを削除
remove_action('template_redirect', 'wp_shortlink_header', 11);

// レスポンスヘッダのx-pingbackを削除
add_filter('pings_open', '__return_false' );

// 外部ブログツールからの投稿を受け入れを停止
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');

// バージョン表記を除去
remove_action('wp_head', 'wp_generator');

// 短縮URL機能を停止
remove_action('wp_head', 'wp_shortlink_wp_head');

// ウィジェット「最近のコメント」の表示を停止
function remove_recent_comment_css() {
    global $wp_widget_factory;
    remove_action( 'wp_head', array( $wp_widget_factory->widgets['WP_Widget_Recent_Comments'], 'recent_comments_style'));
}
add_action( 'widgets_init', 'remove_recent_comment_css');

function register_javascript() {
    wp_deregister_script('comment-reply');
  }
add_action('wp_enqueue_scripts', 'register_javascript');

// 静的ファイルのクエリパラメータを削除
function remove_query_string( $src ){
	$parts = explode( '?ver', $src );
	return $parts[0];
}
add_filter( 'script_loader_src', 'remove_query_string', 15, 1 );
add_filter( 'style_loader_src', 'remove_query_string', 15, 1 );

// HTMLコード内に挿入されるglobal-stylesを削除
// 【WordPress 5.9】緊急対応「global-styles-inline-css」を削除する方法
// https://on-ze.com/archives/10109
add_action( 'wp_enqueue_scripts', 'remove_my_global_styles' );
function remove_my_global_styles() {
	wp_dequeue_style( 'global-styles' );
}

/** 省略 **/

上記設定により、wp_head()で出力される要素の多くが停止されるようになります。

不要な機能を停止させる

WordPressには多くの便利な機能がありますが、通常のサイト運営では使うことのないものもあります。

使わない状態であるならば、いっそ機能を停止させておいた方がセキュリティ的には良いでしょう。

REST API

REST APIはウェブサイトの情報を外部から取得するためのシステムです。

WordPressでは/wp-json/をURLに加えることでウェブサイト内の様々な情報を取得することができます(example.com/wp-json/など)。

しかし、REST APIは便利な反面、アクセス制限をしていないと誰でも情報を閲覧することができてしまいます。

例えば、/wp-json/wp/v2/usersにアクセスした場合、そこにはWordPressに登録しているユーザの情報が表示されています。

前のセクションでauthorへのアクセスを制限してユーザを確認させない方法を実施しましたが、ここで登録ユーザの情報を閲覧できるため、対策した意味がなくなってしまいます。

この際、REST APIは使う予定が無いのでいっそ停止してしまおうと考えてしまいがちですが、REST APIは多くのプラグインだけでなく、管理ページでも使われているので、停止は推奨されていません。

しかし、ログインユーザに限りREST APIを有効化する方法や、REST APIの使用を許可制にする方法があり、どちらもfunctions.phpに記載するだけで機能します。

記述の際はどちらか必要な方を使用してください。

functions.php

/** 省略 **/

// WordPressのREST API周りのfilter hook / action hook のまとめと、一部のREST APIを開放する方法
// https://qiita.com/TanakanoAnchan/items/f4fc11f66e9cf2d7490e
// 非ログイン時ではREST APIへのアクセスをすべて404にする
add_action( 'rest_api_init', function () {

    //ログインしていない時
    if ( ! is_user_logged_in() ) {

        // ステータスコードを404に変更する
        status_header( '404' );

        // テンプレート内の404ページにリダイレクトする
        include( get_query_template( '404' ) );

        // 処理を停止する
        exit();

    }

}, -1 );

// REST APIを許可制にする
// WordPress のユーザー・一覧表示対策
// https://www.webdesignleaves.com/pr/wp/wp_user_enumeration.html
function deny_rest_api_except_permitted( $result, $wp_rest_server, $request ){

    //permit oembed, Contact Form 7, Akismet
    $permitted_routes = [ 'oembed', 'contact-form-7', 'akismet'];

    $route = $request->get_route();

    foreach ( $permitted_routes as $r ) {
        if ( strpos( $route, "/$r/" ) === 0 ) return $result;
    }

    //permit Gutenberg(ユーザーが投稿やページの編集が可能な場合)
    if ( current_user_can( 'edit_posts' ) || current_user_can( 'edit_pages' )) {
        return $result;
    }

    return new WP_Error( 'rest_disabled', __( 'The REST API on this site has been disabled.' ), array( 'status' => rest_authorization_required_code() ) );

}
add_filter( 'rest_pre_dispatch', 'deny_rest_api_except_permitted', 10, 3 );

/** 省略 **/

REST APIを許可制にする例では$permitted_routesとしてoembedcontact-form-7akismetが記載されていますが、他にも追加したいプラグインがある場合はそこに新しく対象を記載します。

なお、/wp-json/内にあるnamespaceで、プラグインに使われている名前およびルートを確認できます。

RSS Feed

RSS Feedはサイト内の記事に関する更新情報を取得できる便利な機能です。

しかしその需要は年々大きく減少しており、ウェブブラウザではFirefoxが数年前にRSS Feed Readerを廃止しています。

RSS Feed配信については大きなサイトを除いて、惰性でなんとなく配信しているという人は多いのではないでしょうか。

WordPressでもRSS Feedで更新情報を配信できますが、その情報にはWordPressに関する情報も含まれているため、WordPressを使っていることが分かってしまいます。

RSS Feedのテンプレートそのものをカスタマイズする方法もありますが、購読者がほとんどいないならば停止してしまった方がリソースの節約やセキュリティ的にも良いでしょう。

RSS Feedを停止する場合、まずはfunctions.phpに各種RSSの機能を停止させましょう。

functions.php

/** 省略 **/

// 【WordPress】【セキュリティ対策】不要なRSSフィード配信は無効化しよう
// https://cree.fun/6008
// フィード配信を停止
remove_action('do_feed_rdf', 'do_feed_rdf');
remove_action('do_feed_rss', 'do_feed_rss');
remove_action('do_feed_rss2', 'do_feed_rss2');
remove_action('do_feed_atom', 'do_feed_atom');

// フィードリンクを除去
remove_action('wp_head', 'feed_links', 2);
remove_action('wp_head', 'feed_links_extra', 3);

/** 省略 **/

以上の設定でRSS Feedの配信は停止しますが、ページそのものにはまだアクセスが可能です。

そのため、RSS FeedのURLへ直接アクセスするとそのページにはXMLスタイルシートが存在していないことから、クライアント側でエラーページが表示されてしまいます。

これを解決するにはfunctions.phpを編集し、RSS Feedの関連URLを404ページに移動させなければなりません。

functions.php

/** 省略 **/

// RSS Feedのページを404ページに内部リダイレクトする
function disable_rss_feed() {

    // "/feed(/)"、"/rss(2)(/)"、"/rdf(/)"、"/atom(/)"を含んだページにアクセスした場合
    if( preg_match('#/(?:feed|rss2?|rdf|atom)(?:$|/)#', $_SERVER['REQUEST_URI']) ){

        // ステータスコードを404に変更する
        status_header( '404' );

        // テンプレート内の404ページにリダイレクトする
        include( get_query_template( '404' ) );

        // 処理を停止する
        exit();

    }
}
// "init"に対してアクションフックを行う
add_action('init', 'disable_rss_feed');

/** 省略 **/

以上の設定により、RSS Feed関連のURLにアクセスすると404ページが表示されるようになります。

自動リダイレクト

WordPressにはURLの正規化を行う自動リダイレクト機能が搭載されています(/wp-includes/canonical.phpによって制御)。

例えば各ファイルやディレクトリにアクセスする際、最後にスラッシュ(トレイリングスラッシュ)を付けていないと自動的にスラッシュが付与される機能(/wp-admin/wp-admin/となる)、記事ページのURLに近い文字をタイプするとそのページに移動できる(example.com/abをタイプするとexample.com/about/に移動できる)機能などがあります。

これらは一見便利な機能に見えますが、意図したように動作しないこともあるため、不便な側面もあります。

そのため、functions.phpを用いて一部を無効化しますが、必要となる項目のみを記述してください。

functions.php

/** 省略 **/

// 存在しないページでは自動リダイレクトを無効にする
add_filter('redirect_canonical', 'remove_redirect_guess_404_permalink', 10, 2);
function remove_redirect_guess_404_permalink($redirect_url, $requested_url) {

    // 存在しないページの場合はリダイレクトしない
    if(is_404()) {
        return false;
    }

    // 存在するページではリダイレクトする
    return $redirect_url;
}

// 特定のページだけWordPressの自動リダイレクト機能を無効にする
// https://www.doe.co.jp/hp-tips/wordpress/special-redirect/
function redirect_special_permalink( $redirect_url, $requested_url ) {
    $match_url = array(
        'feed',
    );

    foreach( $match_url as $url ) {
        if ( strpos($requested_url, $url) ) return false;
    }

    return $redirect_url;

}
add_filter( 'redirect_canonical', 'redirect_special_permalink', 10, 2 );

// リダイレクトをすべて無効化
// add_filter( 'redirect_canonical', '__return_false' );

/** 省略 **/

上記コードでは存在しないページのみをリダイレクトしない設定の他に、オプションとして指定したページのみリダイレクトしない設定、そして自動リダイレクトを完全に無効化する設定を追加しています。

自動リダイレクトの完全無効化に関しては、URL末尾のスラッシュ有り無し関係なくリダイレクトされない(どちらでもページの閲覧ができる)ため、注意が必要です。

もし自動リダイレクトを無効にするならば、NginxのRewrite機能を用い、拡張子がついたファイル(もしくはクエリ)を除いたすべてのURLにトレイリングスラッシュ(末尾のスラッシュ)を付与することで、この問題を回避できます。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confを編集し、条件を追加します。

ドメイン名.conf

## 省略 ##

# Serverディレクション内の他設定よりも後ろに設置する
# クエリ文字列がついておらず、拡張子ファイルを除いたすべてのURLにトレイリングスラッシュを付与
if ($args = "") {
    rewrite ^([^.]+[^/])$ $1/ permanent;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

これにより、設定したウェブサイト内ではすべてのURLにトレイリングスラッシュが追加されるようになりました。

また、この設定ではファイルの存在有無に関わらず、末尾にスラッシュが付与されています。

QueryVars

WordPressにはQuery Vars(クエリ変数)という、テーマやプラグインのデザイン・機能を補助するための変数が用意されています。

例えば、?p=数値?cat=値を入力することで、該当する記事ページや一覧に移動できます。

しかし、前述したようにこの機能はテーマやプラグインの補助を行うものであって、使わない時は使うことがほとんどありません、と言いたい所ですが、実は各種パーマリンクの設定でもこれが使われていますので、全て無効化することはできません。

クエリ文字列に関しては一部無効化しても影響がないため、WordPressを使用していることを隠すならば、ある程度は無効化してしまっても良いでしょう。

ただし、パーマリンクを基本にしていたり、何度も言うようにテーマやプラグインでクエリ変数を使用している場合は、無効化した際に意図しない動作が発生するため注意が必要です。

今回の例では、Public Query Varsを一部無効化(検索(?s=)やプレビュー(?preview=)は有効化のまま)し、Private Query Varsはそのままにします。

また、ログイン中のユーザに関してはこの無効化を解除します。

もし設定後に何らかのエラーや不具合が発生した場合は調整してください。

無効化する場合はfunctions.phpに新しく追加のコードを記述します。

functions.php

/** 省略 **/

// Public Query Varsをクエリ文字列に限り無効化
function disable_public_query_vars() {

    // ログインしていない場合
    if ( ! is_user_logged_in() ) {
        if(preg_match('#\?(?:attachment|attachment_id|author_name|cat|calendar|category_name|comments_popup|cpage|day|error|exact|feed|hour|m|minute|monthnum|more|name|order|orderby|p|page_id|page|paged|pagename|pb|post_type|posts|preview|robots|s|search|second|sentence|static|subpost|subpost_id|taxonomy|tag|tag_id|tb|term|w|withcomments|withoutcomments|year)=.*#', $_SERVER['REQUEST_URI']) ){

            // ステータスコードを404に変更する
            status_header( '404' );

           // テンプレート内の404ページにリダイレクトする
           include( get_query_template( '404' ) );

           // 処理を停止する
           exit();
        }
    }
}
// "init"に対してアクションフックを行う
add_action('init', 'disable_public_query_vars');

/** 省略 **/

以上のコードを加えることで、非ログイン中ではPublic Query Varsを使うことができなくなります。

ただし、上記コードでは指定したクエリ文字列にアクセスがあった場合に404ページに移動する設定です。

もし存在しないクエリ文字列と同様の動作を行う(見た目上は変化無し)場合にはウェブサーバ側で設定を書き換える必要があります。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confを編集し、条件を追加します。


# Public Query Vars判定用変数を"A"に設定する
set $wpq_ "A";

# WPにログイン中の場合は"$wpq_"を空欄にする
if ($http_cookie ~ wordpress_logged_in_) {
    set $wpq_ "";
}

# クエリ文字列がPublic Query Varsである場合、"$wpq_"に"B"を加える
if ($args ~ "(?:attachment|attachment_id|author_name|cat|calendar|category_name|comments_popup|cpage|day|error|exact|feed|hour|m|minute|monthnum|more|name|order|orderby|p|page_id|page|paged|pagename|pb|post_type|posts|robots|second|sentence|static|subpost|subpost_id|taxonomy|tag|tag_id|tb|term|w|withcomments|withoutcomments|year)=.*") {
       set $wpq_ "${wpq_}B";
}

# "$wpq_"が"AB"である場合、内部リダイレクトする
if ($wpq_ = "AB" ) {
   rewrite ^/  /?access=query-vars? last;
}

設定後は$ sudo service nginx restartでNginxを再起動します。

これにより、クエリ文字列がPublic Query Var関連だった場合、内部リダイレクトが行われるようになります。

Block Editor

WordPressはバージョン5.0からGutenbergというブロックエディタを用いた新しい記事編集システムを採用しています。

このシステムではCSSやHTMLの知識が無い方でも、ある程度直感的かつ視覚的にコンテンツを作成編集できるのですが、細かい装飾が困難であったり編集手順がやや多かったりで、これまで従来のエディタに慣れ親しんで来た方にはやや不評でした。

WordPressはGutenbergのブロックエディタをメインとして開発しつづけているため、時間が経つにつれて仕方なく移行する方達も増えてきましたが、それでもまだ従来のエディタを使い続けている方もまだまだ多いです。

また、ブロックエディタでは常にHTML内に専用のCSS(wp-includes/css/dist/block-library/style.min.css)が出力され、これはブロックエディタでは必ず必要なファイルであることから、少々邪魔に感じます。

最新版では常にブロックエディタのみが使用可能になっており、従来のエディタに戻す場合はプラグインの導入、もしくはfunctions.phpでの編集が必要です。

プラグインを入れなくてもブロックエディタを簡単に無効化できるならば、そちらの方がセキュリティ的にも良いのでfunctions.phpを編集する方法を採用します。

functions.php

/** 省略 **/

// WordPress:Gutenberg(ブロックエディタ)を無効化する方法
// https://www.nxworld.net/wp-disable-gutenberg.html
// ブロックエディタを無効化
add_filter( 'use_block_editor_for_post', '__return_false' );

// Remove the Gutenberg Block Library CSS from WordPress
// https://smartwp.com/remove-gutenberg-css/
// HTML内に出力されるブロックエディタ用CSSを無効化
function mytheme_enqueue() {
    wp_dequeue_style( 'wp-block-library' );
    wp_dequeue_style( 'wp-block-library-theme' );
    wp_dequeue_style( 'wc-blocks-style' ); 
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue' );

/** 省略 **/

以上の設定により、ブロックエディタの無効化および関連CSSの出力停止が行われます。

コンテンツを徹底的に秘匿する

セキュリティを考えた場合、ウェブサイトがWordPressであることを攻撃者にできるだけ悟られないようにすることが大切です。

WordPressにはwp-adminwp-contentwp-includesという専用のディレクトリだけでなく、wp-文字列というファイルなども存在しています。

これらにアクセスして存在が確認できた場合、そのウェブサイトがWordPressを使用しているということに繋がります。

名前が共通であるということは機械的にアクセスすることもできるため、悪意のある者が様々なファイルに何度もアクセスしてくるケースが多々あります。

そのため、これらの存在をできる限り秘匿する必要があります。

不要なファイル

先程にも話した通り、WordPressのインストールディレクトリには様々なファイルが作られています。

多くは必要なファイルですが、一部は特に削除しても問題ないファイルです。

その中でも、インストールディレクトリ直下にあるlicense.txtreadme.htmlwp-config-sample.phpは第三者のアクセスが不要なファイルです。

license.txtはその名の通りWordPressに関するライセンスを記載したファイルです。

readme.htmlはWordPressの簡単な説明書のようなことが記載されています。

wp-config-sample.phpwp-config.phpを作成する際のサンプルとなるファイルです。

これらは削除することができますが、WordPressのアップデートを行った際に復活する場合があるため、設定ファイルで処理の追加を行います。

シリーズ内ではNginxを用いているため、$ sudo vim /etc/nginx/conf.d/ドメイン名.confで処理を記述します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# 以下のファイルにアクセス時、"/?access=files"へ内部リダイレクトする
location ~ /(?:license\.txt|readme\.html|wp-config-sample\.php)$ {
    rewrite ^/  /?access=files? last;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

これにより、該当ファイルにアクセスするとURLはそのままで内部リダイレクトが行われ、WordPressテンプレートの404ページが存在していれば、それが表示されます。

登録関連ファイル

wp-activate.phpwp-signup.phpはユーザ登録を行う際に必要となるファイルです。

wp-activate.phpはユーザ登録後の認証のために使用されるファイルです。

wp-signup.phpはユーザ登録を行う際に用いられます。

ただし、管理ページでしかユーザを登録しないならば、どちらのファイルも不要です。

使用する予定が無い場合は$ sudo vim /etc/nginx/conf.d/ドメイン名.confで両ファイルのアクセスを排除します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-activate.php"もしくは"wp-signup.php"にアクセス時、"/?access=activate-signup"へ内部リダイレクトする
location ~ /(?:wp-activate|wp-signup)\.php$ {
    rewrite ^/  /?access=activate-signup? last;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

以上の設定により、どちらのファイルでも内部リダイレクトが行われるため、外部からは直接アクセスできなくなります。

wp-blog-header.php

wp-blog-header.phpはWordPressの設定読み込みおよびテンプレートの読み込みを行うファイルです。

しかしながら、多くのWordPressサイトではファイルに直接アクセスした際、エラーが発生して404となっていることが多いです。

そのため、外部からアクセスは不要と考え、こちらも$ sudo vim /etc/nginx/conf.d/ドメイン名.confで外部アクセスを排除します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-blog-header.php"にアクセス時、"/?access=wp-blog-header"へ内部リダイレクトする
location ~ /wp-blog-header\.php$ {
    rewrite ^/  /?access=wp-blog-header? last;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

wp-comments-post.php

wp-comments-post.phpは名前の通り、コメントの投稿に関するファイルです。

コメント欄を使用しない、もしくはサードパーティ製のコメントツールを使用するならばこのファイルは不要です。

これまでと同じく、アクセスを排除するならば$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定を行います。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-comments-post.php"にアクセス時、"/?access=wp-comments-post"へ内部リダイレクトする
location ~ /wp-comments-post\.php$ {
    rewrite ^/  /?access=wp-comments-post? last;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

wp-config.php

wp-config.phpは不要なファイルではありませんが、外部アクセスは弾いた方が良いとされるファイルです。

これまでの設定では、deny allによってすべてのアクセスを排除していますが、特定ファイルにアクセスした場合にのみ特殊な動作をするのは、多少仕組みに理解のある者ならばWordPressを利用していることが分かってしまいます。

そのため、ファイルに直接アクセスが来た場合でも、違和感の無い方法でアクセスを排除した方がセキュリティ的にも良いでしょう。

設定を行う場合、こちらも$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定を行います。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-config.php"にアクセス時、"/?access=wp-config"へ内部リダイレクトする
location ~ /wp-config\.php$ {
    rewrite ^/  /?access=wp-config? last;
}

## 省略 ##

設定後は$ sudo service nginx restartでNginxを再起動します。

この方法を使うことでwp-config.phpへ直接アクセスが来た場合でも、他の404と同じ挙動を示すことができます。

wp-cron.php

wp-cron.phpは予約投稿やプラグインによるバックアップなど、時限的に作動させるファイルですが、これは訪問者がやってくるごとに起動するという厄介な仕様を持っています。

例えば訪問者の少ないウェブサイトでは全く予約投稿してくれなかったり、その逆ではウェブサイトに高い負荷が常に発生したりします。

そのため、WordPressによってwp-cron.phpを動かすことはやめておくべきです。

このファイルを停止させるということは予約機能が失われるので、大変不便だと思う人がいるかもしれません。

ただし、停止させるのはWordPress自身が動かす場合であって、サーバ側で動かすならば全く問題がありません。

というわけでwp-cron.phpをWordPress側は停止させ、サーバ側で作動させる方法を説明します。

まずは$ sudo vim /var/www/ドメイン名/apps/wp-config.phpで設定ファイルを編集します(フルパスの場所はそれぞれ変更してください)。

wp-config.php

/* カスタム値は、この行と「編集が必要なのはここまでです」の行の間に追加してください。 */

/** 省略 **/

// WordPressでのwp-cron.phpを停止する
define('DISABLE_WP_CRON', 'true');

/** 省略 **/

/* 編集が必要なのはここまでです ! WordPress でのパブリッシングをお楽しみください。 */

保存するとWordPress側でのwp-cron.phpは作動しなくなりました。

次にサーバ側の設定を行います。

$ sudo vim /etc/crontabでサーバ側のcronを編集します。

crontab

## 省略 ##
# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
* * * * * nginx /usr/bin/php -q /var/www/ドメイン名/apps/wp-cron.php > /dev/null

## 省略 ##

上記の設定では毎分/var/www/ドメイン名/apps/wp-cron.phpをサーバ側が動かすようになっています(フルパスはそれぞれの環境ごとに変更してください)。

もし毎分動かすのが嫌な場合は、最初の*5に変更することで5分ごとに動かすようになります。

ただし、cronの仕様により、時間のズレ(投稿予約時間が18:10でcronが20分ごとの場合、10分の予約が飛ばされるため作動しない)には気をつけなければなりません。

そしてこれら設定に続き、ファイルの存在を秘匿するために$ sudo vim /etc/nginx/conf.d/ドメイン名.confで外部アクセスを排除します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-cron.php"にアクセス時、"/?access=wp-cron"内部リダイレクトする
location ~ /wp-cron\.php$ {
        rewrite ^/  /?access=wp-cron? last;
}

## 省略 ##

設定が終われば$ sudo service nginx restartで再起動します。

これによりwp-cron.phpはWordPressが動かすのではなくサーバ側でのみ作動し、外部からの直接アクセスもできないようになりました。

使用機会の少ないファイル

wp-links-opml.phpwp-mail.phpwp-trackback.phpは使用する機会があまり無いと思われるファイルです。

wp-links-opml.phpはWordPress管理ページのリンクで設定したブログロールのリンク情報を、OPMLと呼ばれるXMLがベースのフォーマットを出力するためのファイルです。

しかし、WordPress 3.5以降ではリンクマネージャは非表示になっていることから、自ら使おうとしない限りは使用することはありません。

wp-mail.phpはメールを用いてブログ投稿を行うためのファイルです。メールでの投稿を行わないならば使うことはありません。

wp-trackback.phpはトラックバックと呼ばれる外部サイトからのリンク引用を通知するシステムです。トラックバックを無効化している場合は使用することはありません。

これらも使用しないことが確定しているならば、$ sudo vim /etc/nginx/conf.d/ドメイン名.confで編集してアクセスを排除します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-links-opml.php"、"wp-mail.php"、"wp-trackback.php"にアクセス時、"/?access=files"へ内部リダイレクトする
location ~ /(?:wp-links-opml|wp-mail|wp-trackback)\.php$ {
        rewrite ^/  /?access=files? last;
}

## 省略 ##

設定が終われば$ sudo service nginx restartで再起動します。

wp-load.php

wp-load.phpはWordPress関数を使うために読み込まれるファイルです。

このファイルを読み込むことで、WordPress外にある外部ファイルでもWordPressの仕組みを利用することができます。

基本的にサーバ側がphpファイルを実行する際に読み込まれるものであるため、外部からのアクセスは必要ありません。

そのため、このファイルもこれまでと同様に外部アクセスを排除します。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで追加の編集を行います。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-load.php"にアクセス時、"/?access=wp-load"へ内部リダイレクトする
location ~ /wp-load\.php$ {
        rewrite ^/  /?access=wp-load? last;
}

## 省略 ##

設定が終われば$ sudo service nginx restartで再起動します。

wp-login.php

wp-login.phpはWordPressの管理ページに入る際必要となる大切なファイルです。

このファイルはどのWordPressサイトでも共通の名前であるため、ファイルを直接指定した不正アクセスを目的とする接続が非常に多いです。

これまでの設定ではwp-login.phpは無効化して更にステータスコードも404に指定しています(ファイルそのものは本来コード200)。

そのため、アクセスそのものを回避したい場合は、今までと同じように内部リダイレクトでアクセス制限を行います。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで追加の編集を行います。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "/wp-login.php"を含むURLにアクセス時、"/?access=wp-login"へ内部リダイレクトする
location ~ /wp-login\.php$ {
    rewrite ^/  /?access=wp-login? last;
}

## 省略 ##

設定が終われば$ sudo service nginx restartで再起動します。

これにより、wp-login.phpは外部アクセスされないようになりました。

wp-settings.php

wp-settings.phpはWordPressに関する共通変数の設定と修正に使用されるファイルです。

しかし、外部からアクセスした場合にはエラーが発生するため、こちらも外部アクセスが不要と判断し、ファイルへのアクセスを排除します。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで編集を行います。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "wp-settings.php"にアクセス時、"/?access=wp-settings"へ内部リダイレクトする
location ~ /wp-settings\.php$ {
    rewrite ^/  /?access=wp-settings? last;
}

## 省略 ##

設定後は$ sudo service nginx restartで再起動します。

xmlrpc.php

xmlrpc.phpはWordPressと他のシステム間でXML-RPCを使った通信を行えるようにするためのファイルです。

このファイルを使うことで、WordPressモバイルアプリでの通信やトラックバック等をサポートでき、Jetpackをはじめとする複数のプラグインでも様々な機能のために利用されていました。

しかし、現在ではREST APIがその代替として使われることが多くなりました。

今となっては下位互換として設置しているだけなので、過去と比べて使う頻度は大きく減っています。

そのため、自身のサイトがこのファイルを使っていないことが明らかなのであれば、無効化しても問題ありません。

なお、このファイルは悪用されるケースが多いため、使用していない場合は放置せずに無効化させることを推奨します。

無効化する場合、まずはWordPressテーマ内のfunctions.phpを編集します。

functions.php

/** 省略 **/

// xmlrpc.phpを無効化
add_filter( 'xmlrpc_enabled', '__return_false' );

/** 省略 **/

上記設定により、xmlrpc.phpの機能は無効化されました。

しかし、ファイル自体は存在しているため、$ sudo vim /etc/nginx/conf.d/ドメイン名.confで外部アクセスを排除するための設定を追加します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "xmlrpc.php"にアクセス時、"/?access=xml-rpc"へ内部リダイレクトする
location ~ /xmlrpc\.php$ {
        rewrite ^/  /?access=xml-rpc? last;
}

## 省略 ##

設定が終われば$ sudo service nginx restartで再起動します。

wp-admin

wp-adminはその名の通り、管理関連のファイルが設置されているディレクトリです。

そのことから、基本的に外部アクセスは弾いておくべきであり、公開する必要はほとんどありません。

WordPressインストール直後の場合、このディレクトリ以降は大抵アクセスすることはできませんが、ファイルにアクセスするとログインページへと移動してしまいます。

つまり、他のセクションでログインページへのアクセスを無効化する方法を紹介したものの、まだログインページにアクセスする方法は残っているのです。

このセクションではwp-admin関連のファイルおよびディレクトリへのアクセスを弾く方法を説明します。

ただし、完全にアクセスを弾いた場合はログインユーザですらアクセスできなくなることや、ログインページでは必要なファイルが読み込まれなくなってしまうため、ログインユーザもしくはログインページにアクセスした者限定でwp-adminにアクセスできるようにします。

また、/wp-admin/admin-ajax.phpはプラグイン等で第三者にも必要となるケースがあるため、アクセス可能にしなければならない場合もあります。第三者のアクセス必要がなければ、こちらもログインユーザおよびログインページアクセス者にのみアクセスを許可します。

ではまず、ログインURL変更の際にログイン用ページに使用したファイルであるtest-example.phpを編集します。

test-example.php

<?php

// 第4章 WordPressの登録・認証系の秘匿化
// https://ka2.org/part4-conceal-of-registration-or-authentication-on-wordpress

// WordPress管理画面のログインURLを変更する
// https://gray-code.com/blog/wordpress/change-the-url-of-admin-page/

// 許可するIPリスト
$allow_ips = [
    // ログイン画面にアクセスを許可するIPアドレスを定義(未定義の場合は全ての接続元を許可する)
    // コメントアウトの行は空欄に該当する
    // cf. '123.123.123.123', ...

];
// ログインページの表示が許可された場合の処理
if ( empty( $allow_ips ) || ( ! empty( $allow_ips ) && in_array( $_SERVER['REMOTE_ADDR'], $allow_ips, true ) ) ) {
    define( 'LOGIN_PAGE_FILE', basename( $_SERVER['SCRIPT_FILENAME'] ) );
    define( 'LOGIN_CREDENTIAL', hash( 'sha512', $_SERVER['HTTP_HOST'] .':'. $_SERVER['DOCUMENT_ROOT'] ) );;

    // Cookie用変数を定義
    // Cookie名を"wp_check_"に設定
    $name = 'wp_check_';

    // Cookieの値には暗号論的に安全なランダムバイト列(32bytes)を16進表現に変換した値を使用
    $value = bin2hex(random_bytes(32));

    // 期限は60秒に指定(expires)、Cookieのpathは'/'に指定、domainは指定無し(サブドメインを含めない)
    // Cookieは同一サイトのみ付与(strict)、CookieはHTTPSでのみ送信(secure)、JavaScriptによるCookie送信を禁止(httponly)
    $options = ['expires' => time()+60,'path' => '/','domain' => '', 'samesite' => 'strict', 'secure' => true, 'httponly' => true];

    // Cookieを配置する
    setcookie($name, $value, $options);

    // wp-login.phpを読み込む
    require_once dirname( __FILE__ ) .'/wp-login.php';

    // このファイルの設置場所を"/apps/"から公開ディレクトリ直下に変更する場合
    // functions.phpでも設定が必要
    // require_once dirname( __FILE__ ) .'/apps/wp-login.php';

} else {
// ログインページの表示が許可されない場合の処理

    // WordPressの情報を取得するためにwp-load.phpを読み込む
    require_once dirname( __FILE__ ) . '/wp-load.php';

    // このファイルの設置場所を"/apps/"から公開ディレクトリ直下に変更する場合
    // functions.phpでも設定が必要
    // require_once dirname( __FILE__ ) . '/apps/wp-load.php';

    // ステータスコードを404に変更する
    status_header( '404' );

    // テンプレート内の404ページにリダイレクトする
    include( get_query_template( '404' ) );

    // 処理を停止する
    exit();
}

上記ではこれまでの記述にwp_check_という名前のCookieを新しく追加しており(名前は自由)、このCookieはNginxの設定で必要となってきます。

次に$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定ファイルを編集します。

ドメイン名.conf

## 省略 ##

# WordPressのファイル読み込み許可を判定する変数の初期値を"deny"にセットする(名前と値は自由)
set $wp_ "deny";

# WordPressにログイン中は"$wp_"を空欄にする
if ($http_cookie ~ wordpress_logged_in_) {
    set $wp_ "";
}
# ログインページ用Cookieの"wp_check_"が存在していれば"$wp_"を空欄にする
if ($http_cookie ~ wp_check_) {
    set $wp_ "";
}

# "/wp-admin/admin-ajax.php"へのアクセス設定
location = /apps/wp-admin/admin-ajax.php {

    # ログイン者とログインページアクセス者のみアクセス可能にする場合はコメントアウト
    # `wp_`を空欄にすることでアクセス制限を解除(誰でもアクセスできる)
    set $wp_ "";

    # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
    try_files       $uri$wp_ $uri$wp_/ /index.php?$args$wp_ /?access=wp-admin;
        # PHPに渡すパラメータファイルを指定
        include fastcgi_params;

        # FASTCGIサーバーへのキープアライブ接続をON
        fastcgi_keep_conn on;

        # スラッシュで終わるURIの後にFASTCGIが要求するファイル名
        fastcgi_index   index.php;

        # fastcgi_path_info変数の値をキャプチャする正規表現の定義
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;

        # FastCGIサーバーに渡すパラメータを指定
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # コード300以上のレスポンスをerror_pageディレクティブで処理
        fastcgi_intercept_errors on;

        # FastCGIサーバーのアドレスをUnixドメインソケットパスに指定
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;
    }

# "location ~ \.php$"の処理よりも上にする
# "非ログインか"$wp_"が空欄でない時、"/wp-admin"か"/wp-admin/"を含むURLにアクセスすると内部転送する
location ~ "/wp-admin(?:$|/)" {

    # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
    try_files  $uri$wp_ $uri$wp_/ /index.php?$args$wp_ /?access=wp-admin;

    location ~ \.php$ {
        # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
        try_files      $uri$wp_ $uri$wp_/ /index.php?$args$wp_ /?access=wp-admin;

        # PHPに渡すパラメータファイルを指定
        include fastcgi_params;

        # FASTCGIサーバーへのキープアライブ接続をON
        fastcgi_keep_conn on;

        # スラッシュで終わるURIの後にFASTCGIが要求するファイル名
        fastcgi_index   index.php;

        # fastcgi_path_info変数の値をキャプチャする正規表現の定義
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;

        # FastCGIサーバーに渡すパラメータを指定
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # コード300以上のレスポンスをerror_pageディレクティブで処理
        fastcgi_intercept_errors on;

        # FastCGIサーバーのアドレスをUnixドメインソケットパスに指定
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;

    }

}

## 省略 ##

設定が終われば$ sudo service nginx restartで再起動します。

これにより、wp-adminおよび関連ファイルへのアクセスは、ログインユーザもしくはログインページにアクセスした者限定で行えるようになりました。

それ以外の第三者は関連ファイルにアクセスしても、404ページが表示されるようになっているため、wp-adminの存在に気付きにくくなります(/wp-admin/admin-ajax.phpにアクセスできる場合は別)。

wp-includes

wp-includesはWordPressのシステム全般に関連するファイルが格納されているディレクトリです。

設定によっては第三者のアクセスが不要になるため、アクセス制限を行っても問題はありませんが、プラグインやテーマによってはwp-includesを使用している場合があります。

アクセス制限を行う際には先程のwp-adminの時と同じく、ログイン者およびログインページアクセス者のみwp-includesおよび関連ファイルのアクセスを許可します。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定ファイルを編集します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"の処理よりも上にする
# "非ログインで"/wp-includes"か"/wp-includes/"を含むURLにアクセス時、内部転送する
location ~ "/wp-includes(?:$|/)" {

    # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
    try_files  $uri$wp_ $uri$wp_/ /index.php?$args$wp_ /?access=wp-includes;

    location ~ \.php$ {
        # 左から順に参照し、該当が無ければ最後のuri(もしくはcode)に内部転送される
        try_files      $uri$wp_ $uri$wp_/ /index.php?$args$wp_ /?access=wp-includes;

        # PHPに渡すパラメータファイルを指定
        include fastcgi_params;

        # FASTCGIサーバーへのキープアライブ接続をON
        fastcgi_keep_conn on;

        # スラッシュで終わるURIの後にFASTCGIが要求するファイル名
        fastcgi_index   index.php;

        # fastcgi_path_info変数の値をキャプチャする正規表現の定義
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;

        # FastCGIサーバーに渡すパラメータを指定
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # コード300以上のレスポンスをerror_pageディレクティブで処理
        fastcgi_intercept_errors on;

        # FastCGIサーバーのアドレスをUnixドメインソケットパスに指定
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;

    }

}

## 省略 ##

設定が終われば$ sudo service nginx restartで再起動します。

これにより、wp-includesおよびその関連ファイルのアクセスは、ログイン者もしくはログインページアクセス者のみに限定されるようになりました。

wp-content

wp-contentはWordPressでのアップロードファイルやテーマ、プラグイン等が格納されているディレクトリであり、wp-adminwp-includesと同様に悪意あるアクセスは多くなっています。

しかし、このディレクトリは第三者にもリソースの読み込みが必要となっているケースも多いため、アクセス制限は基本的にできません。

けれども前2つとは異なりwp-contentはディレクトリそのものの名前を変更することができることから、WordPressを使用しているという情報をある程度秘匿することができます。

今回はシンボリックリンクを用いてwp-contentの場所を変更しますが、ディレクトリを直接リネームして場所を変更するとwp-contentという名前のディレクトリが消えるため、このセクション後半のNginxを用いたアクセス制限は必要ありません。

このセクションではWordPressのデフォルト構成からなるべく外す設定を行いますが、保守性のためにディレクトリそのものの移動やリネームは行わない方法を使います。

まずはwp-contentのシンボリックリンクを作成します。今回の例ではwp-contentディレクトリをfilesという名前でシンボリックリンクを作成します。

$ ln -s /var/www/ドメイン名/apps/wp-content /var/www/ドメイン名/filesを実行し、シンボリックリンクを作成します。

例では変更後階層のフルパスは/var/www/ドメイン名/filesとなっています。

次に$ sudo vim /var/www/ドメイン名/apps/wp-config.phpを編集してwp-contentの場所を変更させます。

wp-config.php

/** 省略 **/

/* カスタム値は、この行と「編集が必要なのはここまでです」の行の間に追加してください。 */

/** 省略 **/

// "wp-content"の場所を変更
define( 'WP_CONTENT_DIR', "{$_SERVER['DOCUMENT_ROOT']}/files" );
define( 'WP_CONTENT_URL', WP_HOME . '/files' );

/** 省略 **/

/* 編集が必要なのはここまでです ! WordPress でのパブリッシングをお楽しみください。 */

/** 省略 **/

変更後、サイトにアクセスして変更前と見た目は何も変化が無いのに加え、ソース内ではwp-contentからfilesに変更されていれば問題ありません(FastCGI Cacheなどのキャッシュ類が効いていると変更後の情報になっていない場合があるので注意)。

これによりwp-contentfilesというディレクトリ名で運用することが可能となりました。

しかし、wp-contentディレクトリそのものは存在しているため、更にNginxでディレクトリの存在を秘匿します(ディレクトリをリネームしている場合は存在しないので不要)。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定ファイルを編集します。

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "/wp-content"もしくは"/wp-content/"を含むURLにアクセス時、"/?access=wp-content"へ内部リダイレクトする
location ~ "/wp-content(?:$|/)" {
     rewrite ^/  /?access=wp-content? last;
}

## 省略 ##

編集後は$ sudo service nginx restartで再起動します。

これでwp-contentへアクセスがやってきても、他の存在しないファイルと同様の動作をするようになります。

Pluginsディレクトリ

wp-contentを無事変更できましたら、次はプラグイン(plugins)ディレクトリも変更しておくとより良いでしょう。

こちらも先程と同じく簡単に変更することができます。

ただし、プラグインによってはpluginsというディレクトリ名でないと使用できないものもあります。

その場合は自身でプラグインソースを変更することで使用できますが、可用性を考慮していないプラグインは変更によって別の悪影響が発生する可能性もあるため、できればそのようなプラグインは使わない方が良いかもしれません。

では最初に$ ln -s /var/www/ドメイン名/apps/wp-content/plugins /var/www/ドメイン名/addonsでシンボリックリンクを作成します。

例では変更後階層のフルパスは/var/www/ドメイン名/addonsとなっています。

次にwp-config.phpで再び追加編集を行います。

wp-config.php

/** 省略 **/

/* カスタム値は、この行と「編集が必要なのはここまでです」の行の間に追加してください。 */

/** 省略 **/

// "plugins"の場所を変更
define( 'WP_PLUGIN_DIR', "{$_SERVER['DOCUMENT_ROOT']}/addons" );
define( 'WP_PLUGIN_URL', WP_HOME . '/addons' );

/** 省略 **/

/* 編集が必要なのはここまでです ! WordPress でのパブリッシングをお楽しみください。 */

/** 省略 **/

これによりpluginsディレクトリはaddonsディレクトリとして変更することができました。

そして前回と同じようにNginxの設定を変更してpluginsディレクトリそのもののアクセスを排除します。

$ sudo vim /etc/nginx/conf.d/ドメイン名.confで設定ファイルを編集します。

ドメイン名.conf

## 省略 ##

# "location ~ \.php$"より上に置くこと
# "/plugins"もしくは"/plugins/"を含むURLにアクセス時、"/?access=plugins"へ内部リダイレクトする
location ~ "/plugins(?:$|/)" {
     rewrite ^/  /?access=plugins? last;
}

## 省略 ##

編集後は$ sudo service nginx restartで再起動します。

これでpluginsディレクトリへのアクセスも排除することができました。

WordPressを使用しているかチェックする

とても多くの設定を行ってきましたが、これによってサイトの構造は大きく変化しました。

そのため、構造上はWordPressを使用していることを外部からはある程度判別しにくくなったと思われます。

ただし、テーマ内で出力されるHTMLコード内にはWordPressだと分かってしまう要素が数多くあるため、テーマ内のコードもすべて改変することにより、更に秘匿することができるでしょう。

最後に、ウェブサイトがWordPressを使用しているかどうかを簡単にチェックできるサービスを使って、外部からはどのように見えるのかを調べてみることにします。

これまでの設定を加えたテスト用サイトを調べてみた所、以下のような結果になりました。

外部から見てWordPressサイトであるかをチェック(要素を横2列に編集しています)

このテストではチェックした項目に限り、サイトがWordPressで無いことを示しています。

WordPressを構成する要素は多岐に渡るため、このテストを突破したとしても完璧な設定とは言えませんが、ある程度の成果を残すことはできました。

WordPressその他設定

このセクションはおまけのようなもので、使うと便利かもしれない設定をいくつか紹介します。興味がなければスルーしてください。

これらはすべてfunctions.php内に記載するものです。

functions.php

/** 省略 **/

// __return_false()
// https://developer.wordpress.org/reference/functions/__return_false/
// load_textdomain_mofile
// https://developer.wordpress.org/reference/hooks/load_textdomain_mofile/
// 翻訳ファイルを無効化
add_filter( 'load_textdomain_mofile', '__return_false' );

// NginxとWordpressの404ページがproxy cacheできない
// http://qiita.com/osamu1203/items/9ce685df1ce3649c1819
// 404ページをキャッシュ無効化させない
function remove_nocache_on_404(){
    if(is_404()){
        header_remove("Pragma");
        header_remove("Cache-Control");
        header_remove("Expires");
    }
}
add_action('wp','remove_nocache_on_404');

// コメントの投稿欄でタグを無効にしてみた
// http://serenegiant.com/blog/?p=1360
// コメントのHTML使用を無効化
remove_filter('comment_text', 'make_clickable', 9);
add_filter( 'comment_text_rss', 'escape_tags', 9);
add_filter( 'comment_excerpt',  'escape_tags', 9);
function escape_tags( $comment_content ) {
	if ( get_comment_type() == 'comment' ) {
		$comment_content = htmlentities($comment_content, ENT_QUOTES, "UTF-8");
	}
	return $comment_content;
}

// WPによる自動画像圧縮を無効
add_filter('jpeg_quality', function($arg){return 100;});
add_filter( 'wp_editor_set_quality', function($arg){return 100;});

// HTMLをミニファイ(縮小)する
// unfulvio / functions.php
// https://gist.github.com/unfulvio/5889564
/* Minifies HTML and removes comments (except IE tags and comments within script tags)
 *
 * To disable compression of code portions, use '<!--wp-html-compression no compression-->' tag
 *
 * @see http://forrst.com/posts/Wordpress_Minify_output_HTML-29q
 * @see http://www.intert3chmedia.net/2011/12/minify-html-javascript-css-without.html
 */
class WP_HTML_Compression
{
        // Settings
        protected $compress_css = true;
        protected $compress_js = false;
        protected $info_comment = true;
        protected $remove_comments = true;

        // Variables
        protected $html;

        public function __construct($html)
        {
                if (!empty($html))
                {
                        $this->parseHTML($html);
                }
        }

        public function __toString()
        {
                return $this->html;
        }

        protected function minifyHTML($html)
        {
                $pattern = '/<(?<script>script).*?<\/script\s*>|<(?<style>style).*?<\/style\s*>|<!(?<comment>--).*?-->|<(?<tag>[\/\w.:-]*)(?:".*?"|\'.*?\'|[^\'">]+)*>|(?<text>((<[^!\/\w.:-])?[^<]*)+)|/si';                
                preg_match_all($pattern, $html, $matches, PREG_SET_ORDER);
                $overriding = false;
                $raw_tag = false;
                // Variable reused for output
                $html = '';
                foreach ( $matches as $token ) {

                        $tag = (isset($token['tag'])) ? strtolower($token['tag']) : null;    
                        $content = $token[0];

                        if ( is_null( $tag ) ) {

                                if ( !empty( $token['script'] ) ) {

                                        $strip = $this->compress_js;

                                } else if ( !empty($token['style'] ) ) {

                                        $strip = $this->compress_css;

                                } else if ( $content == '<!--wp-html-compression no compression-->' ) {

                                        $overriding = !$overriding;
                                        // Don't print the comment
                                        continue;

                                } else if ( $this->remove_comments ) {

                                        if ( !$overriding && $raw_tag != 'textarea' ) {

                                                // Remove any HTML comments, except MSIE conditional comments
                                                $content = preg_replace('/<!--(?!\s*(?:\[if [^\]]+]|<!|>))(?:(?!-->).)*-->/s', '', $content);                                                
                                        }
                                }

                        } else {

                                if ( $tag == 'pre' || $tag == 'textarea' || $tag == 'script' ) {

                                        $raw_tag = $tag;

                                } else if ( $tag == '/pre' || $tag == '/textarea' || $tag == '/script' ) {

                                        $raw_tag = false;

                                } else {

                                        if ($raw_tag || $overriding) {

                                                $strip = false;

                                        } else {

                                                $strip = true;

                                                // Remove any empty attributes, except:
                                                // action, alt, content, src
                                                $content = preg_replace('/(\s+)(\w++(?<!\baction|\balt|\bcontent|\bsrc)="")/', '$1', $content);

                                                // Remove any space before the end of self-closing XHTML tags
                                                // JavaScript excluded
                                                $content = str_replace(' />', '/>', $content);                                                
                                        }

                                }

                        }

                        if ( $strip ) {

                                $content = $this->removeWhiteSpace($content);                                
                        }

                        $html .= $content;                        
                }

                return $html;
        }

        public function parseHTML($html)
        {
                $this->html = $this->minifyHTML($html);
        }

        protected function removeWhiteSpace($str)
        {
                $str = str_replace( "\t", ' ', $str );
                $str = str_replace( "\n",  '', $str );
                $str = str_replace( "\r",  '', $str );

                while ( stristr($str, '  ' ) ) {

                        $str = str_replace('  ', ' ', $str);
                }

                return $str;
        }
}

function wp_html_compression_finish($html) {

        return new WP_HTML_Compression($html);
}

function wp_html_compression_start() {

        ob_start( 'wp_html_compression_finish' );        
}

add_action( 'get_header', 'wp_html_compression_start' );

/** 省略 **/

おわりに

多くの対策方法を紹介してきましたが、これで問題が全く発生しないというわけではありません。

クラッキング等を行う悪意のある人々は常にWordPressの脆弱性を狙っています。

彼らからの攻撃をできるだけ回避する方法として大きな役割を持つのは、WordPressを常に最新版へアップデートすることです。

脆弱性のあるファイルをそのまま利用することはリスクが大きいため、たとえ面倒だとしても更新を欠かさずに行ってください。

また、WordPressを拡張するには非常に便利なプラグインですが、便利だからと言って何でもインストールするのは大変よろしくありません。

拡張できるということは何でもできるに等しく、プラグインそのものの脆弱性や機能の重複などによって予期しない不具合が発生する可能性があります。

ウェブサイトの動作が遅くなるだけならまだしも、データベースの破壊や大切な情報の流出、更にはウィルス侵入のきっかけになってしまっては元も子もありません。

そのため、基本的にプラグインは使用しないと考え、それでもプラグインをインストールしたい際には、それが『本当にこのサイトに必要なのか』どうかを考えなければなりません。

これはプラグインに限らずテーマも同様であり、ソースコードに悪意のあるコードが含まれている可能性があるため、信頼できない所のファイルを導入するのは特にやめておくべきです。

WordPressには多くの問題が指摘されているものの、それはWordPressだけに留まらず、利用する者の意識と行動によってその危険性は大きく左右されます。

少しでもリスクを減らすために、ウェブサイトを運営する際にはセキュリティについて日々意識していきましょう。

参考資料