PHPでWebサイトやWebアプリケーションを開発する場合、セキュリティ対策は欠かせません。
PHPそのものに問題があるというより、ユーザー入力の扱い方やデータベースへの接続方法、セッション管理、ファイルアップロード、サーバー設定などに不備があると、脆弱性につながる可能性があります。
特に、SQLインジェクションやクロスサイトスクリプティング(XSS)、CSRF、認証・認可の不備、ファイルアップロード機能の悪用などは、PHPに限らずWebアプリケーション全般で注意が必要な問題です。
安全なPHP開発を行うためには、個別の攻撃手法への対策だけでなく、「外部から受け取った値を信用しない」「必要最小限の権限だけを与える」「PHPやライブラリを継続的に更新する」といった基本原則を押さえておくことが重要です。
SQLインジェクションを防ぐ
ユーザー入力をSQL文へ直接連結しない
SQLインジェクションとは、フォームやURLなどから入力された値を利用して、本来意図していないSQL文を実行させる攻撃です。
例えば、次のようにユーザー入力を直接SQL文へ連結する実装は避ける必要があります。
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = " . $id;
入力値によってSQL文の構造を変更できてしまうと、データの取得や改ざん、削除などにつながる可能性があります。
プリペアドステートメントを利用する
SQLインジェクション対策では、PDOなどのプリペアドステートメントを利用するのが基本です。
$stmt = $pdo->prepare(
'SELECT * FROM users WHERE id = :id'
);
$stmt->execute([
'id' => $id
]);
SQL文と入力値を分離することで、ユーザーが入力した文字列をSQLコードとして解釈されにくくできます。
ただし、テーブル名やカラム名、ORDER BYに指定する項目など、SQLの構造部分は通常のプレースホルダでは置き換えられません。
こうした値を外部から受け取る場合は、許可する値をあらかじめ限定する方法が有効です。
$allowedSorts = ['name', 'created_at'];
$sort = $_GET['sort'] ?? 'created_at';
if (!in_array($sort, $allowedSorts, true)) {
$sort = 'created_at';
}
$sql = "SELECT * FROM users ORDER BY {$sort}";
クロスサイトスクリプティング(XSS)を防ぐ
外部入力をそのままHTMLへ出力しない
XSSとは、悪意のあるHTMLやJavaScriptをWebページ上で実行させる攻撃です。
例えば、ユーザーから受け取った値をそのまま出力すると危険です。
echo $_GET['name'];
攻撃者がJavaScriptを含む文字列を送信すると、ブラウザ上で実行される可能性があります。
出力先に応じて適切にエスケープする
HTML本文へ文字列を出力する場合は、htmlspecialchars()を利用する方法が一般的です。
echo htmlspecialchars(
$_GET['name'],
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
ただし、XSS対策は単純にすべての値へhtmlspecialchars()を適用すれば完了するわけではありません。
HTML本文、HTML属性、JavaScript、URLなど、出力する場所によって適切な処理が異なります。
そのため、「入力時に一括して無害化する」のではなく、値を出力するタイミングで、その出力先に適したエスケープを行うことが重要です。
CSRF対策を行う
CSRFとは
CSRFは、ログイン中のユーザーに対して、本人が意図していない操作を実行させる攻撃です。
例えば、ユーザーがログインした状態で攻撃者のWebページを開いた結果、意図せずメールアドレス変更や商品購入、データ削除などのリクエストが送信される可能性があります。
CSRFトークンを使用する
重要なPOST処理では、CSRFトークンを発行して検証する方法が一般的です。
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
フォーム内にトークンを埋め込みます。
<input
type="hidden"
name="csrf_token"
value="<?= htmlspecialchars(
$_SESSION['csrf_token'],
ENT_QUOTES,
'UTF-8'
) ?>"
>
リクエストを受け取った際には、サーバー側でトークンを比較します。
if (
!isset($_POST['csrf_token']) ||
!hash_equals(
$_SESSION['csrf_token'],
$_POST['csrf_token']
)
) {
exit('Invalid request');
}
LaravelやSymfonyなどのフレームワークを利用している場合は、独自に実装するよりも、フレームワーク標準のCSRF対策を利用する方が安全です。
SameSite属性だけに依存しない
CookieのSameSite属性もCSRF対策を補助する機能ですが、これだけですべてのCSRFを防げると考えるのは適切ではありません。
重要な状態変更処理では、CSRFトークンなど複数の対策を組み合わせることが重要です。
パスワードを安全に保存する
パスワードを平文で保存しない
ユーザーのパスワードを、そのままデータベースへ保存してはいけません。
データベースが漏えいした場合、すべてのパスワードが第三者に知られてしまう可能性があります。
また、md5()やsha1()をパスワード保存目的で直接利用する方法も避けるべきです。
password_hash()を利用する
PHPでは、パスワード保存専用のpassword_hash()が用意されています。
$hash = password_hash(
$password,
PASSWORD_DEFAULT
);
ログイン時にはpassword_verify()を利用して確認します。
if (password_verify($password, $hash)) {
// ログイン成功
}
独自の暗号化処理を作成するのではなく、PHP標準のパスワードAPIを利用することが重要です。
ハッシュを保存するカラムには十分な長さを確保する
PASSWORD_DEFAULTで使用されるアルゴリズムは、将来的に変更される可能性があります。
そのため、データベースにハッシュを保存する場合は、例えばVARCHAR(255)など十分な長さを確保しておくと安全です。
password_needs_rehash()も活用する
アルゴリズムやコスト設定を変更した場合は、password_needs_rehash()を利用すると、古いハッシュを検出できます。
if (password_verify($password, $hash)) {
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash(
$password,
PASSWORD_DEFAULT
);
// 新しいハッシュをデータベースへ保存
}
}
ログイン時に少しずつ最新のハッシュへ移行できるため、長期間運用するサービスでは有効です。
セッションを安全に管理する
ログイン後にセッションIDを再生成する
ログイン機能では、セッションIDを盗まれるとユーザーになりすまされる可能性があります。
そのため、ログイン成功後など、ユーザーの権限状態が変化するタイミングではセッションIDを再生成します。
session_regenerate_id(true);
これにより、セッション固定攻撃のリスクを軽減できます。
セッションCookieを適切に設定する
セッションCookieでは、次の属性が重要です。
- Secure
- HttpOnly
- SameSite
例えば、php.iniでは次のような設定を検討できます。
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1
Secureを有効にする場合はHTTPS環境が前提です。
また、SameSiteの適切な値は、外部サービスとの認証連携などシステム構成によって異なります。
単純にすべてStrictへ設定すればよいわけではありません。
外部から受け取る値を信用しない
GETやPOSTの値は必ず検証する
PHPでは、次のような値を外部から受け取ることがあります。
$_GET
$_POST
$_COOKIE
$_FILES
これらの値はユーザー側から自由に変更できると考えるべきです。
JavaScriptで入力チェックを行っていたとしても、HTTPリクエストを直接送信すれば簡単に回避できます。
そのため、必ずサーバー側でも検証を行います。
$_SERVERにも操作可能な値が含まれる
$_SERVERのすべてがユーザー入力というわけではありませんが、HTTPリクエスト由来の値にはクライアント側から変更可能なものがあります。
例えば、次のような値です。
$_SERVER['HTTP_HOST']
$_SERVER['HTTP_USER_AGENT']
$_SERVER['HTTP_REFERER']
そのため、$_SERVERについても値の出所を確認し、安全であると決めつけないことが重要です。
filter_input()などを利用する
整数だけを受け付けたい場合は、次のように検証できます。
$id = filter_input(
INPUT_GET,
'id',
FILTER_VALIDATE_INT
);
if ($id === false || $id === null) {
exit('Invalid ID');
}
入力検証では、その値がアプリケーションの仕様として正しいかどうかを判断します。
一方、出力エスケープは、その値がHTMLやJavaScriptなどでコードとして解釈されないようにする処理です。
入力検証と出力エスケープは目的が異なるため、両方を適切に行う必要があります。
ファイルアップロードを厳格に管理する
拡張子だけで判断しない
ファイルアップロード機能は、重大な脆弱性につながりやすい機能の一つです。
例えば、ファイル名がimage.jpgだったとしても、本当にJPEG画像であるとは限りません。
そのため、拡張子だけでファイルの安全性を判断してはいけません。
MIMEタイプだけでも判断しない
ブラウザから送信されるMIMEタイプやContent-Typeは偽装できる可能性があります。
そのため、MIMEタイプだけをセキュリティ判断の根拠にすることも避ける必要があります。
ファイルアップロードでは、次のような対策を組み合わせます。
- 許可する拡張子を限定する
- ファイルサイズを制限する
- MIMEタイプやファイル内容を確認する
- ファイル名をランダムな値へ変更する
- 可能であればWeb公開領域外へ保存する
- アップロード先でPHPなどのスクリプトを実行できないようにする
- 必要に応じてウイルスやマルウェア検査を行う
特に、ユーザーがアップロードしたPHPファイルをWebサーバーから実行できる構成は避ける必要があります。
ディレクトリトラバーサルを防ぐ
ファイルパスを直接受け取らない
次のように、ユーザー入力をそのままファイルパスへ連結する実装は危険です。
$file = $_GET['file'];
readfile('/var/www/files/' . $file);
攻撃者が../などを利用すると、本来アクセスできないファイルへ到達する可能性があります。
可能であれば、ユーザーから実際のファイルパスを受け取らない設計にします。
例えば、次のようにIDとファイルをサーバー側で対応付ける方法があります。
1 → report.pdf
2 → manual.pdf
3 → guide.pdf
ユーザーはIDだけを指定し、実際のファイルパスはサーバー側で決定する方が安全です。
OSコマンドインジェクションを防ぐ
ユーザー入力をシェルコマンドへ直接渡さない
PHPには、OSコマンドを実行するための関数があります。
exec()
system()
shell_exec()
passthru()
例えば、次のようにユーザー入力をそのまま連結すると危険です。
system('ping ' . $_GET['host']);
入力内容によって、意図していないコマンドを実行される可能性があります。
最も安全なのは、可能な限りシェルコマンドを呼び出さない設計にすることです。
どうしても必要な場合は、入力値を許可リスト方式で厳格に制限し、用途に応じたエスケープ処理を組み合わせます。
eval()の使用を避ける
外部入力をPHPコードとして実行しない
eval()は、渡された文字列をPHPコードとして評価・実行する関数です。
次のようなコードは非常に危険です。
eval($_POST['code']);
外部入力をそのままeval()へ渡すと、任意のPHPコードを実行される可能性があります。
eval()を使用する必要があると感じた場合は、別の設計へ置き換えられないか検討することが重要です。
本番環境では詳細なエラーを表示しない
display_errorsを無効にする
開発環境ではエラー表示が便利ですが、本番環境で詳細なエラーを表示すると内部情報が漏れる可能性があります。
例えば、次のような情報が表示される可能性があります。
- サーバー上のファイルパス
- SQL文
- 内部クラス名
- ライブラリ構成
- スタックトレース
- データベース関連情報
本番環境では、一般的に次のような設定を行います。
display_errors = Off
log_errors = On
ただし、単にエラーを非表示にするだけでは十分ではありません。
ログを適切に保存し、異常なアクセスやエラーを監視できるようにすることも重要です。
PHPやライブラリを定期的に更新する
サポート終了したPHPを使い続けない
PHPにはバージョンごとにサポート期間があります。
サポートが終了したバージョンでは、新しい脆弱性が見つかってもセキュリティ修正を受けられません。
2026年8月時点では、PHP 8.2はセキュリティサポート期間にあり、2026年12月31日にサポート終了予定です。
そのため、新規開発ではフレームワークやライブラリとの互換性を確認しながら、より新しいサポート対象バージョンを検討することが望ましいでしょう。
Composerパッケージの脆弱性も確認する
PHP本体を更新していても、Composerで利用している依存パッケージに脆弱性が残っている場合があります。
既知のセキュリティ問題を確認するには、次のコマンドを利用できます。
composer audit
CI/CDに組み込み、継続的に依存パッケージの状態を確認する方法も有効です。
本番環境ではcomposer installを基本にする
本番環境で安易にcomposer updateを実行すると、依存パッケージのバージョンが変化し、予期しない不具合が発生する可能性があります。
通常は開発環境などで依存関係を更新してテストし、composer.lockを確定させたうえで、本番環境では次のようにインストールします。
composer install
これにより、検証済みの依存関係を本番環境でも再現しやすくなります。
Webサイト全体をHTTPS化する
HTTP通信を避ける
HTTPSは、ログインページや個人情報入力ページだけではなく、Webサイト・Webアプリケーション全体へ適用するのが基本です。
HTTP通信では、通信内容を盗聴・改ざんされるリスクがあります。
特に次のような情報を扱う場合は、安全な通信が欠かせません。
- パスワード
- Cookie
- 個人情報
- フォーム入力
- 決済関連情報
HTTPからHTTPSへリダイレクトし、必要に応じてHSTSも設定すると、通信の安全性を高められます。
ファイルやプロセスの権限を最小限にする
PHP-FPMなどを過剰な権限で動かさない
WebサーバーやPHP-FPMへ必要以上の権限を与えると、アプリケーションが侵害された場合の被害が大きくなります。
可能な限り、必要最低限のユーザー権限で動作させます。
また、書き込み権限も必要なディレクトリだけに限定します。
chmod 777を安易に使用しない
権限エラーが発生した際に、次のような設定で解決しようとするのは避けるべきです。
chmod -R 777 .
所有ユーザー、グループ、Webサーバー、PHP-FPMの実行ユーザーを確認し、必要な権限だけを付与します。
これは「最小権限の原則」と呼ばれる、セキュリティの基本的な考え方です。
機密情報をソースコードに直接書かない
データベースパスワードやAPIキーを分離する
次のように、機密情報をソースコードへ直接記述することは避けます。
$dbPassword = 'my-secret-password';
ソースコードをGitなどへ登録した場合、意図せず外部へ公開される可能性があります。
一般的には、次のような方法で管理します。
- 環境変数
.env- クラウドなどのシークレット管理サービス
.env自体も適切に保護する
.envを利用していても、それだけで安全になるわけではありません。
.envは平文ファイルであるため、次の点に注意します。
- Gitへコミットしない
- Webブラウザからアクセスできない場所へ置く
- ファイル権限を適切に設定する
- バックアップファイルを公開しない
本番環境では、可能であれば専用のシークレット管理サービスを利用する方法も検討できます。
認証と認可を分けて考える
認証とは
認証とは、アクセスしているユーザーが誰なのかを確認する処理です。
例えば、IDとパスワードによるログインが該当します。
認可とは
認可とは、そのユーザーがどの操作を実行してよいのかを判断する処理です。
例えば、ログイン済みであっても一般ユーザーが管理者専用機能を利用できないようにする必要があります。
if ($_SESSION['role'] !== 'admin') {
http_response_code(403);
exit('Forbidden');
}
管理画面のボタンを非表示にするだけでは、セキュリティ対策として不十分です。
URLやAPIへ直接アクセスされても操作できないように、必ずサーバー側で認可チェックを行います。
重要な操作には追加の保護を行う
多要素認証や再認証を検討する
次のような重要な操作では、通常のログイン状態だけに依存しない方が安全です。
- パスワード変更
- メールアドレス変更
- 決済
- 退会
- 管理者権限の付与
- APIキーの発行
必要に応じて、次の対策を組み合わせます。
- パスワードの再入力
- 多要素認証
- CSRF対策
- 操作ログ
- 確認画面
- レート制限
複数のセキュリティ対策を組み合わせる考え方は「多層防御」と呼ばれます。
HTTPセキュリティヘッダーも活用する
Content-Security-Policyを検討する
CSP(Content-Security-Policy)は、ブラウザが読み込めるJavaScriptやCSSなどの出所を制限する仕組みです。
適切に設定すると、XSSなどによる被害を軽減できる場合があります。
ただし、CSPはXSS対策そのものを置き換えるものではありません。
出力エスケープなどの基本対策を行ったうえで、追加の防御として利用します。
その他のHTTPヘッダーも設定する
Webアプリケーションでは、次のようなHTTPセキュリティヘッダーも検討できます。
X-Content-Type-OptionsReferrer-PolicyContent-Security-Policy- クリックジャッキング対策に関する設定
利用するヘッダーや設定値は、Webサイトの構成や外部サービスとの連携状況に応じて決定します。
ログイン機能への攻撃にも備える
ログイン試行回数を制限する
ログインフォームを無制限に試行できる状態では、総当たり攻撃やパスワードリスト攻撃を受ける可能性があります。
そのため、IPアドレスやアカウント単位などでレート制限を実施する方法があります。
ただし、単純なアカウントロックだけでは正規ユーザーへの妨害に悪用される可能性もあるため、設計には注意が必要です。
多要素認証も有効
管理者アカウントや重要な情報を扱うサービスでは、多要素認証を導入するとアカウント乗っ取りへの耐性を高められます。
パスワードだけに依存しない認証設計を検討することも重要です。
PHPのセキュリティ対策は多層的に行う
PHPのセキュリティ対策では、特定の関数や設定を一つ導入すれば安全になるわけではありません。
次のような対策を組み合わせることが重要です。
- 外部入力を信用しない
- SQLにはプリペアドステートメントを利用する
- 出力先に応じて適切にエスケープする
- CSRF対策を実施する
- パスワードは
password_hash()で保存する - セッションを適切に管理する
- ファイルアップロードを厳格に制限する
- 認証と認可を分けて実装する
- PHPや依存ライブラリを定期的に更新する
- HTTPSを利用する
- 最小権限の原則を守る
- 機密情報をソースコードから分離する
- セキュリティログを記録・監視する
- 必要に応じてCSPなどのHTTPセキュリティヘッダーを利用する
セキュリティ対策は、問題が発生してから追加するのではなく、設計・開発・テスト・運用の各段階で継続的に行うことが大切です。
PHPの機能だけで対策を完結させるのではなく、Webサーバー、データベース、OS、フレームワーク、Composerパッケージなども含めて、システム全体で安全性を確保することが重要です。
以上、PHPのセキュリティ対策や注意点についてでした。
最後までお読みいただき、ありがとうございました。










