PHPで作ったWebサイトやWebアプリを公開すると、その日から世界中のボットがアクセスしてきます。管理画面のログインページを総当たりで試すもの、古いバージョンの弱点を探すもの、うっかり公開された設定ファイルを探すものなど、相手は「人」というより「自動化されたプログラム」です。狙われるのは有名な大企業だけではありません。

PHPのセキュリティというと「SQLインジェクション対策をすればいい」と考えられがちですが、実際には守るべき場所がいくつもの層に分かれています。この記事では、セキュリティを次の4つの層に分けて、それぞれ何をすればよいかを順番に解説します。

層守るもの主な対策の例
1. サーバーOS・Webサーバー・データベース・ネットワークアップデート、SSHの鍵認証、ファイアウォール、HTTPS
2. PHP本体PHPのバージョンと設定(php.ini)サポート中のバージョン、エラー非表示、セッション設定
3. フレームワークCodeIgniter や Laravel などの設定と機能本番モード、CSRF保護、エスケープ、バリデーション
4. ソースコード自分たちが書いたプログラムSQLインジェクション・XSS対策、認可チェック、パスワード管理

大切なのは、どれか一つの層だけで守ろうとしないことです。コードに穴があってもサーバー側の設定で被害が広がらない、サーバー設定に漏れがあってもコード側で防げる。このように何重にも守る考え方を「多層防御」と呼びます。

1. サーバーレベルのセキュリティ

まずは土台となるサーバーです。どれだけ丁寧にコードを書いても、サーバーそのものに入られてしまえば意味がありません。

OS とミドルウェアを最新に保つ

OS(Linux など)、Webサーバー(Apache / Nginx)、データベース(MySQL / MariaDB / PostgreSQL)には、定期的にセキュリティ修正が出ています。公開済みの脆弱性は攻撃方法も広く知られているため、アップデートを放置することが最も多い侵入原因の一つです。

  • セキュリティアップデートは定期的に適用する(自動適用できる仕組みもあります)
  • サポートが終了した OS やソフトウェアは使い続けない
  • 使っていないソフトウェアやサービスはアンインストール・停止する

SSH ログインを強化する

サーバーに管理者として入る入口が SSH です。ここが突破されるとサーバー全体を乗っ取られます。

  • パスワード認証を無効にし、鍵認証だけにする
  • root ユーザーで直接ログインできないようにする
  • ログイン失敗が続く IP アドレスを自動で遮断する(fail2ban など)
# /etc/ssh/sshd_config の例
PasswordAuthentication no
PermitRootLogin no

ファイアウォールで「開ける穴」を最小にする

外部に公開する必要があるのは、基本的に Web の 80番(HTTP)と 443番(HTTPS)、そして管理用の SSH だけです。特にデータベースのポート(MySQL なら 3306番)をインターネットに公開してはいけません。SSH も、可能であれば接続元 IP を限定しましょう。

HTTPS を必ず使う

HTTP のままだと、ログイン時のパスワードやセッション情報が通信の途中で盗み見られる可能性があります。今は Let's Encrypt などで無料の証明書が使えるので、サイト全体を HTTPS にして、HTTP へのアクセスは HTTPS へリダイレクトしましょう。HSTS ヘッダーを付けると、ブラウザが常に HTTPS で接続するようになります。

ドキュメントルートは「公開フォルダ」だけにする

見落とされがちですが、とても重要なポイントです。Web サーバーが外部に公開するフォルダ(ドキュメントルート)を、プロジェクト全体ではなく public フォルダだけに向けます。

/var/www/myapp/
├── app/        ← 公開しない(プログラム本体)
├── vendor/     ← 公開しない(ライブラリ)
├── writable/   ← 公開しない(ログ・キャッシュ)
├── .env        ← 絶対に公開しない(パスワードなど)
└── public/     ← ここだけをドキュメントルートにする
    └── index.php

これを間違えると、https://example.com/.env にアクセスするだけでデータベースのパスワードが丸見えになる、という事故が起きます。念のため、Web サーバー側でも .env や .git などの隠しファイルへのアクセスを拒否しておきましょう。

