<CodeLearn/>
セキュリティ レッスン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などのフレームワークは自動エスケープ機能を持つが、過信は禁物