セキュアなWebアプリ開発 ― 押さえておきたい脆弱性と対策まとめ
本記事の出典
徳丸 浩 著『体系的に学ぶ 安全なWebアプリケーションの作り方 第2版』(SBクリエイティブ)の内容をもとに、要点を整理・再構成したものです。
より詳しい解説や背景については、ぜひ原著をご参照ください。
はじめに ― 脆弱性とは何か
脆弱性とは、ひと言でいえば 「悪用できるバグ」 のことです。
個人情報の漏洩、サイト改ざん、なりすまし、DoS攻撃など深刻な被害につながります。
なぜ放置してはいけないのか?
| 観点 | ポイント |
|---|---|
| 💰 経済的損失 | 補填・機会損失・信用失墜等で 数億〜数十億円 の被害になりうる |
| ⚖️ 法的義務 | 個人情報保護法により 安全管理措置 が義務付けられている |
| 😢 回復不能 | 漏洩した個人情報や毀損された名誉は 元に戻せない |
| 🤖 犯罪への荷担 | 脆弱なサイトは ボットネット構築 に悪用され、DDoS等に加担してしまう |
脆弱性が生まれる2つの原因
- バグ ― SQLインジェクション、XSSなど。セキュリティと無関係の箇所で発生しアプリ全体に影響
- チェック不足 ― ディレクトリ・トラバーサルなど。セキュリティチェックの必要性に対する認識が不足
セキュリティバグ vs セキュリティ機能
| 種類 | 例 | 誰が判断? |
|---|---|---|
| セキュリティバグ | XSS、SQLi | なくすのが 当然(開発者の責務) |
| セキュリティ機能 | HTTPS、二段階認証 | 費用と相談して 発注者が判断 |
Web技術の基礎
HTTPの基本構造
HTTPのリクエストとレスポンスは、どちらも リクエスト/ステータスライン・ヘッダ・ボディ の3要素で構成されます。
GETとPOSTの使い分け
POSTを使うべき3つの場面:
- データ更新(副作用あり)
- 秘密情報の送信
- 大量データの送信
GETではパラメータがURL上に露出し、Referer経由の漏洩やログへの記録が起こりえます。
hiddenパラメータの注意点
HTTPレイヤーではhiddenもテキストボックスも同じ扱いです。
プロキシツールで書き換え可能なため、ブラウザから送信する値はすべて利用者が書き換え可能 という前提で設計する必要があります。
クッキーとセッション管理
HTTPはステートレスであるため、状態保持のために クッキー(cookie) が導入されました。
基本ルール: クッキーにはセッションIDのみ格納し、実際のデータはサーバー側で管理する。
セッションIDに求められる3つの要件
| # | 要件 | 対策 |
|---|---|---|
| 1 | 推測できない | 暗号論的擬似乱数で生成。セッション管理機構は自作しない |
| 2 | 強制されない | 認証後にセッションIDを変更 する(固定化攻撃対策) |
| 3 | 漏洩しない | クッキー属性の適切な設定、TLS暗号化、XSS対策 |
クッキーの重要属性
| 属性 | ポイント |
|---|---|
| Domain | 原則 設定しない(設定すると送信範囲が広がり漏洩リスク増) |
| Secure | HTTPS通信時のみクッキーを送信。セッションIDには 必須 |
| HttpOnly | JavaScriptからアクセス不可。XSSによるID窃取を困難にする |
受動的攻撃と同一オリジンポリシー
| 種類 | しくみ | 代表例 |
|---|---|---|
| 能動的攻撃 | 攻撃者がサーバーを 直接 攻撃 | SQLインジェクション |
| 受動的攻撃 | 利用者に罠を踏ませ、利用者 経由 で攻撃 | XSS、CSRF |
同一オリジンポリシー
ブラウザはJavaScriptの機能を制限するサンドボックスを提供しており、その中心が 同一オリジンポリシー です。
同一オリジンの条件(3つすべて一致が必要):
ホスト(FQDN)、スキーム(プロトコル)、ポート番号
XSS攻撃は、対象サイト内にスクリプトを送り込むことでこの制約を 回避 する手法です。
CORS(Cross-Origin Resource Sharing)
| パターン | しくみ |
|---|---|
| シンプルなリクエスト | GET/HEAD/POST + 限定ヘッダの場合、レスポンスの Access-Control-Allow-Origin で許可 |
| プリフライト | シンプルでない場合、先に OPTIONSメソッド で許可を確認 |
| 認証情報つき | クッキー等を送信するには withCredentials = true + Allow-Credentials: true の 両方 が必要 |
主要な攻撃手法と対策
インジェクションの共通原理
脆弱性は 出力に起因 するものと 処理に起因 するものに分かれ、入力に起因する脆弱性はありません。
共通原理は「データを想定している箇所に命令を混入させる」ことです。
| 出力先 | 脆弱性 | 注入されるもの |
|---|---|---|
| HTML | XSS | スクリプトやHTML |
| SQL | SQLインジェクション | SQL文 |
| シェル | OSコマンド・インジェクション | コマンド |
| メール | メールヘッダ・インジェクション | ヘッダ・本文 |
| HTTPヘッダ | HTTPヘッダ・インジェクション | レスポンスヘッダ |
XSS(クロスサイト・スクリプティング)
HTML生成時の実装不備により、攻撃者のスクリプトが利用者のブラウザ上で実行されます。
影響: クッキー窃取によるなりすまし、利用者権限での機能悪用、フィッシング
対策の基本
- HTMLのメタ文字(
<>&"')を エスケープ する - 属性値は必ず ダブルクォート で囲む
- HTTPレスポンスに 文字エンコーディングを指定 する
コード例
// ❌ 危険:エスケープなし
echo "<p>検索キーワード: " . $_GET['keyword'] . "</p>";
// ✅ 安全:htmlspecialchars でエスケープ
echo "<p>検索キーワード: " . htmlspecialchars($_GET['keyword'], ENT_QUOTES, 'UTF-8') . "</p>";
// ❌ 危険:innerHTML はHTMLとして解釈される
element.innerHTML = userInput;
// ✅ 安全:textContent はテキストとして扱われる
element.textContent = userInput;
SQLインジェクション
SQLの呼び出し方の不備により、攻撃者がSQL文を改変・注入できます。
利用者の関与なしに攻撃可能(能動的攻撃)。
影響: 全データの窃取、データ改ざん、認証回避、プログラム実行
対策
- プレースホルダ を用いてSQLを組み立てる
- 詳細なエラーメッセージを画面に表示しない
- DBアカウントの権限を最小限にする
コード例
// ❌ 危険:文字列連結でSQL構築
$sql = "SELECT * FROM users WHERE id = '" . $_POST['id'] . "'";
$result = $pdo->query($sql);
// ✅ 安全:プレースホルダ(プリペアドステートメント)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND password = ?");
$stmt->execute([$_POST['id'], $_POST['password']]);
$user = $stmt->fetch();
CSRF(クロスサイト・リクエストフォージェリ)
「重要な処理」(購入・退会・パスワード変更等)のリクエストが利用者の意図したものか確認していない場合に発生します。
対策
- CSRFトークン を埋め込み、リクエスト時に検証
- 重要な処理実行後に登録済メールアドレスに通知メールを送信(保険的対策)
コード例
// トークン生成(フォーム表示時)
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// トークン検証(フォーム送信時)
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
die('不正なリクエストです');
}
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token"
value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<button type="submit">送金</button>
</form>
クリックジャッキング
透明なiframeを重ねることで、利用者が意図しないボタンをクリックさせる手法です。
// ✅ 対策:X-Frame-Options ヘッダ
header('X-Frame-Options: DENY');
セッション管理の不備
| 攻撃手法 | 対策 |
|---|---|
| IDの推測 | 開発ツールのセッション管理機構を使う(自作しない) |
| IDの盗み出し | Secure/HttpOnly設定、URLにIDを含めない |
| IDの固定化 | 認証成功時にセッションIDを変更 |
コード例
// ✅ セッションの安全な設定
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', 1);
ini_set('session.use_only_cookies', 1);
// ✅ セッションID固定化攻撃への対策
session_start();
if ($authenticated) {
session_regenerate_id(true); // 古いセッションファイルを削除
$_SESSION['user_id'] = $user['id'];
}
OSコマンド・インジェクション
シェル経由でOSコマンドを呼び出す際の不備により、意図しないコマンドが実行されます。
対策(優先順):
- OSコマンドを呼び出さない(言語のライブラリで代替)
- シェルを経由せずコマンドを直接実行する
- パラメータをエスケープする
// ❌ 危険:ユーザ入力をそのままコマンドに渡す
exec("cat /var/data/" . $_POST['filename']);
// ✅ より安全:そもそもOSコマンドを使わない
$content = file_get_contents('/var/data/' . basename($_POST['filename']));
メールヘッダ・インジェクション
メールの宛先や件名に 改行文字 を混入し、ヘッダや本文を追加・改変する攻撃です。
対策: メール送信専用ライブラリを使用し、外部パラメータをヘッダに直接含めない。
// ❌ 危険:件名にユーザ入力をそのまま使用
mb_send_mail($to, $_POST['subject'], $body);
// ✅ 安全:改行を除去
$subject = str_replace(["\r", "\n"], '', $_POST['subject']);
リダイレクト処理の脆弱性
| 脆弱性 | 対策 |
|---|---|
| オープンリダイレクト | リダイレクト先を 固定 or 許可リストに制限 |
| HTTPヘッダ・インジェクション | リダイレクトやクッキー生成は 専用API に任せる |
// ❌ 危険:ユーザ入力をそのままリダイレクト先にする
header('Location: ' . $_GET['redirect_url']);
// ✅ 安全:許可リスト方式
$allowed = ['home' => '/dashboard', 'profile' => '/user/profile'];
$key = $_GET['redirect'] ?? 'home';
header('Location: ' . ($allowed[$key] ?? '/dashboard'));
exit;
構造化データの脆弱性
| 脆弱性 | 対策 |
|---|---|
| evalインジェクション | evalを 使わない |
| 安全でないデシリアライゼーション | シリアライズ形式に JSONを使用 |
| XXE(XML外部実体参照) | 外部実体参照を 禁止。JSONを優先 |
// ❌ 危険
eval('$result = ' . $_GET['calc'] . ';');
$data = unserialize($_COOKIE['user_data']);
// ✅ 安全:JSON を使う
$data = json_decode($_COOKIE['user_prefs'], true) ?? [];
ファイル関連の脆弱性
| 脆弱性 | 対策 |
|---|---|
| ディレクトリ・トラバーサル | ファイル名を外部から指定しない |
| 意図しないファイル公開 | 非公開ファイルは公開ディレクトリの 外 に置く |
| アップロードによる攻撃 | 拡張子チェック、サイズ制限、公開ディレクトリ外に保存 |
// ✅ アップロード時の拡張子チェック
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
die('許可されていないファイル形式です');
}
JavaScript実装の脆弱性
| 脆弱性 | 対策 |
|---|---|
| DOM Based XSS |
innerHTML → textContent
|
| Webストレージの不適切利用 | 秘密情報は 保存しない |
| postMessageの不備 | 受信時に オリジンを検証 |
// ✅ postMessage のオリジン検証
window.addEventListener('message', (e) => {
if (e.origin !== 'https://trusted-site.com') return;
processData(e.data);
});
セキュリティ機能の正しい実装
認証(Authentication)― パスワードの安全な保存
パスワードは ハッシュ値 で保存します。ソルト(ユーザ毎の乱数)と ストレッチング(ハッシュ計算の繰り返し)で、レインボーテーブルやオフラインブルートフォースに対抗します。
// ❌ 危険な保存方法
$hash = md5($password); // 絶対に使わない
$hash = sha1($password); // 絶対に使わない
// ✅ 安全な保存方法
$hash = password_hash($password, PASSWORD_DEFAULT);
// → BCrypt + ソルト + ストレッチングが自動適用
// ✅ パスワードの検証(ログイン時)
if (password_verify($inputPassword, $storedHash)) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
} else {
$error = 'IDまたはパスワードが違います'; // どちらが間違いか伝えない
}
認可(Authorization)― 権限管理
認証された利用者に対して 権限を与える ことです。権限情報は セッション変数 に保持し、処理の 直前 にチェックします。
// ❌ 危険:パラメータのユーザIDをそのまま使う
$stmt = $pdo->prepare('SELECT * FROM profiles WHERE user_id = ?');
$stmt->execute([$_GET['id']]); // 他人の情報も見える
// ✅ 安全:セッションからユーザIDを取得
$stmt = $pdo->prepare('SELECT * FROM profiles WHERE user_id = ?');
$stmt->execute([$_SESSION['user_id']]); // 改ざん不可
セキュリティヘッダ
すべてのレスポンスで設定すべきヘッダがあります。
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
# Nginx の場合
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'" always;
文字コードとセキュリティ
文字コードの扱いに不備があると脆弱性の原因になります。
4つのポイント:
- 文字集合を統一 ― アプリケーション全体を Unicode で統一
- 入力時にチェック ― 不正な文字エンコーディングをエラーにする
- 処理で正しく扱う ― マルチバイト対応の関数を使う
- 出力時に正しく指定 ― Content-Typeで指定、DBは utf8mb4
// ✅ 入力文字エンコーディングの検証
if (!mb_check_encoding($_GET['name'], 'UTF-8')) {
die('不正な文字エンコーディングです');
}
-- ✅ MySQL は utf8mb4 を使う(utf8 では絵文字等が保存できない)
CREATE DATABASE myapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
// ✅ PDO接続時の文字コード指定
$dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4';
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 静的プレースホルダを使用
]);
おわりに
本記事では徳丸本をベースに、Webアプリケーション開発で押さえるべき脆弱性と対策を整理しました。
セキュリティバグをなくすことは開発者の責務です。
落とし穴の場所は事前に学習可能であり、本記事と原著がその一助となれば幸いです。
参考文献
徳丸 浩『体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 ― 脆弱性が生まれる原理と対策の実践』SBクリエイティブ、2018年