INDEX
はじめに
「500エラーを403にリダイレクトする」——最初にこの案件を聞いたとき、正直そこまで大変な作業だとは思っていませんでした。.htaccessにルールを何行か書けば終わるだろう、と。
でも実際に手を動かしてみると、既存コードへの影響範囲の広さに何度もヒヤリとさせられました。「ここも影響受けるのか」「この機能、動かなくなってる」という発見が続いて、エラーハンドリングを後から全体に適用することの難しさを身をもって体験しました。
この記事では .htaccessの RewriteRuleとErrorDocumentを使った実装を通じて気づいたことや、実際にどんな問題が起きたかを共有したいと思います。同じような改修を検討している方の参考になれば嬉しいです。※ファイル名・ディレクトリ名は架空のものです
そもそも、なぜ500エラーをそのまま出してはいけないのか
まず前提として、なぜこの改修が必要だったのかを整理しておきます。
セキュリティ上のリスク
500エラー(Internal Server Error)は、サーバー内部で何らかの問題が起きたことを示すレスポンスコードです。ブラウザにそのまま返してしまうと、場合によってはスタックトレースやファイルパス、データベースのエラー情報などが露出してしまうことがあります。
攻撃者にとって、こういった情報はサーバー構成を把握するヒントになります。「このサーバーはPHPを使っている」「このパスにファイルがある」といった情報が漏れるだけでも、攻撃の糸口になりうるのです。
ユーザー体験の問題
セキュリティ面だけでなく、ユーザー体験の観点からも500エラーをそのまま見せるのは望ましくありません。意味のわからないエラー画面を突然見せられたユーザーは、「このサイト壊れてる」と感じてそのまま離脱してしまいます。
403(Forbidden)に統一してわかりやすいエラーページを表示することで、「アクセスできなかった」という情報をユーザーに適切に伝えることができます。
今回の実装方針:.htaccessで対応する
PHPのエラーハンドリングには set_exception_handler()などアプリケーション層で対応する方法もあります。ただし今回は、アプリケーションの状態に関係なくサーバー層でまとめて制御したかったため、.htaccessを使ったアプローチを採用しました。PHPコードを直接触らずに済む点も、アプリケーションロジックへの影響を抑えやすいという意味で今回の要件に合っていました。
具体的にはRewriteRule とErrorDocumentの二つを組み合わせて対応しています。
RewriteRuleでアクセスをブロックする
RewriteRuleはApacheのmod_rewriteが提供するディレクティブで、URLのパターンにマッチしたリクエストに対して特定のレスポンスを返すことができます。今回は外部からアクセスされてはいけないパスに対して、
[F]
(Forbidden)フラグを使って403を返す設定を行いました。
RewriteEngine On
RewriteRule ^config/ - [F,L]
RewriteRule ^libs/(dir-a|dir-b|dir-c)/ - [F,L]
RewriteRule ^libs/init\.php$ - [F,L]
各行の意味を分解するとこうなります。
- ^config/ — URLが config/ で始まるパスにマッチ
- – — リダイレクト先なし(URLを書き換えない)
- [F] — 403 Forbiddenを返す
- [L] — このルールが適用されたら以降のルールを処理しない(Last)
[F] フラグを使うと、Apacheが直接403を返してくれるのでPHPの処理が走ることもなく、シンプルに制御できます。
ErrorDocumentでエラーページを表示する
エラーページが用意されている場合はErrorDocumentも合わせて設定しました。これにより、403が返ったときにデフォルトのApacheエラー画面ではなく、サイトのデザインに合わせた独自のエラーページを表示できます。
ErrorDocument 403 /errors/403.html
ErrorDocument 500 /errors/500.html
ErrorDocumentはステータスコードに対してURLを紐づけるだけのシンプルな設定ですが、RewriteRule と組み合わせることで「アクセスをブロックしつつ、わかりやすいエラーページを表示する」という目的を実現できます。
ここが一番大変だった:既存コードへの影響範囲
ここからが本題です。.htaccess にルールを追加するにあたって一番苦労したのが、既存コードへの影響範囲の把握でした。
問題①:正規表現のパターンが正常なパスを巻き込む
RewriteRuleはURLのパターンマッチングで動作するため、記述が少し曖昧だと意図しないパスまでブロックしてしまいます。
たとえば libs/ ディレクトリの一部だけをブロックしたい場合、こう書きたくなります。
RewriteRule ^libs/ - [F,L]
でもこれだと libs/ 以下をすべてブロックしてしまいます。今回の案件では libs/ の中に外部公開してはいけないディレクトリと、通常公開しているディレクトリが混在していたため、単純に libs/ でブロックするわけにはいきませんでした。
そこで以下のように、ブロックしたいサブディレクトリ名を正規表現で明示的に列挙する方法を取りました。
RewriteRule ^libs/(dir-a|dir-b|dir-c)/ - [F,L]
(dir-a|dir-b|dir-c) のように | で区切って指定することで、それ以外の正常なパスを巻き込まずに済みます。一見地味な違いですが、「どこまでブロックして、どこは通すか」を正確に表現するためにディレクトリ構成を細かく確認する作業が必要でした。
問題②:500エラーが出ているファイルをすべて同じパターンで判断してしまった
あるディレクトリ配下のPHPファイルが500エラーを返しているという報告を受けました。他のブロック済みディレクトリと同じパターンに見えたため、同様にブロック対象に追加しました。以下は例です。
RewriteRule ^(.*/)?(config|libs|handler)/.+\.php$ - [F,L]
結果、一部の主要機能が403で弾かれて使えなくなりました。
原因は、500エラーが出ているファイルにも2種類あることを見落としていたことです。
- ブロックしてよいファイル — サーバーサイドのinclude_once専用の断片ファイル。直接HTTPアクセスすること自体が想定外で、単体で呼ばれるとPHPエラーになる
- ブロックしてはいけないファイル — JavaScriptのfetch()から呼ばれるHTTPエンドポイント。直接アクセスされる設計だが、単体起動時に依存ファイルが読めずクラッシュしているだけ
どちらも「直接URLアクセスで500」という症状は同じです。しかし前者は「アクセス自体を禁止する」、後者は「アクセスは許可しつつPHP側を修正する」が正解で、対処がまったく異なります。
ファイルの中身を見れば判断できます。冒頭が以下のような構造であればサーバーサイド断片なのでブロックしてよいです。
<?php
include_once $_SERVER['DOCUMENT_ROOT'] . '/libs/init.php';
$title = 'ページタイトル';
一方、以下のような構造であればHTTPエンドポイントなのでブロックしてはいけません。
<?php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
echo json_encode(['status' => 'ok']);
また、JSファイルを検索して該当パスへのfetchが見つかれば確実にエンドポイントと判断できます。
grep -r "handler/" js/
名前だけでは判断しにくいディレクトリは、ブロック前に必ず以下の2点を確認することが重要です。
- ファイルの冒頭に $_POST / $_GET / header() / 「php://input 判定」があるか
- JSからfetch()やjQuery.ajax()でそのパスを呼んでいるか
どちらかに当てはまればHTTPエンドポイントなのでブロック不可です。慣習的に「断片・データ定義置き場」として使われるディレクトリ名のみをブロック対象とするのが安全です。
問題③:RewriteRuleのルール同士が干渉する
複数のRewriteRule を追加していくうちに、ルール同士が干渉して意図しない挙動になることがありました。
[L]
(Last)フラグは「このルールが適用されたら以降のルールをスキップする」という意味ですが、mod_rewriteはリダイレクトが発生した場合にルールセットを最初から再評価することがあります。そのため、あるルールで処理したつもりが別のルールにも引っかかってしまうケースがあります。
ルールを追加するたびに、既存のルール全体との兼ね合いを確認する必要がありました。ルールの順番や [L] フラグの有無が予想以上に挙動に影響するため、追加する順番にも注意が必要です。
問題④:ErrorDocumentのパスが誤ってブロックされる
ErrorDocumentで指定したエラーページのパスが、RewriteRuleのブロック対象に引っかかってしまうケースも起きました。
たとえば以下のような設定をしたとします。
RewriteRule ^errors/ - [F,L]
ErrorDocument 403 /errors/403.html
errors/ディレクトリをブロックした後に ErrorDocumentでそのディレクトリ内のファイルを指定してしまうと、エラーページ自体にアクセスできなくなります。結果としてApacheのデフォルトエラー画面が表示されてしまい、「エラーページが表示されない」という問題が起きました。
ErrorDocumentで指定するパスがRewriteRuleのブロック対象に含まれていないか、必ず確認が必要です。
影響範囲を把握するためにやったこと
この経験から、.htaccessでルールを追加する前には以下の作業をしっかりやっておくべきだと感じました。
ディレクトリ構成とアクセスパターンを先に整理する
まずプロジェクト全体のディレクトリ構成を確認して、「外部からアクセスされてはいけないパス」と「正常に公開しているパス」を一覧にしました。この一覧がないままRewriteRuleを書くと、正常なパスまでブロックするリスクがあります。
また、PHPのコードがどのパスを内部的に参照しているかも確認しました。インクルードしているファイルのパスや、リダイレクト先のURLが.htaccessのルールと干渉していないかをチェックするためです。
ルールを一行追加するたびに動作確認する
まとめて書いてから確認するのではなく、ルールを一行追加するたびにテスト環境で動作確認するようにしました。一度に大量のルールを追加すると、どのルールが問題を起こしているのか特定が難しくなります。
確認したのは以下の2点です。
- ブロック対象のパスに実際に403が返るか
- 正常に動くべきページや機能が壊れていないか
特に後者は見落としがちです。ページの見た目が正常でも、フォーム送信・データ取得・ログイン処理など、ユーザー操作を伴う機能は一通り確認しておくことをおすすめします。
ErrorDocumentのパスを最後に確認する
ErrorDocument で指定するエラーページのパスが、追加したRewriteRuleと干渉していないかを最後に必ず確認しました。エラーページ自体がブロックされてしまうと、エラーが起きたときに何も表示されなくなってしまいます。
やってみてわかったこと
既存のディレクトリ構成とコードの動きを先に把握する。RewriteRuleはシンプルな記述で広い範囲に影響します。先にどのパスが公開されていてどのパスをPHPが内部で使っているかを整理しておくと、余計なトラブルを避けられます。
「画面が正常に見える」=「機能が正常に動いている」ではない。.htaccessでパスをブロックした後も、ページの見た目は普通に表示されることがあります。でも内部のリクエストや処理が静かに失敗していることがあります。表面的な確認だけでなく、機能レベルで一通り動作確認することが大切です。
ルールの追加は一行ずつ、確認しながら進める。一気に書いてまとめて確認するより、一行追加して確認する、を繰り返す方が問題の切り分けが楽です。
おわりに
「.htaccess に数行書くだけ」と思っていた改修が、既存コードへの影響範囲の確認という地道な作業を伴う案件になりました。エラーハンドリングは地味に見えて、実はサービスの信頼性・セキュリティ・運用のしやすさに深く関わる重要な部分だと改めて実感しています。
もし同じような改修を検討している方がいれば、まずは「外部公開しているパスと内部で使っているパスを整理する」ところから始めることをおすすめします。全体像を把握してから実装に入ると、後から慌てることが減るはずです。