# Nginx の例:ドットで始まるファイルへのアクセスを拒否
location ~ /\. {
    deny all;
}

Web サーバーの余計な情報を隠す

  • エラーページやレスポンスヘッダーにサーバーのバージョンを出さない(Nginx の server_tokens off;、Apache の ServerTokens Prod など)
  • ディレクトリの中身が一覧表示される機能(ディレクトリリスティング)を無効にする

バージョン情報は、攻撃者にとって「どの脆弱性を試せばいいか」のヒントになります。

ファイルの権限を最小にする

PHP を実行するユーザーが書き込めるのは、ログやキャッシュ、アップロード先など本当に必要なフォルダだけにします。プログラムのファイルまで書き込み可能だと、一度侵入されたときにコードを書き換えられてしまいます。「とりあえず 777」は厳禁です。目安は、ファイルが 644、フォルダが 755 です。

データベースユーザーの権限を絞る

アプリが使う DB ユーザーには、そのアプリのデータベースに対する必要な権限だけを与えます。root ユーザーをアプリから使うのはやめましょう。万が一 SQL インジェクションがあっても、被害の範囲を小さくできます。

バックアップとログ監視

どれだけ対策しても、100% 安全にはなりません。だからこそ「やられたときに戻せる」「やられたことに気づける」仕組みが必要です。

  • データベースとアップロードファイルを定期的にバックアップし、サーバーとは別の場所に保存する
  • バックアップから実際に復元できるか、一度は試しておく
  • アクセスログやエラーログを確認し、不審なアクセスに気づけるようにする
  • 必要に応じて WAF(Web Application Firewall)を導入する

2. PHP 本体のセキュリティ

次は PHP そのものです。ここでは「バージョン」と「設定(php.ini)」の 2 点を押さえます。

サポート中のバージョンを使う

PHP の各バージョンには、セキュリティ修正が提供される期間が決まっています。期間が終わったバージョンでは、新しい脆弱性が見つかっても修正されません。現在どのバージョンがサポートされているかは、PHP 公式サイトの Supported Versions で確認できます。

レンタルサーバーでは、古い PHP が初期設定のまま残っていることがよくあります。管理画面で使用中のバージョンを一度確認してみてください。

php.ini の本番向け設定

PHP には、開発向けの php.ini-development と本番向けの php.ini-production という 2 種類の設定ファイルの見本が用意されています。本番サーバーでは production 側をベースにした上で、次の項目を確認しましょう。

; エラー内容を画面に出さない(ログには残す)
display_errors = Off
log_errors = On

; レスポンスヘッダーに PHP のバージョンを出さない
expose_php = Off

; 外部 URL のファイルを include できないようにする
allow_url_include = Off

; セッション Cookie の保護
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1
session.use_only_cookies = 1

それぞれの意味を簡単に説明します。

  • display_errors = Off:エラーが起きたとき、ファイルのパスや SQL 文などの内部情報が画面に表示されるのを防ぎます。エラーはログで確認します。
  • expose_php = Off:X-Powered-By: PHP/8.x.x のようなヘッダーを出さないようにします。
  • allow_url_include = Off:include で外部サイトのファイルを読み込めないようにし、リモートファイルインクルード攻撃を防ぎます。
  • session.cookie_httponly:JavaScript からセッション Cookie を読めなくし、XSS があった場合でも Cookie が盗まれにくくなります。
  • session.cookie_secure:HTTPS 通信のときだけ Cookie を送ります。
  • session.cookie_samesite:他サイトからのリクエストに Cookie を付けにくくし、CSRF 対策を補強します。
  • session.use_strict_mode:サーバーが発行していないセッション ID を受け付けないようにし、セッション固定攻撃を防ぎます。

危険な関数や操作範囲を制限する

アプリで使わないのであれば、外部コマンドを実行する関数を disable_functions で無効にしておくと、万が一コードに穴があったときの被害を減らせます。また open_basedir を設定すると、PHP がアクセスできるディレクトリを限定できます。

