セキュリティのヘッダーを .htaccess でそろえる:SiteGuard とサブドメインの注意

広告WordPress・PHP を学べる本

Amazonのアソシエイトとして、WordPress functions.phpは適格販売により収入を得ています。

管理しているサイトで、HTTP のレスポンスヘッダーを見直していたときのことです。

外から curl -I で見ると、セキュリティのヘッダーが出ていないサイトが多くありました。

出ていたサイトも、X-Frame-Options が DENY になっていました。

DENY だと、WordPress のカスタマイザーで、右のプレビューが出ないことがあります。

そこで、4 つのヘッダーを、.htaccess の同じ書き方にそろえました。

この記事では、そろえた書き方と入れ方、作業の中で気づいた 2 つのこと、作業環境で確かめた結果をまとめます。

そろえた 4 つのヘッダー

ヘッダー 役目 そろえた値
Strict-Transport-Security(HSTS) 一度 https で開いたブラウザーに、決まった期間は http ではなく https で開くように伝える max-age=31536000(1 年)
X-Content-Type-Options ブラウザーが、ファイルの中身から種類を推測しないようにする nosniff
X-Frame-Options ほかのサイトの iframe の中で、ページを表示させない SAMEORIGIN(同じサイトの中だけ表示できる)
Referrer-Policy リンク先に、どこから来たか(リファラー)をどこまで送るかを決める strict-origin-when-cross-origin

strict-origin-when-cross-origin は、ほかのサイトへはドメインまでだけを送り、https から http へは送らない値です。

いまのブラウザーで、何も指定しないときに使われる値と同じです。

ヘッダーで書いておくと、ブラウザーによる違いがなくなります。

そろえた書き方

.htaccess の # BEGIN WordPress のすぐ上に、次のブロックを置きました。

