1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【超超超初心者向け】PHPでやってはいけないこと:セキュリティ&パフォーマンス編

Last updated at Posted at 2025-11-05

PHPは学習しやすい言語ですが、その手軽さゆえに無意識に危険なコードやパフォーマンスの悪いコードをうっかり書いてしまうことがあります。

本記事では、実務でよく見かける「やってはいけないこと」を、備忘録も兼ねてセキュリティとパフォーマンスの観点からまとめてみました。

目次

  1. セキュリティ編
  2. パフォーマンス編
  3. コーディング習慣編

セキュリティ編

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>";

まとめ

本記事で紹介した「やってはいけないこと」は、実務でよく見かける問題です。

セキュリティ最重要ポイント

  1. ユーザー入力は常に疑う(プリペアドステートメント、エスケープ)
  2. CSRFトークンは必須
  3. パスワードはpassword_hash
  4. ファイルアップロードは多層防御

パフォーマンス最重要ポイント

  1. N+1問題に注意(Eager Loading)
  2. 大量データはchunk処理
  3. 必要なカラムのみSELECT
  4. 重い処理はキューで非同期化

セキュアで高速なPHPアプリケーションを構築するには、上記ポイントの把握が必須です。

参考リソース

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?