disable_functions = exec,passthru,shell_exec,system,proc_open,popen
open_basedir = /var/www/myapp:/tmp

ただし、使っているライブラリがこれらの関数を必要とする場合もあるため、設定後は動作確認をしてください。

Composer で入れたライブラリも「PHP の一部」

現代の PHP アプリは、Composer で多くの外部ライブラリを読み込んでいます。ライブラリに脆弱性があれば、自分のコードが完璧でも攻撃されます。Composer には、既知の脆弱性を持つパッケージを調べるコマンドがあります。

composer audit

定期的に実行し、問題が見つかったら composer update で修正版に更新しましょう。composer.lock をリポジトリに含めておくと、本番環境と開発環境で同じバージョンを使えます。

3. フレームワークのセキュリティ

CodeIgniter や Laravel、Symfony などのフレームワークには、よくある攻撃への対策があらかじめ組み込まれています。言い換えると、フレームワークのセキュリティは「機能を正しく有効にして、正しく使う」ことがほぼすべてです。ここでは CodeIgniter 4 と Laravel を例に説明します。

フレームワーク自体を最新に保つ

フレームワークにも脆弱性は見つかります。セキュリティ修正を含むリリースが出たら、早めにアップデートしましょう。公式サイトや GitHub のリリースノート、セキュリティアドバイザリを定期的に確認する習慣をつけると安心です。

本番環境では必ず「本番モード」にする

開発モードのままだと、エラー発生時にソースコードや環境変数を含む詳しいデバッグ画面が表示されてしまいます。攻撃者にとっては宝の山です。

# CodeIgniter 4(.env)
CI_ENVIRONMENT = production

# Laravel(.env)
APP_ENV=production
APP_DEBUG=false

.env(秘密情報)を正しく管理する

データベースのパスワード、API キー、暗号化キーなどは .env に書き、Git にはコミットしません。.gitignore に .env が含まれているか確認しましょう。もし誤ってコミット・公開してしまった場合は、履歴から消すだけでなく、漏れたパスワードやキーそのものを変更する必要があります。

CSRF 保護を有効にする

CSRF(クロスサイト・リクエスト・フォージェリ)は、ログイン中のユーザーに、別サイトから勝手に「退会」「送金」「設定変更」などのリクエストを送らせる攻撃です。フレームワークのフォームに埋め込まれるトークンで防げます。

// CodeIgniter 4:app/Config/Filters.php で CSRF フィルターを有効化
public array $globals = [
    'before' => [
        'csrf',
    ],
];

// ビューのフォーム内
<?= csrf_field() ?>
{{-- Laravel(Blade):web ルートでは標準で有効。フォーム内に追加 --}}
<form method="POST" action="/profile">
    @csrf
    ...
</form>

Webhook のように外部サービスから POST を受ける URL だけは CSRF の対象外にすることがありますが、その場合は署名の検証など、別の方法で正当なリクエストかどうかを必ず確認してください。

出力は必ずエスケープする

ユーザーが入力した値を HTML にそのまま出力すると、XSS(クロスサイト・スクリプティング)の原因になります。フレームワークにはエスケープの仕組みが用意されています。

<!-- CodeIgniter 4 -->
<p><?= esc($user['name']) ?></p>

{{-- Laravel:{{ }} は自動でエスケープされる --}}
<p>{{ $user->name }}</p>

{{-- {!! !!} はエスケープされないので、信頼できる値以外には使わない --}}

ブログ本文のように HTML をそのまま表示したい場合は、管理者だけが入力できるようにする、HTML Purifier などで許可するタグを限定する、といった対策を組み合わせます。

データベース操作はクエリビルダーや ORM を使う

CodeIgniter のクエリビルダーや Laravel の Eloquent を使えば、値は自動的に安全に扱われます。注意が必要なのは、生の SQL を書ける機能を使う場面です。

// 安全:クエリビルダーが値をエスケープする
$user = $db->table('users')->where('email', $email)->get()->getRow();

// 危険:文字列連結で SQL を組み立てている
$db->query("SELECT * FROM users WHERE email = '" . $email . "'");

