総合演習:セキュリティ監査レポート
ストーリー
「最終ミッションだ」高橋さんが真剣な表情で言う。
「ここにサンプルアプリケーションのコードがある。 これまで学んだ全ての知識を使って、セキュリティ監査レポートを作成してくれ」
「監査レポート......実際の業務と同じ形式ですか?」
「そうだ。脆弱性の特定、修正、認証フローの確認、ヘッダーの検証、 そして報告書の作成。5つのパートに分けて進めよう」
サンプルアプリケーション
以下のコードを対象にセキュリティ監査を行ってください。
typescript
// app.ts
import express from 'express';
import cors from 'cors';
import { pool } from './db';
import crypto from 'crypto';
const app = express();
app.use(cors());
app.use(express.json());
const JWT_SECRET = 'my-app-secret-2025';
// ユーザー登録
app.post('/api/register', async (req, res) => {
const { name, email, password } = req.body;
const hash = crypto.createHash('md5').update(password).digest('hex');
await pool.query(
`INSERT INTO users (name, email, password_hash) VALUES ('${name}', '${email}', '${hash}')`
);
res.json({ success: true, message: `${name}さん、登録完了!` });
});
// ログイン
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const hash = crypto.createHash('md5').update(password).digest('hex');
const result = await pool.query(
`SELECT * FROM users WHERE email = '${email}' AND password_hash = '${hash}'`
);
if (result.rows.length === 0) {
return res.status(401).json({ error: `${email} のログインに失敗しました` });
}
const user = result.rows[0];
const token = user.id + ':' + Date.now();
res.json({ token, user });
});
// プロフィール取得
app.get('/api/profile/:id', async (req, res) => {
const result = await pool.query(
`SELECT * FROM users WHERE id = ${req.params.id}`
);
res.json(result.rows[0]);
});
// 商品検索
app.get('/api/products/search', async (req, res) => {
const { q, sort } = req.query;
let query = `SELECT * FROM products WHERE name LIKE '%${q}%'`;
if (sort) {
query += ` ORDER BY ${sort}`;
}
try {
const result = await pool.query(query);
res.json(result.rows);
} catch (error) {
res.status(500).json({ error: error.message, stack: error.stack });
}
});
// コメント投稿
app.post('/api/comments', async (req, res) => {
const { postId, body } = req.body;
await pool.query(
'INSERT INTO comments (post_id, body) VALUES ($1, $2)',
[postId, body]
);
res.json({ success: true });
});
// コメント表示
app.get('/api/posts/:id/comments', async (req, res) => {
const result = await pool.query(
'SELECT * FROM comments WHERE post_id = $1',
[req.params.id]
);
let html = '<div>';
for (const c of result.rows) {
html += `<p>${c.body}</p>`;
}
html += '</div>';
res.send(html);
});
// 管理者API
app.delete('/api/admin/users/:id', async (req, res) => {
await pool.query(`DELETE FROM users WHERE id = ${req.params.id}`);
res.json({ deleted: true });
});
// エラーハンドラ
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message, stack: err.stack });
});
app.listen(3000);Part 1: コード脆弱性の特定(10分)
上記のコードから全ての脆弱性を洗い出し、一覧にしてください。
報告テンプレート
| # | 脆弱性の種類 | 重要度 | 該当箇所 | 影響 |
<details>
<summary>解答</summary>
| # | 脆弱性の種類 | 重要度 | 該当箇所 | 影響 |
|---|---|---|---|---|
| 1 | ハードコードされたJWT秘密鍵 | CRITICAL | 11行目 | トークン偽造が可能 |
| 2 | 弱いハッシュアルゴリズム(MD5) | CRITICAL | 16行目、29行目 | パスワードの復元が容易 |
| 3 | SQLインジェクション(登録) | CRITICAL | 18-19行目 | データの窃取・改ざん |
| 4 | SQLインジェクション(ログイン) | CRITICAL | 31-32行目 | 認証バイパス |
| 5 | SQLインジェクション(プロフィール) | CRITICAL | 43行目 | データ漏洩 |
| 6 | SQLインジェクション(商品検索) | CRITICAL | 49行目 | データの窃取 |
| 7 | SQLインジェクション(管理者API) | CRITICAL | 76行目 | データの削除 |
| 8 | 予測可能なトークン | HIGH | 38行目 | セッションハイジャック |
| 9 | パスワードハッシュの漏洩 | HIGH | 40行目(user全体を返却) | オフラインクラック |
| 10 | 格納型XSS | HIGH | 69行目 | Cookie窃取、セッション乗っ取り |
| 11 | 認証なしの管理者API | CRITICAL | 75行目 |