PHP&Webセキュリティ・脆弱性について【前半】

© Yamanashi Internet Security Team.

Webアプリケーション開発において、セキュリティ対策が不十分なPHPプログラムは重大なサイバー攻撃の標的となります。代表的な3つの脆弱性の仕組みと危険なコード、その対策を解説します。実際には、PHP だけに限ったことではありません。Web APPすべてに言えることです。

1. SQLインジェクション (SQL Injection)

ユーザーの入力値をそのままSQL文に結合することで、開発者が意図しないSQL文が実行されてしまう脆弱性です。データベース内の情報漏洩や改ざん、消去につながります。

❌ 危険なコード例
// URLパラメータからIDを取得
$id = $_GET['id']; 

// ユーザー入力をそのままSQL文に結合している
$sql = "SELECT * FROM users WHERE id = " . $id; 
$result = $conn->query($sql);
💡 解説:
攻撃者が id に 1 OR 1=1 を指定した場合、条件式が常に真となり、全ユーザーの情報が漏洩する危険があります。
🛠️ 対策

ユーザー入力をSQL文に直接結合せず、静的プレースホルダ(プリペアドステートメント)を使用します。

$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();

2. クロスサイトスクリプティング (XSS)

ウェブサイトに悪意のあるスクリプト(JavaScriptなど)を埋め込まれ、それを閲覧した別のユーザーのブラウザ上で実行されてしまう脆弱性です。セッション情報の盗難などに悪用されます。

❌ 危険なコード例
$name = $_GET['name']; 

// 入力された値をそのまま画面に出力している
echo "こんにちは、" . $name . "さん!";
💡 解説:
name に <script>alert('攻撃')</script> などのコードが渡された場合、ブラウザがJavaScriptとして実行してしまいます。
🛠️ 対策

画面に出力する際は、HTMLとして意味を持つ記号を無害な文字列に変換するエスケープ処理(サニタイジング)を行います。

echo "こんにちは、" . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . "さん!";

3. OSコマンドインジェクション

Webサーバーが内部でOSコマンドを実行する際に、ユーザー入力をそのまま渡してしまうことで、サーバーを不正に操作される脆弱性です。

❌ 危険なコード例
$ip = $_GET['ip']; 

// サーバーのシステムコマンドを直接実行している
system("ping -c 4 " . $ip);
💡 解説:
攻撃者が 8.8.8.8; cat /etc/passwd と入力すると、pingの後にサーバー内の重要ファイルを読み出すコマンドが実行されてしまいます。
🛠️ 対策

原則としてユーザー入力をOSコマンドの引数に渡す設計を避けるか、引数を安全にエスケープする関数を使用します。

$safe_ip = escapeshellarg($ip);
system("ping -c 4 " . $safe_ip);

PHP&Webセキュリティ・脆弱性について【後半】

© Yamanashi Internet Security Team.

後半では、より具体的な実装シーン(ファイルアップロードやログイン等)における脆弱性と、実際のサイバー攻撃を想定したケーススタディと具体的な回避コードを解説します。

4. ファイルアップロードの脆弱性

ファイル形式のチェックが不十分な場合、悪意のあるプログラム(Webシェル)をアップロードされ、サーバーを乗っ取られる危険性があります。

❌ 危険なコード例
$target_file = "uploads/" . $_FILES["avatar"]["name"];

// ファイルの拡張子を確認せず、そのまま保存している
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);
💡 解説:
attack.php のようなプログラムがアップロードされ、そのファイルにURLから直接アクセスされると、サーバー上で不正なプログラムが実行されます。
🛠️ 対策

5. ディレクトリトラバーサル (Directory Traversal)

ファイル名やパスを指定するパラメータを悪用され、公開を意図していないサーバー内の重要なシステムファイルや設定ファイルを読み取られてしまう脆弱性です。

❌ 危険なコード例
$file = $_GET['file'];

// 指定されたファイルをそのまま読み込んで表示する
include("pages/" . $file);
💡 解説:
file の値に ../../config/database.php のような相対パスを入力されると、意図したフォルダを飛び越えて重要ファイルが読み込まれてしまいます。
🛠️ 対策

ユーザー入力でファイル名を直接指定させない(IDによるマッピングなどを行う)か、basename() 関数を使ってファイル名部分のみを抽出します。

$file = basename($_GET['file']); // パス移動の文字列(/ や ..)が無効化される
include("pages/" . $file);

6. ログイン画面における認証不備

パスワードの検証方法が不適切だったり、何度もログインを試せる状態(総当たり攻撃が可能)になっていると、アカウントを簡単に乗っ取られてしまいます。

❌ 危険なコード例
$username = $_POST['username'];
$password = $_POST['password']; 

// データベースのパスワードがハッシュ化されずに生のまま比較されている
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
💡 解説:
パスワードが生の文字列(平文)で保存されていると、データベースが漏洩した際に全員のパスワードが露出します。また、連続試行の制限がないと自動ツールで簡単に突破されます。ログインの連続試行制限(アカウントロック)は、不正アクセス(ブルートフォース攻撃や辞書攻撃)からアカウントを保護するためのセキュリティ機能です。WordPressの場合プラグインがあります。レンタルサーバーの場合、管理パネルから標準でログイン試行回数制限をON/OFFできる機能が用意されていますが、これも脆弱性に繋がります(PHPの脆弱性に依存されます)。YINでは、Fail2BAN & CrowdSEC が短時間に指定回数以上記録された場合、その攻撃者のIPアドレスをファイアウォールのルールに追加し、サーバーへのアクセス自体を一定時間ブロックします。脆弱性に依存されません。そもそも悪意を持った攻撃者の場合、多層防衛の仕組みでDROPされます。なお、この場合一定時間ブロックの仕組みは独自ルールで設定します。
🛠️ 対策