// 生の SQL を書くならバインドを使う
$db->query('SELECT * FROM users WHERE email = ?', [$email]);

一括代入(マスアサインメント)を制限する

フォームの値をまとめて保存する機能は便利ですが、攻撃者がフォームに存在しない role=admin のような項目を勝手に送ってくる可能性があります。保存してよい項目をホワイトリストで指定しましょう。

// CodeIgniter 4 のモデル
protected $allowedFields = ['name', 'email', 'profile'];

// Laravel のモデル
protected $fillable = ['name', 'email', 'profile'];

入力値はバリデーションで検証する

メールアドレスの形式、文字数、数値の範囲、選択肢の中から選ばれているかなど、受け取る値は必ず検証します。フレームワークのバリデーション機能を使えば、短いコードで書けます。JavaScript による画面側のチェックは簡単に回避できるため、サーバー側での検証が必須です。

セキュリティヘッダーを付ける

レスポンスに次のようなヘッダーを付けると、ブラウザ側の防御機能が働きます。

  • Content-Security-Policy:読み込んでよいスクリプトの出どころを制限し、XSS の被害を抑える
  • X-Frame-Options / frame-ancestors:他サイトに iframe で埋め込まれるのを防ぐ(クリックジャッキング対策)
  • X-Content-Type-Options: nosniff:ファイルの種類をブラウザが勝手に推測しないようにする
  • Strict-Transport-Security:常に HTTPS で接続させる

CodeIgniter 4 では secureheaders フィルターや app/Config/ContentSecurityPolicy.php、Laravel ではミドルウェアやパッケージで設定できます。

認証は「自作」より公式の仕組みを使う

ログイン、パスワードリセット、ログイン試行回数の制限などは、細かい落とし穴が多い機能です。CodeIgniter なら Shield、Laravel なら Breeze や Fortify など、公式・定番の認証パッケージを使う方が安全です。

4. ソースコードのセキュリティ

最後は、自分たちが書くコードです。フレームワークを使っていても、使い方次第で穴は生まれます。代表的な脆弱性と対策を、悪い例・良い例で見ていきます。

SQL インジェクション:プリペアドステートメントを使う

入力値を SQL 文に直接つなげると、攻撃者が SQL を書き換えて、データの盗み出しや削除ができてしまいます。

// ✕ 悪い例:id に「1 OR 1=1」と入れられると全件取得される
$sql = "SELECT * FROM orders WHERE id = " . $_GET['id'];

// ○ 良い例:PDO のプリペアドステートメント
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = :id');
$stmt->execute(['id' => $_GET['id']]);
$order = $stmt->fetch();

並び替えの列名など、プレースホルダーが使えない部分は、許可する値のリスト(ホワイトリスト)と照合してから使います。

$allowed = ['created_at', 'price', 'name'];
$sort = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'created_at';

XSS:出力するときにエスケープする

// ✕ 悪い例:名前に <script> を入れられると実行されてしまう
echo '<p>こんにちは、' . $_GET['name'] . 'さん</p>';

// ○ 良い例
echo '<p>こんにちは、' . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . 'さん</p>';

ポイントは「入力時」ではなく「出力時」にエスケープすることです。同じ値でも、HTML に出すのか、JavaScript に埋め込むのか、URL に使うのかで必要な処理が変わるためです。JavaScript に値を渡すときは json_encode()、URL のパラメータには urlencode() を使います。

パスワードは password_hash() で保存する

パスワードを平文で保存したり、MD5 や SHA-1 でハッシュ化したりするのは危険です。PHP 標準の関数を使えば、安全なハッシュ化とソルトの付与を自動でやってくれます。

// 登録時
$hash = password_hash($password, PASSWORD_DEFAULT);

// ログイン時
if (password_verify($inputPassword, $user['password_hash'])) {
    // ログイン成功
}

ログイン後はセッション ID を作り直す

ログイン前と同じセッション ID を使い続けると、攻撃者が事前に用意したセッション ID を使わせる「セッション固定攻撃」が成立することがあります。ログイン成功時には ID を作り直しましょう(フレームワークの認証機能は通常これを行っています)。

