PHPは学習しやすい言語ですが、その手軽さゆえに無意識に危険なコードやパフォーマンスの悪いコードをうっかり書いてしまうことがあります。
本記事では、実務でよく見かける「やってはいけないこと」を、備忘録も兼ねてセキュリティとパフォーマンスの観点からまとめてみました。
目次
セキュリティ編
1. SQLインジェクション:生のSQLに直接値を埋め込む
❌ NG例
// 危険!ユーザー入力を直接SQL文に埋め込んでいる
$userId = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $userId";
$result = mysqli_query($conn, $sql);
問題点:
-
$_GET['id']に1 OR 1=1などを入れられると全データが漏洩 -
'; DROP TABLE users; --のような破壊的な操作も可能
✅ OK例
// プリペアドステートメントを使用
$userId = $_GET['id'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
$result = $stmt->fetch();
またはLaravelの場合:
// Eloquent ORM
$user = User::find($id);
// クエリビルダー(プレースホルダー自動使用)
$user = DB::table('users')->where('id', $id)->first();
2. XSS(クロスサイトスクリプティング):エスケープ処理の欠如
❌ NG例
// ユーザー入力をそのまま出力
<?php
$username = $_POST['username'];
echo "ようこそ、" . $username . "さん";
?>
問題点:
-
<script>alert('XSS')</script>などを入力されるとJSが実行される - Cookieの盗難、フィッシング、マルウェア配布などに悪用可能
✅ OK例
// htmlspecialchars でエスケープ
<?php
$username = $_POST['username'];
echo "ようこそ、" . htmlspecialchars($username, ENT_QUOTES, 'UTF-8') . "さん";
?>
Laravelのbladeテンプレートでは:
{{-- 自動エスケープされる --}}
<p>ようこそ、{{ $username }}さん</p>
{{-- エスケープしない場合(信頼できるHTMLのみ) --}}
<div>{!! $trustedHtml !!}</div>
3. CSRF対策の欠如
❌ NG例
// フォーム送信をそのまま処理
if ($_POST['action'] === 'delete') {
deleteUser($_POST['user_id']);
}
問題点:
- 外部サイトから勝手にPOSTリクエストを送られる可能性
- ユーザーの意図しない操作が実行される
✅ OK例
// CSRFトークンの検証
session_start();
// トークン生成(フォーム表示時)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// トークン検証(フォーム送信時)
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('不正なリクエストです');
}
Laravelでは自動的に対応:
<form method="POST" action="/user/delete">
@csrf {{-- CSRFトークンを自動生成 --}}
<button type="submit">削除</button>
</form>
4. パスワードの平文保存・脆弱なハッシュ化
❌ NG例
// 最悪:平文保存
$sql = "INSERT INTO users (password) VALUES ('{$_POST['password']}')";
// 悪い:MD5やSHA1(レインボーテーブル攻撃に弱い)
$hashedPassword = md5($_POST['password']);
// 不十分:ソルトなしのハッシュ
$hashedPassword = hash('sha256', $_POST['password']);
✅ OK例
// password_hash を使用(bcrypt、自動ソルト付き)
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
// 検証時
if (password_verify($_POST['password'], $hashedPassword)) {
// ログイン成功
}
5. ファイルアップロードの検証不足
❌ NG例
// 拡張子のみチェック(危険)
$filename = $_FILES['upload']['name'];
if (pathinfo($filename, PATHINFO_EXTENSION) === 'jpg') {
move_uploaded_file($_FILES['upload']['tmp_name'], 'uploads/' . $filename);
}
問題点:
-
shell.php.jpgのようなファイル名で回避可能 - MIMEタイプの偽装も可能
- PHPファイルをアップロードされてサーバー乗っ取りの危険
✅ OK例
// 複数の検証を組み合わせる
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
$allowedExts = ['jpg', 'jpeg', 'png', 'gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $_FILES['upload']['tmp_name']);
$extension = strtolower(pathinfo($_FILES['upload']['name'], PATHINFO_EXTENSION));
if (in_array($mimeType, $allowedMimes) && in_array($extension, $allowedExts)) {
// ランダムなファイル名を生成
$newFilename = bin2hex(random_bytes(16)) . '.' . $extension;
move_uploaded_file($_FILES['upload']['tmp_name'], 'uploads/' . $newFilename);
}
パフォーマンス編
6. N+1問題:ループ内でのクエリ実行
❌ NG例
// 記事一覧を取得
$posts = Post::all(); // 1回のクエリ
foreach ($posts as $post) {
// 各記事の著者を取得(Nクエリが発生)
echo $post->author->name;
}
// 合計: 1 + N回のクエリ
問題点:
- 100件の記事があれば101回のクエリが実行される
- データベースへの接続オーバーヘッドで激遅に
✅ OK例
// Eager Loadingで一度に取得
$posts = Post::with('author')->get(); // 2回のクエリのみ
foreach ($posts as $post) {
echo $post->author->name;
}
生PHPの場合:
// JOINを使って一度に取得
$sql = "SELECT posts.*, users.name as author_name
FROM posts
INNER JOIN users ON posts.user_id = users.id";
$results = $pdo->query($sql)->fetchAll();
7. SELECT * の乱用
❌ NG例
// 全カラムを取得(不要なデータも含む)
$users = DB::table('users')->select('*')->get();
問題点:
- BLOBやTEXT型など大きなデータも取得してメモリ圧迫
- ネットワーク転送量の増加
- 必要なカラムが不明確でコードの可読性低下
✅ OK例
// 必要なカラムのみ指定
$users = DB::table('users')
->select('id', 'name', 'email')
->get();
8. ループ内での重い処理
❌ NG例
// 1万件のデータを処理
$users = User::all();
foreach ($users as $user) {
// ループ内で毎回外部API呼び出し
$response = file_get_contents("https://api.example.com/check/{$user->id}");
// メール送信
mail($user->email, 'Subject', 'Body');
}
問題点:
- 1万回のAPI呼び出し、メール送信で処理時間が膨大に
- タイムアウトやメモリ不足の原因
✅ OK例
// バッチ処理とキューを活用
User::chunk(100, function ($users) {
foreach ($users as $user) {
// ジョブキューに追加(非同期処理)
ProcessUserJob::dispatch($user);
}
});
またはバルク処理:
// 一括でAPIに送信
$userIds = $users->pluck('id')->toArray();
$response = Http::post('https://api.example.com/bulk-check', [
'user_ids' => $userIds
]);
9. メモリリーク:大量データの一括取得
❌ NG例
// 100万件のデータを一度にメモリに展開
$allUsers = User::all(); // メモリ不足でクラッシュ
foreach ($allUsers as $user) {
processUser($user);
}
✅ OK例
// chunkで分割処理(メモリ効率的)
User::chunk(1000, function ($users) {
foreach ($users as $user) {
processUser($user);
}
});
// またはcursor(さらに省メモリ)
foreach (User::cursor() as $user) {
processUser($user);
}
10. 不要なセッションデータの保持
❌ NG例
// 大きなデータをセッションに保存
$_SESSION['all_products'] = Product::all()->toArray(); // 数MBのデータ
$_SESSION['large_image'] = file_get_contents('huge_image.jpg');
問題点:
- セッションファイルが肥大化
- 毎リクエストでシリアライズ/デシリアライズのオーバーヘッド
✅ OK例
// 必要最小限のIDのみ保存
$_SESSION['selected_product_ids'] = [1, 5, 10];
// 使用時に再取得
$products = Product::whereIn('id', $_SESSION['selected_product_ids'])->get();
// 画像はストレージに保存してパスのみセッションに
$_SESSION['uploaded_image_path'] = 'uploads/abc123.jpg';
コーディング習慣編
11. エラーメッセージの詳細を本番環境で表示
❌ NG例
// 本番環境でもエラー詳細を表示
ini_set('display_errors', 1);
error_reporting(E_ALL);
問題点:
- データベース構造、ファイルパスなどの内部情報が漏洩
- 攻撃者に脆弱性を探すヒントを与える
✅ OK例
// 開発環境
if (getenv('APP_ENV') === 'local') {
ini_set('display_errors', 1);
error_reporting(E_ALL);
} else {
// 本番環境:エラーログに記録、ユーザーには汎用メッセージ
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
}
12. register_globals 的な危険な変数展開
❌ NG例
// $_GET, $_POST を extract(超危険)
extract($_POST);
// 管理者チェック
if ($isAdmin) { // 外部から$isAdmin=trueを送られる可能性
deleteAllUsers();
}
✅ OK例
// 変数を明示的に取得・検証
$username = $_POST['username'] ?? '';
$isAdmin = checkAdminStatus($userId);
if ($isAdmin) {
deleteAllUsers();
}
13. == と === の使い分けミス
❌ NG例
// 型の緩い比較による予期しない挙動
if ($_GET['id'] == 0) { // "0abc" もtrueになる
showDefaultPage();
}
if (in_array('admin', $roles)) { // true が含まれていてもtrueに
grantAccess();
}
✅ OK例
// 厳密な比較を使用
if ($_GET['id'] === '0') {
showDefaultPage();
}
if (in_array('admin', $roles, true)) { // 第3引数でstrict mode
grantAccess();
}
14. 古いPHPバージョンの使用
❌ NG
// PHP 5.6や7.0などサポート切れバージョンの使用
問題点:
- セキュリティパッチが提供されない
- 新機能・パフォーマンス改善の恩恵を受けられない
✅ OK
- PHP 8.1以上(できれば8.2以上)を使用
- 定期的なバージョンアップ計画を立てる
15. 適切なエスケープ関数の選択ミス
❌ NG例
// 用途に合わないエスケープ
$filename = htmlspecialchars($_GET['file']); // ファイル名には不適切
include "templates/{$filename}.php"; // ディレクトリトラバーサル可能
// JavaScriptコンテキストでhtmlspecialchars
echo "<script>var name = '" . htmlspecialchars($name) . "';</script>";
✅ OK例
// ファイル名はホワイトリスト方式
$allowedTemplates = ['home', 'about', 'contact'];
$template = $_GET['template'] ?? 'home';
if (in_array($template, $allowedTemplates, true)) {
include "templates/{$template}.php";
}
// JavaScriptコンテキストではJSON
echo "<script>var name = " . json_encode($name) . ";</script>";
まとめ
本記事で紹介した「やってはいけないこと」は、実務でよく見かける問題です。
セキュリティ最重要ポイント
- ユーザー入力は常に疑う(プリペアドステートメント、エスケープ)
- CSRFトークンは必須
- パスワードはpassword_hash
- ファイルアップロードは多層防御
パフォーマンス最重要ポイント
- N+1問題に注意(Eager Loading)
- 大量データはchunk処理
- 必要なカラムのみSELECT
- 重い処理はキューで非同期化
セキュアで高速なPHPアプリケーションを構築するには、上記ポイントの把握が必須です。