セキュリティ レッスン2
Webアプリの脆弱性
XSS、CSRF、SQLインジェクションの仕組みと対策を学ぼう
XSS(クロスサイトスクリプティング)
XSSとは、攻撃者が悪意のあるJavaScriptをWebページに注入し、 他のユーザーのブラウザで実行させる攻撃です。Cookie窃取、セッションハイジャック、 ページの改ざんなどの被害が発生します。
反射型XSS(Reflected)
URLパラメータに埋め込まれたスクリプトがそのままHTMLに出力される
格納型XSS(Stored)
データベースに保存されたスクリプトが他のユーザーに表示される
DOM-based XSS
クライアント側のJavaScriptが安全でない方法でDOMを操作する
// 危険な例:ユーザー入力をそのままHTMLに挿入
// 攻撃者が name に <script>alert('XSS')</script> を入力すると実行される
document.getElementById("greeting").innerHTML = "こんにちは、" + userName;
// 安全な方法1: textContent を使う(HTMLとして解釈されない)
document.getElementById("greeting").textContent = "こんにちは、" + userName;
// 安全な方法2: エスケープ関数を使う
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// Reactでは自動的にエスケープされるが、dangerouslySetInnerHTMLは危険
// 危険: <div dangerouslySetInnerHTML={{ __html: userInput }} />
// 安全: <div>{userInput}</div> ← 自動エスケープSQLインジェクション
SQLインジェクションとは、ユーザー入力をSQLクエリに直接埋め込むことで、 データベースを不正に操作される攻撃です。データの漏洩、改ざん、削除などの被害が起きます。
// 危険な例:ユーザー入力をそのままSQLに埋め込む
const query = "SELECT * FROM users WHERE email = '" + email + "'";
// 攻撃者が email に ' OR '1'='1 と入力すると:
// SELECT * FROM users WHERE email = '' OR '1'='1'
// → 全ユーザーのデータが漏洩!
// さらに危険: '; DROP TABLE users; -- と入力すると:
// SELECT * FROM users WHERE email = ''; DROP TABLE users; --'
// → テーブルが削除される!// 安全な方法: プリペアドステートメント(パラメータ化クエリ)
// Node.js + PostgreSQL の例
const result = await pool.query(
"SELECT * FROM users WHERE email = $1",
[email] // パラメータとして渡す(エスケープ自動化)
);
// Prisma(ORM)を使えば自動的に安全
const user = await prisma.user.findUnique({
where: { email: email },
});CSRF(クロスサイトリクエストフォージェリ)
CSRFは、ログイン中のユーザーに気づかれないうちに、 意図しないリクエストを送信させる攻撃です。例えば、罠サイトにアクセスしただけで 送金やパスワード変更が実行される可能性があります。
<!-- 攻撃者のサイトに仕込まれた罠 -->
<!-- ユーザーがログイン中の銀行サイトに自動送信される -->
<img src="https://bank.example.com/transfer?to=attacker&amount=100000" />
<!-- またはフォーム自動送信 -->
<form action="https://bank.example.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="100000" />
</form>
<script>document.forms[0].submit();</script>// 対策1: CSRFトークン
// サーバー側でランダムなトークンを生成し、フォームに埋め込む
// リクエスト時にトークンを検証する
// 対策2: SameSite Cookie
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
// 対策3: Originヘッダーの検証
app.post("/transfer", (req, res) => {
const origin = req.headers.origin;
if (origin !== "https://bank.example.com") {
return res.status(403).json({ error: "不正なリクエスト" });
}
// 処理を続行...
});その他の主要な脆弱性
パストラバーサル
ファイルパスを操作してサーバー上の任意のファイルにアクセスする攻撃
// 危険: ユーザー入力をファイルパスにそのまま使う
// GET /file?name=../../../etc/passwd
const filePath = path.join(__dirname, "uploads", req.query.name);
// 安全: パスを正規化して検証
const safePath = path.resolve(__dirname, "uploads", req.query.name);
if (!safePath.startsWith(path.resolve(__dirname, "uploads"))) {
return res.status(403).send("不正なパス");
}オープンリダイレクト
リダイレクト先URLを操作してフィッシングサイトに誘導する攻撃
// 危険: リダイレクト先を検証しない
// /login?redirect=https://evil.com
res.redirect(req.query.redirect);
// 安全: 許可リストで検証
const allowed = ["/dashboard", "/profile", "/settings"];
const target = req.query.redirect || "/dashboard";
if (allowed.includes(target)) {
res.redirect(target);
}入力バリデーションの基本
すべてのセキュリティ対策の基本は入力のバリデーションです。 クライアント側とサーバー側の両方で検証を行いましょう。
// サーバー側バリデーションの例
function validateInput(data) {
const errors = [];
// 型チェック
if (typeof data.email !== "string") {
errors.push("メールアドレスが不正です");
}
// フォーマットチェック
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
errors.push("メールアドレスの形式が不正です");
}
// 長さチェック
if (data.name.length > 100) {
errors.push("名前は100文字以内にしてください");
}
// サニタイズ(不要な文字を除去)
data.name = data.name.trim();
return { isValid: errors.length === 0, errors };
}まとめ
- XSS は出力のエスケープとCSP(Content Security Policy)で防ぐ
- SQLインジェクションはプリペアドステートメントやORMで防ぐ
- CSRF はトークンとSameSite Cookieで防ぐ
- ユーザー入力は常に信頼せず、サーバー側で必ずバリデーションする
- Reactなどのフレームワークは自動エスケープ機能を持つが、過信は禁物