session_regenerate_id(true);

認可チェック:「ログインしているか」だけでなく「その人のデータか」を確認する

非常に多い脆弱性です。たとえば /orders/123 を表示する画面で、ログインしているかどうかしか確認していないと、URL の数字を 124 に変えるだけで他人の注文が見えてしまいます。

// ✕ 悪い例:誰の注文でも表示できてしまう
$order = $orderModel->find($id);

// ○ 良い例:ログイン中のユーザーの注文に限定する
$order = $orderModel
    ->where('id', $id)
    ->where('user_id', $currentUserId)
    ->first();

if (! $order) {
    // 404 を返す
}

管理画面も同様です。画面にリンクを出していないだけでは不十分で、処理を実行する側で毎回権限を確認します。

ファイルアップロード:中身を確認し、実行させない

画像アップロードのつもりが、PHP ファイルをアップロードされて実行されると、サーバーを乗っ取られます。

  • 拡張子だけでなく、finfo などでファイルの中身(MIME タイプ)を確認する
  • 許可する種類をホワイトリストで決める(jpg / png / pdf など)
  • 保存するファイル名はランダムに生成し、ユーザーが指定した名前をそのまま使わない
  • 保存先は公開フォルダの外にするか、そのフォルダで PHP が実行されないように設定する
  • ファイルサイズの上限を決める
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime  = $finfo->file($_FILES['image']['tmp_name']);
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];

if (! isset($allowed[$mime])) {
    exit('許可されていないファイル形式です');
}

$filename = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
move_uploaded_file($_FILES['image']['tmp_name'], '/var/www/myapp/writable/uploads/' . $filename);

パストラバーサル:ユーザー入力でファイルパスを作らない

?file=../../.env のように指定されると、意図しないファイルを読み出されます。ファイル名を受け取る必要がある場合は、basename() でフォルダ部分を取り除き、さらに許可リストと照合するのが安全です。できれば、ファイル名ではなく ID を受け取り、データベースから実際のパスを引く設計にしましょう。

OS コマンドインジェクション:外部コマンドはなるべく使わない

exec() や system() にユーザー入力を渡すと、任意のコマンドを実行される危険があります。まずは PHP の関数やライブラリで代替できないか検討し、どうしても必要な場合は escapeshellarg() で引数をエスケープします。

unserialize() に外部からの値を渡さない

Cookie やリクエストの値を unserialize() すると、オブジェクトの仕組みを悪用した攻撃につながることがあります。外部とデータをやり取りするなら json_encode() / json_decode() を使いましょう。

比較は === を使い、トークンの比較は hash_equals()

PHP の == は型を自動変換して比較するため、思わぬ値が「等しい」と判定されることがあります。セキュリティに関わる比較では厳密比較の === を使い、in_array() も第3引数に true を指定します。API トークンや署名の比較には、比較にかかる時間から値を推測されにくい hash_equals() を使います。

if (hash_equals($expectedSignature, $receivedSignature)) {
    // 署名が正しい
}

トークンや乱数は random_bytes() で作る

パスワードリセット用の URL や招待コードなどに rand() や uniqid() を使うと、値を推測される可能性があります。暗号論的に安全な random_bytes() や random_int() を使いましょう。

$token = bin2hex(random_bytes(32));

オープンリダイレクト:戻り先 URL をそのまま使わない

ログイン後に ?redirect=... で指定されたページへ移動する機能は、外部の偽サイトへ誘導するのに悪用されることがあります。リダイレクト先は自サイト内のパスに限定しましょう。

エラーメッセージで情報を与えない

ログイン失敗時に「このメールアドレスは登録されていません」と表示すると、登録済みのメールアドレスを調べられてしまいます。「メールアドレスまたはパスワードが正しくありません」のように、どちらが間違っているか分からない表示にするのが一般的です。

AI が書いたコードこそレビューする