パスワードは必ず安全なハッシュ関数(password_hash / password_verify)を使用し、連続失敗時にはアカウントロックや画像認証を導入します。

$stmt = $conn->prepare("SELECT password_hash FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result()->fetch_assoc();

if ($result && password_verify($password, $result['password_hash'])) {
    // ログイン成功
}

7. お問い合わせフォーム:メールヘッダインジェクション

入力欄の改行コードを悪用され、攻撃者が任意の宛先に大量のスパムメールを送信する(踏み台にされる)脆弱性です。

❌ 危険なコード例
$email = $_POST['email']; 
$message = $_POST['message'];

// ユーザーの入力をそのままメールのヘッダ(第4引数)に渡している
$headers = "From: " . $email;
mb_send_mail("admin@example.com", "お問い合わせ", $message, $headers);
💡 解説:
メールヘッダは改行で区切られます。email の中に test@example.com\nBcc: spam@victim.com のように改行とBccを混ぜて送信されると、意図しない宛先にメールが転送されてしまいます。
🛠️ 対策

メールアドレスや件名の入力欄からは、改行コード(\r や \n)を完全に除去またはエラーにします。また、PHPMailer などの安全な外部ライブラリを使用するのが有効です。

7. お問い合わせフォーム:メールヘッダインジェクション

入力欄の改行コードを悪用され、攻撃者が任意の宛先に大量のスパムメールを送信する(踏み台にされる)脆弱性です。

❌ 危険なコード例
$headers = "From: " . $email;
mb_send_mail("admin@example.com", "お問い合わせ", $message, $headers);
💡 解説: email の中に \nBcc: spam@victim.com のように改行とBccを混ぜて送信されると、意図しない宛先にメールが転送されてしまいます。
🛠️ 対策

入力欄から改行コードを完全に除去するか、PHPMailer などの信頼できるライブラリを使用します。

8. PHP本体のセキュリティパッチを当てない場合の危惧される攻撃

どれだけ自分で書くコードを安全に記述していても、PHPエンジン自体のバグに対する修正パッチを当てず、古いバージョンを使い続けていると、自動化された攻撃の標的になります。

① RCE(遠隔コード実行:Remote Code Execution)

【危険性】 自身のプログラムに問題がなくても、特定のWebリクエストをサーバーに送るだけでPHP自体のバグ(マルチバイト処理やiconvなどの脆弱性)を突かれ、攻撃者のプログラムをサーバー上で直接実行されてしまいます。これにより、サーバー全体の権限が奪取されます。

② DoS(サービス拒否:Denial of Service)攻撃

【危険性】 PHP内部のメモリリークや無限ループのバグを利用されます。攻撃者が細工したデータを送信するだけで、サーバーのCPU使用率が100%に張り付いたり、PHPプロセスが異常終了(クラッシュ)してWebサイトが完全にダウンします。

③ メモリ内のデータ漏洩(バッファオーバーフロー等)

【危険性】 PHPのコアシステムのバグを突かれ、サーバーのメモリ上に一時保存されている「他のユーザーのセッションデータ」や「内部変数の値」が丸ごと読み取られてしまうケースがあります。

9. JavaScript(JS)環境におけるパッチ放置のリスク

JavaScript環境においてもPHPと全く同様です。自作コードに不備がなくても、実行エンジン(Node.jsなど)や、利用している外部ライブラリ(npmパッケージ等)のパッチ未適用により、深刻な被害を招きます。Web制作会社さんによくあるケースです。制作会社の大小の規模ではありません。検収段階で専門家による監査が必要です。なお、YIN に外部からAI診断などされますと、すべて攻撃とみなされ、1年間はアクセスできなくなります。

① フロントエンド(ブラウザ側JS)でのリスク

jQuery、React、Vue.jsなどの古いバージョンや外部プラグインを使い続けることで、以下の標的になります。

② バックエンド(Node.js / サーバー側JS)でのリスク

サーバーサイドでJavaScript(Node.js)を動かしている場合、Webフレームワーク(Express等)やnpmパッケージの脆弱性を放置すると、PHPと同等の壊滅的な被害が発生します。

🛡️ JS環境での一般的な自動対策:
膨大な外部ライブラリを安全に保つため、現場では npm audit や yarn audit コマンドで既知の脆弱性を自動検知したり、Dependabot などを導入してパッチ適用のプルリクエストを自動生成する対策が必須となっています。

10. PHPの脆弱性を突かれた攻撃のケーススタディと回避策

ケースA:個人情報ECサイトからの顧客データ大量流出(SQLインジェクション)

【背景】 商品検索機能にユーザー入力を直接結合した危険なSQL文が使われていたため、UNION攻撃により管理者の平文パスワードが画面に露出。数万件の個人情報が流出した。

🛡️ ケースAの具体的な回避コード:
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE :keyword");
$search_keyword = '%' . $keyword . '%';
$stmt->bindValue(':keyword', $search_keyword, PDO::PARAM_STR);
$stmt->execute();
ケースB:ファイルアップロード×トラバーサルによるサーバーの完全乗っ取り

【背景】 ファイル名のサニタイズ(basename処理)を怠ったため、Webシェル(attack.php)をWeb公開ディレクトリに直接書き込まれ、サーバーの全権限を奪取された。

🛡️ ケースBの具体的な回避コード:
$original_name = basename($_FILES["avatar"]["name"]);
$extension = pathinfo($original_name, PATHINFO_EXTENSION);

// 拡張子のチェックと、ファイル名のランダム文字列への完全書き換え
$secure_filename = bin2hex(random_bytes(16)) . '.' . $extension;
$target_file = "uploads/" . $secure_filename;
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);

© Yamanashi Internet Security Team Since 1997