セキュリティ レッスン5
セキュリティ総合演習
脆弱性を見つけて修正するスキルを身につけよう
演習1: XSS脆弱性を修正しよう
以下のコードにはXSS脆弱性があります。問題を特定し、安全なコードに修正してください。
// 脆弱なコード: コメント投稿機能
app.get("/comments", async (req, res) => {
const comments = await db.comment.findMany();
let html = "<h1>コメント一覧</h1>";
for (const comment of comments) {
// 問題: ユーザー入力をそのままHTMLに埋め込んでいる
html += "<div class='comment'>";
html += "<strong>" + comment.author + "</strong>";
html += "<p>" + comment.body + "</p>";
html += "</div>";
}
res.send(html);
});// 修正版: HTMLエスケープを適用
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
app.get("/comments", async (req, res) => {
const comments = await db.comment.findMany();
let html = "<h1>コメント一覧</h1>";
for (const comment of comments) {
html += "<div class='comment'>";
html += "<strong>" + escapeHtml(comment.author) + "</strong>";
html += "<p>" + escapeHtml(comment.body) + "</p>";
html += "</div>";
}
res.send(html);
});
// さらに良い方法: Reactなどのフレームワークを使えば自動エスケープ
// CSP(Content-Security-Policy)ヘッダーも設定する
// Content-Security-Policy: default-src 'self'; script-src 'self'演習2: SQLインジェクションを修正しよう
以下のログイン処理にはSQLインジェクション脆弱性があります。安全な実装に書き換えてください。
// 脆弱なコード: ログイン処理
app.post("/login", async (req, res) => {
const { email, password } = req.body;
// 問題: 文字列結合でSQLを構築
const query = "SELECT * FROM users WHERE email = '" + email
+ "' AND password = '" + password + "'";
const user = await db.raw(query);
if (user.length > 0) {
res.json({ message: "ログイン成功" });
} else {
res.status(401).json({ error: "認証失敗" });
}
});
// 攻撃: email に ' OR '1'='1' -- と入力すると全ユーザーでログイン可能// 修正版: プリペアドステートメント + パスワードハッシュ
import bcrypt from "bcrypt";
app.post("/login", async (req, res) => {
const { email, password } = req.body;
// 入力バリデーション
if (!email || !password) {
return res.status(400).json({ error: "入力が不足しています" });
}
// パラメータ化クエリ(SQLインジェクション対策)
const user = await db.query(
"SELECT * FROM users WHERE email = $1",
[email]
);
if (user.rows.length === 0) {
return res.status(401).json({ error: "認証失敗" });
}
// bcrypt でパスワード検証(平文比較はNG)
const isValid = await bcrypt.compare(password, user.rows[0].password);
if (isValid) {
// セッション or JWTを発行
res.json({ message: "ログイン成功" });
} else {
res.status(401).json({ error: "認証失敗" });
}
});演習3: 安全なAPIを設計しよう
以下の要件を満たす安全なユーザー情報更新APIを設計してください。
- 認証されたユーザーのみアクセス可能
- 自分のデータのみ更新可能(認可チェック)
- 入力値のバリデーション
- レートリミット
import { rateLimit } from "express-rate-limit";
import jwt from "jsonwebtoken";
// レートリミット: 15分に100リクエストまで
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: "リクエスト回数制限を超えました" },
});
// 認証ミドルウェア
function authenticate(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: "認証が必要です" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: "トークンが無効です" });
}
}
// バリデーション
function validateUpdate(data) {
const errors = [];
if (data.name && (typeof data.name !== "string" || data.name.length > 50)) {
errors.push("名前は50文字以内の文字列にしてください");
}
if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.push("メールアドレスの形式が不正です");
}
return errors;
}
// 安全なAPI
app.put("/api/users/:id", limiter, authenticate, async (req, res) => {
// 認可: 自分のデータのみ更新可能
if (req.user.id !== parseInt(req.params.id)) {
return res.status(403).json({ error: "権限がありません" });
}
// バリデーション
const errors = validateUpdate(req.body);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// 更新(ORMで安全にクエリ)
const updated = await prisma.user.update({
where: { id: req.user.id },
data: { name: req.body.name, email: req.body.email },
});
res.json(updated);
});開発時のセキュリティチェックリスト
入力・出力
- すべてのユーザー入力をサーバー側でバリデーション
- HTMLの出力にはエスケープを適用
- SQL はプリペアドステートメントを使用
認証・認可
- パスワードは bcrypt でハッシュ化
- セッションCookie に HttpOnly, Secure, SameSite を設定
- APIに認証・認可チェックを実装
通信・インフラ
- HTTPS を強制
- CORS を適切に設定
- レートリミットを導入
- 環境変数でシークレットを管理
まとめ
- 脆弱なコードを見つけたら、攻撃シナリオを想定して修正する
- XSS にはエスケープ、SQLインジェクションにはパラメータ化クエリで対応
- 認証・認可・バリデーション・レートリミットを組み合わせて多層防御する
- セキュリティは機能追加後ではなく、設計段階から考慮する
- 定期的にセキュリティチェックリストを確認し、漏れがないか検証する