PHPでフレームワークを使わずにWebアプリケーションを構築する場合、セキュリティ対策をすべて開発者自身が制御する必要があります。 本記事では、以下6つの対策について解説します。
- XSS(クロスサイト・スクリプティング)
- CSRF(クロスサイト・リクエスト・フォージェリ)
- セッションハイジャック対策
- SQLインジェクション
- OSコマンドインジェクション
- ディレクトリトラバーサル
XSS対策 htmlspecialcharsによるサニタイズ
ユーザーの入力をブラウザに出力する際、HTMLタグやJavaScriptとして実行されるのを防ぎます。
$username = $_POST['username'] ?? '';
echo "こんにちは、" . htmlspecialchars($username, ENT_QUOTES, 'UTF-8') . "さん";
htmlspecialchars関数は、特定の文字(<, >, &, “, ‘)をHTMLエンティティ(<など)に変換します。引数のENT_QUOTESは、シングルクォートとダブルクォートの両方を変換対象にするための指定です。UTF-8の引数は文字コードを明示的に指定し、マルチバイト文字による誤作動を防ぎます。
NG例
echo "こんにちは、" . $_POST['username'] . "さん";
<script>alert('XSS')</script>のような入力がそのまま実行されてしまいます。
CSRF対策 ワンタイムトークンの発行と検証
利用者の意図しないリクエスト(設定変更や投稿など)を強制される攻撃を防ぐため、正当な画面からの遷移であることを確認します。
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $token)) {
http_response_code(403);
exit('Invalid CSRF token');
}
}
暗号論的に安全なランダムトークンを生成し、hash_equalsで比較します。
NG例
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
updateUserSettings($_POST);
}
攻撃者が用意した外部サイトから、ログイン済みユーザーに意図しないリクエストを送信させることができてしまいます。
セッションハイジャック対策
セッションIDが第三者に盗用されるリスクを低減するため、Cookieの設定とセッション管理を厳格に行います。
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_only_cookies', 1);
session_start();
session_regenerate_id(true);
1~3行目でそれぞれJavaScriptからのCookie読み取り禁止、HTTPS接続時のみCookieを送信許可、URLのパラメーターからのセッション受け渡しを禁止しています。 session_regenerate_id関数はログイン時などの重要なタイミングでセッションIDを更新し、セッション固定攻撃を防ぎます。
SQLインジェクション対策 PDOプリペアドステートメントの利用
ユーザーが入力した文字が、意図せずSQLの命令(コマンド)の一部として実行されてしまう事を防ぎます。
$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';
$pdo = new PDO($dsn, 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE email = :email");
$stmt->bindValue(':email', $_POST['email'], PDO::PARAM_STR);
$stmt->execute();
prepareとbindValueを使用し、値を命令から分離します。
NG例
$email = $_POST['email'];
$sql = "SELECT id, name FROM users WHERE email = '$email'";
$result = mysqli_query($conn, $sql);
' OR '1'='1のような入力で全件取得されたり、'; DROP TABLE users; --でテーブルを削除されるリスクがあります。
OSコマンドインジェクション
システムコマンドを実行する際、外部入力をコマンドの一部として扱わないようにします。
$mode = $_GET['mode'] ?? 'default';
$allowed_commands = [
'backup' => '/usr/bin/backup_script',
'status' => '/usr/bin/status_check'
];
if (!isset($allowed_commands[$mode])) {
exit('Invalid request');
}
$cmd = $allowed_commands[$mode];
exec($cmd, $output);
ユーザー入力を直接コマンドライン引数に渡さず、あらかじめ定義したホワイトリストから選択させます。
NG例
$file = $_GET['file'];
exec("cat /var/www/html/logs/$file", $output);
; rm -rf /のような入力でサーバー上の任意のコマンドを実行されるリスクがあります。
ディレクトリトラバーサル
ファイル操作において、想定外のディレクトリへのアクセスを制限します。
$base_dir = '/var/www/html/storage/';
$requested_file = $_GET['file'] ?? '';
$filename = basename($requested_file);
$target_path = $base_dir . $filename;
if (strpos(realpath($target_path), realpath($base_dir)) !== 0) {
exit('Access denied');
}
if (file_exists($target_path)) {
readfile($target_path);
}
basename()でディレクトリ要素を除去し、realpath()で最終的なパスが許可範囲内にあるかを確認します。
NG例
$file = $_GET['file'];
readfile('/var/www/html/storage/' . $file);
../../etc/passwdのような入力でサーバーの任意のファイルを読み取られるリスクがあります。
この記事のまとめ
ノンフレームワークでの開発では、標準関数を適切に組み合わせてデータの安全性を確保する必要があります。
| 脆弱性 | 対策 |
|---|---|
| XSS | htmlspecialcharsで出力時にエスケープ |
| CSRF | ワンタイムトークンの発行とhash_equalsによる検証 |
| セッションハイジャック | Cookie設定の厳格化とsession_regenerate_idの使用 |
| SQLインジェクション | PDOプリペアドステートメントで値と命令を分離 |
| OSコマンドインジェクション | ホワイトリストからのみコマンドを選択 |
| ディレクトリトラバーサル | basenameとrealpathでパスを検証 |
フレームワークの仕組みを理解するいい機会にもなります。