AI を使った開発(バイブコーディング)は非常に速く、便利です。一方で、AI が生成したコードには、ここまでに紹介したような穴が含まれていることもあります。動いたからといってそのまま公開せず、少なくとも次の点は人の目で確認しましょう。

  • SQL を文字列連結で組み立てていないか
  • 出力時にエスケープしているか
  • 他人のデータにアクセスできないよう、認可チェックがあるか
  • API キーやパスワードがコードに直接書かれていないか
  • デバッグ用のコードや、テスト用の管理者アカウントが残っていないか

AI に「このコードのセキュリティ上の問題点を指摘して」と依頼してレビューさせるのも有効ですが、最終的な判断と責任は人が持つ、という姿勢が大切です。

公開前のセキュリティチェックリスト

ここまでの内容を、公開前に確認できるチェックリストにまとめました。

サーバー

  • OS・Web サーバー・データベースが最新の状態になっている
  • SSH はパスワード認証が無効で、root ログインも禁止されている
  • データベースのポートが外部に公開されていない
  • サイト全体が HTTPS になっている
  • ドキュメントルートが public フォルダになっており、.env や .git にアクセスできない
  • 書き込み権限が必要なフォルダだけに付与されている
  • バックアップを取得し、復元できることを確認している

PHP

  • サポート中の PHP バージョンを使っている
  • display_errors = Off、expose_php = Off になっている
  • セッション Cookie に HttpOnly・Secure・SameSite が設定されている
  • composer audit で既知の脆弱性がない

フレームワーク

  • フレームワークが最新のセキュリティ修正を含むバージョンになっている
  • 本番モードになっており、デバッグ画面が表示されない
  • .env が Git に含まれていない
  • CSRF 保護が有効になっている
  • モデルで保存可能な項目が制限されている

ソースコード

  • SQL はプリペアドステートメントまたはクエリビルダーで実行している
  • 出力時にエスケープしている
  • パスワードは password_hash() で保存している
  • すべての画面・処理で認可チェックをしている
  • ファイルアップロードで種類・サイズ・保存先を制限している
  • トークンは random_bytes() で生成し、hash_equals() で比較している

よくある質問

小さな個人サイトでも狙われますか?

狙われます。攻撃の多くはボットが自動で行っており、規模や知名度に関係なく、インターネット上のサーバーを片っ端から調べています。乗っ取られたサーバーが、迷惑メールの送信や別の攻撃の踏み台に使われることもあります。

レンタルサーバーなら、サーバーのセキュリティは気にしなくていいですか?

OS やミドルウェアの更新は事業者が行ってくれることが多いため、負担は大きく減ります。ただし、PHP のバージョン選択、ドキュメントルートの設定、ファイルの権限、管理画面のパスワードなど、利用者側が設定する部分は残ります。

フレームワークを使えば安全ですか?

多くの攻撃への対策が用意されているため、ゼロから作るよりはるかに安全です。ただし、エスケープを外す記法を使う、生の SQL を文字列連結で書く、認可チェックを忘れる、といった使い方をすれば穴は生まれます。「対策の道具はそろっている。正しく使うのは自分」と考えるのが正確です。

参考

まとめ

PHP のセキュリティは、サーバー、PHP 本体、フレームワーク、ソースコードの 4 つの層で考えると整理しやすくなります。サーバーでは「更新」と「入口を絞ること」、PHP では「サポート中のバージョン」と「本番向けの設定」、フレームワークでは「本番モードと標準機能の正しい利用」、ソースコードでは「入力を信用せず、出力時にエスケープし、権限を毎回確認すること」が基本です。

一度にすべてを完璧にする必要はありません。まずは上のチェックリストで現状を確認し、当てはまらない項目から一つずつ対応していきましょう。どれか一つの層が破られても、他の層で被害を食い止められる状態を目指すことが、現実的で強いセキュリティにつながります。

作りたいものがあるなら、VibeCoder に依頼しませんか?

AIを使いこなす VibeCoder が、最速1日で納品します。依頼の送信は無料です。担当者より2〜3営業日以内にご連絡します。

※納期は内容・規模によって異なります。

無料で依頼を送る →