.htaccess(# BEGIN WordPress のすぐ上)
# BEGIN security headers
<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=31536000" env=HTTPS
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# END security headers
書き方 役目
# BEGIN security headers 〜 # END security headers 自分で書いたブロックだとわかるようにする区切り。あとで探すときや、もう入っているかを見るときにも使う
<IfModule mod_headers.c> mod_headers が使えるときだけ読む。使えないサーバーでも 500 のエラーにならない
Header always set ヘッダーを決まった値にする。always を付けると、200 のほか、301 や 404 などの応答にも付く
env=HTTPS https で開いたときだけ HSTS を出す。http の応答に HSTS を付けても意味がないため
"SAMEORIGIN" 同じサイトの中の iframe は許す。カスタマイザーのプレビューは同じサイトの iframe なので表示できる

# BEGIN WordPress 〜 # END WordPress の中は、WordPress がパーマリンクの設定を保存したときなどに書き直します。

そのため、自分の決まりは、その区切りの外に置きます。

always を付ける理由

mod_headers は、ヘッダーを 2 つの表に分けて持っています。

書き方 入る表 付く応答
Header set ふつうの表(onsuccess) 200 などの成功の応答
Header always set always の表 301・404 などを含む、すべての応答

作業環境でも、Header set だけで書いたときは、301 の応答にヘッダーが付きませんでした(下の「確かめた結果」の 5)。

転送やエラーの画面にも同じヘッダーを付けたいので、always にそろえました。

X-Frame-Options を SAMEORIGIN にした理由

カスタマイザーは、右のプレビューに、同じサイトのページを iframe で開いています。

DENY は、同じサイトの iframe も止めるので、プレビューが出ないことがあります。

プレビューが出ないと、色・メニュー・ウィジェット・追加 CSS などを変えたときに、公開する前に見た目を確かめられません。

確かめないまま「公開」を押すことになり、崩れていても、表のページを開くまで気づけません。

気づくまでのあいだ、サイトを見に来た人には、崩れたページが表示されてしまいます。

WordPress も、ログインの画面と管理画面では、自分で X-Frame-Options: SAMEORIGIN を送っています(send_frame_options_header())。

.htaccess の値も SAMEORIGIN にしておくと、WordPress が送る値とぶつかりません。

HSTS に includeSubDomains を付けない理由

HSTS には、includeSubDomains を付けると、サブドメインもすべて https で開くように伝える書き方があります。

今回は付けていません。

サブドメインの中に、まだ https にしていないものがあると、ブラウザーがそのサブドメインを開けなくなるためです。

HSTS は、ブラウザーが max-age の期間(ここでは 1 年)覚えているので、あとから外しても、すぐには戻りません。

すべてのサブドメインが https になってから、付けるかを考えます。

SSH で入れるコマンド

サーバーに SSH で入り、次のコマンドを 1 回流します。

パスは、エックスサーバーのフォルダの形で書いています。ほかのサーバーでは、公開のフォルダの場所に合わせて変えてください。

SSH でサーバーに入って(パスは例の名前)
(
    cd /home/(ユーザー名)/(ドメイン)/public_html || exit 1
    f=.htaccess
    b=$(mktemp)
    cat > "$b" <<'EOF'
# BEGIN security headers
<IfModule mod_headers.c>
    Header always set Strict-Transport-Security "max-age=31536000" env=HTTPS
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# END security headers

EOF
    if grep -q '^# BEGIN security headers' "$f"; then
        echo 'skip: already added'
    elif ! grep -q '^# BEGIN WordPress' "$f"; then
        echo 'stop: # BEGIN WordPress not found'
    else
        cp -p "$f" "$f.$(date +%Y%m%d)"
        awk 'NR == FNR { b = b $0 "\n"; next }
            !done && /^# BEGIN WordPress/ { printf "%s", b; done = 1 }
            { print }' "$b" "$f" > "$f.new" \
            && cat "$f.new" > "$f" && rm "$f.new" && echo 'added'
    fi
    rm -f "$b"
)
書き方 役目
( … ) 全体をサブシェルで動かす。exit しても、SSH の接続は切れない
cd … || exit 1 サイトのフォルダに移る。フォルダがなければ何もしない
cat > "$b" <<'EOF' 足すブロックを、一時ファイルに書く。'EOF' と引用符で囲むと、%{…} や $ がそのまま書かれる
grep -q '^# BEGIN security headers' 同じブロックがもうあれば、何もしない。2 回流しても 2 つにならない
grep -q '^# BEGIN WordPress' 置き場所の # BEGIN WordPress の行がなければ、書き換えずに止める
cp -p "$f" "$f.$(date +%Y%m%d)" 書き換える前に、.htaccess.20261014 のような名前でバックアップを取る
awk 'NR == FNR { … }' 1 つ目のファイル(ブロック)を読んでおき、2 つ目のファイル(.htaccess)の # BEGIN WordPress の行の前に 1 回だけ出す
cat "$f.new" > "$f" 中身だけを入れ替える。mv と違い、.htaccess のパーミッションはそのまま残る

流したあとは、diff .htaccess.(日付) .htaccess で、足した行だけが増えていることを見ます。

うまく動かないときは、バックアップを cp で戻せば、元の状態に戻ります。

気づいたこと 1:SiteGuard の区切りの中に書いた決まりは消える

あるサイトで、http を https に転送する決まりが、SiteGuard の区切りの中に書かれていました。

SiteGuard の区切りは、#SITEGUARD_PLUGIN_SETTINGS_START から #SITEGUARD_PLUGIN_SETTINGS_END までの行です。

SiteGuard の設定を保存したときに、この区切りの中が書き直されて、転送の決まりが消えていました。

そのため、http のまま開ける状態になっていました。

転送の決まりを区切りの外に置き直して、http が 301 で https に転送されることを確かめました。

.htaccess(SiteGuard の区切りの外に置く)
# BEGIN https redirect
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
# END https redirect

#SITEGUARD_PLUGIN_SETTINGS_START
(SiteGuard が書く行。ここには自分の決まりを書かない)
#SITEGUARD_PLUGIN_SETTINGS_END
書き方 役目
RewriteCond %{HTTPS} off http で開いたときだけ、次の行を動かす
RewriteRule … [R=301,L] 同じ URL の https へ、301 で転送する
# BEGIN https redirect など 自分で書いたブロックだとわかるようにする区切り

SiteGuard がどう書き直すか

SiteGuard(バージョン 1.8.9)の .htaccess を書く処理を、作業環境で、試しの .htaccess に向けて動かしました。

区切りの中には、機能ごとの小さな区切り(#==== SITEGUARD_RENAME_LOGIN_SETTINGS_START など)があります。

行を置いた場所 ログインの URL の変更を保存したとき 区切り全体を消す処理のとき
機能ごとの区切りの中 消えた 消えた
外側の区切りの中で、機能ごとの区切りの外 残った 消えた
外側の区切りの外 残った 残った

機能ごとの区切りは、保存のたびに、いったん消してから書き直されます。

外側の区切りの中も、SiteGuard が自分のものとして扱う場所です。

自分の決まりは、外側の区切りの外に置くのが安全です。

気づいたこと 2:親のフォルダの .htaccess は、サブドメインにも効く

エックスサーバーでは、サブドメインの公開フォルダが、本体の public_html の中にあります。

例:/home/(ユーザー名)/(ドメイン)/public_html/(サブドメイン)/

Apache は、開くファイルまでの親のフォルダの .htaccess も順に読みます。

そのため、本体の .htaccess に書いた Header は、サブドメインのページにも付きます。

あるサイトでは、サブドメインのフォルダの .htaccess に、古い書き方が残っていました。

サブドメインのフォルダの .htaccess(前)
# サブドメインのフォルダの .htaccess(前)
<IfModule mod_headers.c>
    Header set X-Frame-Options DENY
</IfModule>

本体に Header always set X-Frame-Options "SAMEORIGIN" を足すと、サブドメインのページには、SAMEORIGIN と DENY の 2 つが出るおそれがありました。

Header set と Header always set は別の表に入るので、子のフォルダの Header set は、親の Header always set を上書きしません。

値の違う X-Frame-Options が 2 つあると、ブラウザーによって扱いが分かれるおそれがあります。

そこで、サブドメインの古い行を、本体と同じ 4 行のブロックに入れ替えました。

同じ always の表に同じ値を書くので、1 つにまとまります(下の「確かめた結果」の 3)。

確かめ方

手元の PC のターミナルで
# 4 つのヘッダーを見る(https)
curl -sI https://(ドメイン)/ | grep -iE '^(strict-transport-security|x-content-type-options|x-frame-options|referrer-policy):'

# 同じ名前が 2 つ出ていないかを数える(4 つとも 1 になればよい)
curl -sI https://(ドメイン)/ | grep -ioE '^(strict-transport-security|x-content-type-options|x-frame-options|referrer-policy):' | sort | uniq -c

# http は 301 で https へ
curl -sI http://(ドメイン)/ | grep -iE '^(HTTP|location)'
見ること どう見るか よいとき
4 つのヘッダー curl -sI で https のトップを見る 4 つが 1 つずつ出る
ページが開くか トップ・記事・sitemap.xml・テーマの CSS を curl -sI で見る どれも 200
https への転送 http のトップを curl -sI で見る 301 で、Location が https
管理画面 ブラウザーでログインする ログインできる
カスタマイザー 「外観」→「カスタマイズ」を開く 右にプレビューが出る
プレビュー 投稿の編集の画面で「プレビュー」を押す 記事のプレビューが出る

サブドメインがあるサイトは、サブドメインのトップも、同じように見ます。

確かめた結果

作業環境(Apache 2.4.56・本番から複製した WordPress 7.1.2 のサイト)で確かめました。

作業環境は http だけなので、HSTS は env=HTTPS により出ません。

入れるコマンド

まず、作業環境のサイトの .htaccess を手元にコピーして、コマンドを流しました。

試したこと 結果
1 回目 added。# BEGIN WordPress のすぐ上にブロックが入った。日付の付いたバックアップは元のファイルと同じ
同じファイルに 2 回目 skip: already added。ブロックは 1 つのまま
# BEGIN WordPress のない .htaccess stop: # BEGIN WordPress not found。ファイルは変わらず、バックアップも作られない

足す前と後

次に、作業環境のサイトの .htaccess のバックアップを取り、同じコマンドで足して、curl -sI で見ました。

開いたページ 足す前 足した後
トップ 200・4 つとも出ない 200・3 つが 1 つずつ(HSTS は http なので出ない)
記事 200・4 つとも出ない 200・3 つが 1 つずつ
sitemap.xml 200・4 つとも出ない 200・3 つが 1 つずつ
テーマの CSS 200・4 つとも出ない 200・3 つが 1 つずつ
ないページ(転送される URL) 302・4 つとも出ない 302・3 つが 1 つずつ(always のため)
ログインの画面 (足す前は見ていません) 200・X-Frame-Options と Referrer-Policy が同じ値で 2 つずつ

ログインの画面で 2 つずつ出たのは、WordPress が PHP からも同じヘッダーを送っているためです。

PHP が送るヘッダーはふつうの表に入り、Header always set は always の表に入るので、両方が出ます。

値は .htaccess と同じ(SAMEORIGIN・strict-origin-when-cross-origin)なので、そのままにしました。

確かめたあと、.htaccess をバックアップから戻し、コマンドが作った日付のバックアップも消しました。

ログイン・カスタマイザー・プレビューの画面の動きは、作業環境では試していません。

親と子の .htaccess

作業環境のサイトの中に、試しのフォルダ(親)と、その中のフォルダ(子)を作りました。

子のフォルダの HTML(200)と、子の .htaccess の Redirect 301 の URL(301)を開いて、X-Frame-Options の数を数えました。

親の .htaccess 子の .htaccess 200 の応答 301 の応答
1 always set SAMEORIGIN (なし) 1 つ(SAMEORIGIN) 1 つ(SAMEORIGIN)
2 always set SAMEORIGIN set DENY 2 つ(SAMEORIGIN・DENY) 1 つ(SAMEORIGIN)
3 always set SAMEORIGIN always set SAMEORIGIN 1 つ(SAMEORIGIN) 1 つ(SAMEORIGIN)
4 always set SAMEORIGIN set SAMEORIGIN 2 つ(SAMEORIGIN・SAMEORIGIN) 1 つ(SAMEORIGIN)
5 set DENY set SAMEORIGIN 1 つ(SAMEORIGIN) 出ない
6 (なし) set DENY と always set SAMEORIGIN 2 つ(SAMEORIGIN・DENY) 1 つ(SAMEORIGIN)
7 always set SAMEORIGIN always set SAMEORIGIN と set DENY 2 つ(SAMEORIGIN・DENY) 1 つ(SAMEORIGIN)

1 から、親の .htaccess のヘッダーが、子のフォルダにも付くことがわかります。

2・4・6・7 のように、set と always set が両方あると、200 の応答で 2 つ出ました。

同じ表どうし(3 と 5)では、1 つになりました。

5 では、親の DENY ではなく、子の SAMEORIGIN が出ました。

5 のように set だけのときは、301 の応答にヘッダーが付きませんでした。

今回の試しは、同じサイトの中の親と子のフォルダで行いました。

サブドメインの形そのもの(別のドメインで、本体の public_html の中のフォルダを開く形)では、作業環境で試していません。

確かめたあと、試しのフォルダは消しました。

SiteGuard の書き直し

SiteGuard 1.8.9 の .htaccess を書くクラスを、作業環境で、試しの .htaccess に向けて動かしました(プラグインのファイルは変えていません)。

3 か所に試しの行を置き、ログインの URL の変更を保存したときと同じ順(消してから書く)で動かした結果が、上の「SiteGuard がどう書き直すか」の表です。

このブログの本番(https)

このブログの本番でも、表のページを curl -sI で見ました。

開いたページ 結果
トップ(https) 200・4 つが 1 つずつ(HSTS も出た)
sitemap.xml(https) 200・4 つが 1 つずつ
トップ(http) 301・Location は https

気をつけること

  • 書き換える前に、.htaccess のバックアップを取ってください。書き間違えると、サイト全体が 500 のエラーになります
  • 自分の決まりは、# BEGIN WordPress 〜 # END WordPress の外、SiteGuard の区切りの外に置いてください。どちらも、保存のときに書き直されます
  • サブドメインがあるときは、サブドメインのフォルダの .htaccess に、古い Header set が残っていないかを見てください。残っていたら、同じブロックに入れ替えます
  • キャッシュのプラグインや、ほかのセキュリティのプラグインも、同じヘッダーを送ることがあります。足したあとは、ヘッダーの数を数えてください
  • HSTS は、ブラウザーが max-age の期間覚えています。https をやめる予定のあるサイトには付けないでください
  • includeSubDomains は、すべてのサブドメインが https になってから考えてください

まとめ

  • 4 つのヘッダーを、Header always set の同じブロックにそろえ、# BEGIN WordPress のすぐ上に置いた
  • X-Frame-Options は SAMEORIGIN。DENY だとカスタマイザーのプレビューが出ないことがある
  • HSTS は env=HTTPS で https のときだけ出し、includeSubDomains は付けない
  • 入れるときは、同じものがあれば何もしない・先にバックアップ・# BEGIN WordPress がなければ書き換えない、のコマンドを 1 回流す
  • SiteGuard の区切りの中に書いた決まりは、設定の保存で消える。区切りの外に置く
  • 親のフォルダの .htaccess のヘッダーは、下のフォルダ(サブドメイン)にも付く。set と always set が両方あると 2 つ出るので、同じ書き方にそろえる

参考

関連機能

  1. WordPress .htaccessでCache-Controlヘッダーによるブラウザキャッシュの設定方法

  2. WordPress wp-cron.phpを無効 定期処理の実行タイミングをページアクセス時からcronに変更してページ表示を高速化

  3. WordPress のリビジョンの数を減らす・止める(wp-config.php と投稿タイプごとの設定・自動保存の間隔)

サイドバー

よく使うカスタマイズ

最近の記事

Profile

PAGE TOP