管理しているサイトで、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 のすぐ上に、次のブロックを置きました。
# 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 回流します。
パスは、エックスサーバーのフォルダの形で書いています。ほかのサーバーでは、公開のフォルダの場所に合わせて変えてください。
(
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 に転送されることを確かめました。
# 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(前)
<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)。
確かめ方
# 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 つ出るので、同じ書き方にそろえる


