ドットファイルを一律に拒否するつもりで .htaccess に <FilesMatch "^\."> を書いても、/.git/config は 200 で中身が返りました。/.env は 403 になります。FilesMatch が見るのはパスの最後のファイル名だけで、/.git/config のファイル名は config だからです。
Apache 2.4.58(Ubuntu)で、ドキュメントルートに .env、.git/config、.aws/credentials などを置いて測りました。
| リクエスト | 何も書かない | <FilesMatch "^\."> |
|---|---|---|
/.env |
200 | 403 |
/.git/config |
200 | 200 |
/.git/HEAD |
200 | 200 |
/.git/(一覧表示あり) |
200 | 200 |
/.aws/credentials |
200 | 200 |
/sub/.git/config |
200 | 200 |
/.well-known/acme-challenge/tok |
200 | 200 |
止まったのは、ドキュメントルート直下のドットファイルだけでした。ドットで始まるディレクトリの中身は、どれも返っています。
先に断っておくと、<FilesMatch> がファイル名に対して働くことは、Apache のドキュメントの <Files> と <FilesMatch> の説明に書かれています。.git を隠すなら RedirectMatch や RewriteRule でパスを見る、という書き方も、いろいろな記事で紹介されています。ここで書くのは、ドットファイル対策としてよく見かける書き方を実際のリクエストで並べた結果と、ディレクトリまで止める書き方との比較です。
きっかけ
自分の WordPress のサイトに、.env、.git/config、.aws/credentials などを片っ端から試すアクセスが来ていました。どれも Wordfence で止まっています。
そのログを Claude に見せたところ、対策の1つとして、次の .htaccess を勧められました。
<FilesMatch "^\.">
Require all denied
</FilesMatch>
<FilesMatch "\.(env|bak|old|save|sql|log|yml)$">
Require all denied
</FilesMatch>
ねらわれたパスの中には /.git/config もありました。この設定で本当に止まるのかを、手元の Apache で確かめました。
FilesMatch は最後のファイル名だけを見る
/.git/config を開くと、Apache はディスク上の .git/config を探します。<FilesMatch> の正規表現は、その最後の config だけに当てられます。^\. には当たりません。2つ目の \.(env|bak|…)$ にも当たりません。
$ curl -s http://localhost:8098/.git/config
[core]
repositoryformatversion = 0
/.git/ は、ディレクトリの一覧表示(Options Indexes)が有効だと、HEAD や config へのリンクが並んだページが返りました。
/.git だけは 403 でした。この形では、最後の名前が .git なので ^\. に当たります。末尾に / を付けると、もう当たりません。
Options -Indexes で一覧表示を切ると、/.git/ は 403 になります。それでも /.git/config は 200 のままでした。一覧表示を切っても、ファイルを直接開かれれば返します。
ディレクトリまで止める書き方を3つ比べる
パスのどこかに /. があれば止める、という書き方を3つ試しました。.well-known は、SSL 証明書の更新で使うので除外しています。
# A: RedirectMatch(404 を返す)
RedirectMatch 404 "/\.(?!well-known/)"
# B: mod_rewrite(403 を返す)
RewriteEngine On
RewriteRule "(^|/)\.(?!well-known/)" - [F]
# C: <If>(403 を返す。Apache 2.4 以降)
<If "%{REQUEST_URI} =~ m#/\.(?!well-known/)#">
Require all denied
</If>
| リクエスト | FilesMatch(2つ) | A | B | C |
|---|---|---|---|---|
/.env |
403 | 404 | 403 | 403 |
/.git/config |
200 | 404 | 403 | 403 |
/.git/ |
200 | 404 | 403 | 403 |
/.aws/credentials |
200 | 404 | 403 | 403 |
/sub/.git/config |
200 | 404 | 403 | 403 |
/%2egit/config |
200 | 404 | 403 | 403 |
/.git//config |
200 | 404 | 403 | 403 |
/.well-known/acme-challenge/tok |
200 | 200 | 200 | 200 |
/wp-config.php.bak |
403 | 200 | 200 | 200 |
A・B・C のどれでも、ドットで始まるディレクトリの中まで止まりました。%2e でドットを書いたものや、// を挟んだものも止まっています。
wp-config.php.bak のような、ドットで始まらないバックアップファイルは、A・B・C のどれでも止まりません。拡張子で止める FilesMatch は、こちらのために残しておく必要があります。B の下に2つ目の FilesMatch を足すと、/.git/config と /wp-config.php.bak の両方が 403 になり、.well-known は 200 のままでした。
RewriteEngine On
RewriteRule "(^|/)\.(?!well-known/)" - [F]
<FilesMatch "\.(env|bak|old|save|sql|log|yml)$">
Require all denied
</FilesMatch>
A は 404 を返すので、ファイルがあるかどうかを外から見分けにくくなります。B と C は 403 です。
B は FollowSymLinks が無いと全部 403 になる
B を最初に試したとき、/index.html まで 403 になりました。エラーログには次の行が出ていました。
AH00670: Options FollowSymLinks and SymLinksIfOwnerMatch are both off,
so the RewriteRule directive is also forbidden ...
.htaccess で RewriteRule を使うには、そのディレクトリで FollowSymLinks か SymLinksIfOwnerMatch が有効になっている必要があります。テスト用の設定で Options Indexes とだけ書いていたため、両方とも切れていました。Options Indexes FollowSymLinks に直すと、表の結果になりました。
WordPress のパーマリンクも RewriteRule で動いているので、ふつうの WordPress のサイトでは有効になっているはずです。
WordPress のサイトで気にしておくこと
/.git/config が問題になるのは、ドキュメントルートに .git が本当にあるときです。git でデプロイしているサイトや、テーマやプラグインのフォルダーを git で管理しているサイトでは、/wp-content/themes/mytheme/.git/config のような場所に置かれていることがあります。表の /sub/.git/config が、この形です。
WordPress のセキュリティプラグインは、PHP の中で動きます。.git/config のような静的なファイルは、PHP を通らずに Apache がそのまま返すので、プラグインの判定より前に配信されます(この点は、今回は測っていません)。静的なファイルを止めるなら、Apache の設定で止めるのが確実です。
止められているかどうかは、curl で確かめられます。
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.git/config
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/wp-content/themes/mytheme/.git/config
200 が返ったら、中身が見えています。
ドットファイルを止めるときに見ておくこと
-
<FilesMatch "^\.">は、パスの最後のファイル名しか見ない。/.git/configや/.aws/credentialsは止まらない - ディレクトリまで止めるなら、
RedirectMatch、RewriteRule、<If>のどれかでパスを見る -
.well-knownは除外しておく - ドットで始まらないバックアップファイルは、拡張子で止める
FilesMatchを別に残す -
.htaccessのRewriteRuleにはFollowSymLinksが要る - 一覧表示を切っても、ファイルを直接開かれれば返る
- 書いたら、
curlで実際のパスを開いて確かめる
LiteSpeed など Apache 以外のサーバーでは試していません。
あなたのサイトの /.git/config は、いま開くと何が返ってきますか。
手元で試す手順
apt-get install -y apache2
D=/var/www/dt; mkdir -p $D/.git $D/.aws $D/.well-known/acme-challenge $D/sub/.git
echo SECRET > $D/.env
printf '[core]\n\trepositoryformatversion = 0\n' > $D/.git/config
echo KEY > $D/.aws/credentials; echo TOKEN > $D/.well-known/acme-challenge/tok
echo BAK > $D/wp-config.php.bak; echo sub > $D/sub/.git/config; echo index > $D/index.html
cat > /etc/apache2/sites-available/dt.conf <<'EOF'
Listen 8098
<VirtualHost *:8098>
DocumentRoot /var/www/dt
<Directory /var/www/dt>
AllowOverride All
Options Indexes FollowSymLinks
Require all granted
</Directory>
</VirtualHost>
EOF
a2ensite dt; a2enmod rewrite alias; apache2ctl start
for p in /.env /.git/config /.git/ /.aws/credentials /sub/.git/config \
/%2egit/config /.well-known/acme-challenge/tok /wp-config.php.bak; do
printf '%-35s %s\n' "$p" \
"$(curl -s --path-as-is -o /dev/null -w '%{http_code}' "http://localhost:8098$p")"
done
$D/.htaccess の中身を差し替えながら、最後の for を流します。
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/