nginxの location は、多くの人が思っているような「設定ファイルの上から順に最初に一致したもの」ではありません。この誤解のせいで、認証をかけたはずのパスがすり抜ける、公開ディレクトリの外のファイルが読めてしまうといった事故が起きます。
- 対象: nginx でサイトを運用している Web 担当者・制作会社・インフラ担当(中級者)
-
解決すること:
locationの評価順を正しく理解し、認証バイパス・パストラバーサル・ヘッダ消失につながる 4 つの設定ミスを塞ぐ - 前提: 攻撃手順は書きません。すべて「同じ穴を作らない」ための設定と確認方法です
location の評価順は「書いた順」ではない
nginx は次の順序で location を選びます。これは設定ファイルの記述順とは無関係です。
-
完全一致
= /path… 一致したら即決定(以降を見ない) -
^~付きの前方一致 … 一致する前方一致のうち最長のものに^~が付いていれば、それを採用し正規表現を評価しない -
正規表現
~(大文字小文字を区別)/~*(区別しない) … 設定ファイルに書いた順で先頭から評価し、最初に一致したものを採用 - 前方一致(通常) … 正規表現がどれも一致しなければ、最長の前方一致を採用
ポイントは 「最長の前方一致を覚えておき、正規表現が一致すればそちらを優先する」 という点です。つまり、location /admin/ で認証をかけていても、後ろに書いた正規表現 location ~ \.php$ が先に一致すれば、認証ブロックは丸ごと無視されます。
まずは自分のサーバで、実際に有効になっている設定を展開して確認しておきます。
# 全 include を展開した「最終的に効いている設定」を表示
sudo nginx -T | less
# 変更後は必ず構文チェック → リロード
sudo nginx -t && sudo systemctl reload nginx
罠1: 正規表現 location が認証を素通りする
次はよくある「危ない」書き方です。管理画面に BASIC 認証をかけたつもりが、PHP は認証なしで実行できてしまいます。
# 悪い例: /admin/ に認証をかけたつもり
location /admin/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
/admin/tool.php へのリクエストは、前方一致 /admin/ より 正規表現 \.php$ が優先されるため、auth_basic を含まない PHP 用 location で処理されます。結果として認証はかかりません。
対策は、保護したい前方一致に ^~ を付けて正規表現より優先させることです。そのうえで、認証ブロックの中で PHP を処理します。
# 良い例: ^~ で正規表現より優先させ、内側で PHP を処理
location ^~ /admin/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
確認は、認証情報なしで 401 が返ることを見ます。
# 401 (Unauthorized) が返れば OK。200 が返るなら素通りしている
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/admin/tool.php
罠2: alias + 末尾スラッシュ不一致でディレクトリの外が読める
alias を使うとき、location と alias で末尾スラッシュの有無がずれていると、公開ディレクトリの外のファイルが参照できてしまう典型的なミスです。
# 悪い例: location に末尾スラッシュが無く、alias に付いている
location /assets {
alias /var/www/app/public/assets/;
}
この場合、/assets の後ろに付いた文字列がそのまま alias のパスに連結されるため、../ を含むリクエストで /var/www/app/public/assets/ の親ディレクトリへ抜け出せる余地が生まれます(設定情報などが置かれた領域まで到達しうる)。
対策は 2 つ。どちらか片方で十分です。
# 対策A: location と alias の末尾スラッシュを揃える
location /assets/ {
alias /var/www/app/public/assets/;
}
# 対策B(推奨): alias ではなく root を使う
# root は「location のパスを document root に付け足す」動きなので、
# 末尾スラッシュのズレによる抜け出しが起きにくい
location /assets/ {
root /var/www/app/public; # 実体は /var/www/app/public/assets/...
}
確認は、../ を含むパスが 404 / 403 になり、公開範囲の外に出られないことを見ます。
# 公開域の外に出られないこと(404 か 403 が返る)を確認
curl -s -o /dev/null -w '%{http_code}\n' 'https://example.com/assets/../nginx.conf'
罠3: PHP location に try_files が無く、存在しないパスを FPM に渡す
~ \.php$ の location で、リクエストされた .php が実在するか確認せずに FastCGI へ渡すと、アップロードされたファイルや存在しないパスが意図せず PHP として解釈される余地を残します。
# 悪い例: 実在チェックが無い
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
対策は、try_files $uri =404; で実在しない .php を FPM に渡さないこと、そして fastcgi_split_path_info と cgi.fix_pathinfo=0 で PATH_INFO の付け足しを封じることです。
# 良い例
location ~ [^/]\.php(/|$) {
try_files $uri =404; # 実在しない .php は 404
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
あわせて PHP 側でも次を設定しておきます(php.ini)。
; 存在しないパスを近くの .php に紐付けさせない
cgi.fix_pathinfo=0
罠4: 内側 location の add_header が上位のセキュリティヘッダを消す
nginx の add_header には見落としやすい継承ルールがあります。ある階層に add_header を 1 つでも書くと、上位の階層で定義した add_header はその階層では一切引き継がれません。
server {
# サイト全体にセキュリティヘッダを付けたつもり
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
location /api/ {
# ここで add_header を書いた瞬間、上の 2 つは /api/ では消える
add_header Cache-Control "no-store" always;
}
}
この結果、/api/ 配下だけ X-Content-Type-Options や X-Frame-Options が欠落します。対策は、ヘッダ群を 1 つのファイルにまとめて include し、add_header を書く階層すべてで読み込むことです。
# /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
server {
include snippets/security-headers.conf;
location /api/ {
include snippets/security-headers.conf; # 内側でも読み直す
add_header Cache-Control "no-store" always;
}
}
確認は、代表的なパスすべてでヘッダが揃っているかを見ます。
# 各パスで nosniff / X-Frame-Options が出ているか確認
curl -sI https://example.com/ | grep -iE 'x-content-type-options|x-frame-options'
curl -sI https://example.com/api/test | grep -iE 'x-content-type-options|x-frame-options'
まとめ
-
locationは「書いた順」ではなく 完全一致 →^~前方一致 → 正規表現(記述順)→ 最長前方一致 の順で選ばれる -
罠1: 認証は保護対象に
^~を付けて正規表現より優先し、内側で PHP を処理する -
罠2:
aliasはlocationと末尾スラッシュを揃えるか、可能ならrootに置き換える -
罠3: PHP location には
try_files $uri =404;とcgi.fix_pathinfo=0を付ける -
罠4:
add_headerは書いた階層で上位が消えるので、スニペットを各階層でincludeする - 変更後は
nginx -Tで最終形を確認し、nginx -t && reload、curlで挙動を検証する
location の優先順位は一度理解すれば応用が効きます。既存サイトでも nginx -T で今の設定を一度棚卸ししておくと、思わぬ素通りに気づけます。
関連記事
本記事のような設定の抜けは、Webサイトを9つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp