SQLインジェクションとは?

SQLインジェクションは、ユーザー入力をSQLクエリに直接埋め込むことで、攻撃者が意図しないSQLを実行させる脆弱性です。OWASP Top 10に常にランクインする、最も危険なWeb脆弱性の1つです。

被害の例

被害説明
データ漏洩顧客情報、クレジットカード情報の流出
データ改ざん商品価格、権限情報の書き換え
データ削除テーブルの削除、データベースの破壊
認証バイパスログイン認証の迂回
サーバー侵害OS コマンドの実行(一部のDB)

SQLインジェクションの仕組み

脆弱なコードの例

// NG: ユーザー入力を直接SQLに埋め込み
app.get('/users', async (req, res) => {
  const username = req.query.username;
  const query = `SELECT * FROM users WHERE username = '${username}'`;
  const result = await db.query(query);
  res.json(result);
});

攻撃の例

正常なリクエスト:

GET /users?username=tanaka
SQL: SELECT * FROM users WHERE username = 'tanaka'

攻撃リクエスト:

GET /users?username=' OR '1'='1
SQL: SELECT * FROM users WHERE username = '' OR '1'='1'
→ 全ユーザーの情報が返される!

さらに危険な攻撃:

GET /users?username='; DROP TABLE users; --
SQL: SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
→ usersテーブルが削除される!

ログイン認証のバイパス

// 脆弱なログイン処理
const query = `
  SELECT * FROM users
  WHERE email = '${email}' AND password = '${password}'
`;

攻撃:

email: [email protected]
password: ' OR '1'='1

SQL: SELECT * FROM users
     WHERE email = '[email protected]' AND password = '' OR '1'='1'
→ 条件が常に真になり、管理者としてログインできる

対策1: プリペアドステートメント(最重要)

プリペアドステートメント(パラメータ化クエリ) は、SQLインジェクション対策の最も効果的で基本的な方法です。

仕組み

1. SQL テンプレートをDBに送信(プレースホルダ付き)
   SELECT * FROM users WHERE username = ?

2. パラメータを別途送信
   ["tanaka"]

3. DBがSQL構造とデータを分離して処理
   → データがSQLの一部として解釈されない

Node.js + PostgreSQL (pg)

// OK: プリペアドステートメント
app.get('/users', async (req, res) => {
  const username = req.query.username;
  const query = 'SELECT * FROM users WHERE username = $1';
  const result = await db.query(query, [username]);
  res.json(result.rows);
});

// 複数パラメータ
const query = 'SELECT * FROM users WHERE email = $1 AND status = $2';
const result = await db.query(query, [email, 'active']);

Node.js + MySQL (mysql2)

// OK: プリペアドステートメント
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ?',
  [username]
);

// IN句
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE id IN (?, ?, ?)',
  [1, 2, 3]
);

Python + SQLAlchemy

# OK: パラメータバインディング
from sqlalchemy import text

result = db.execute(
    text("SELECT * FROM users WHERE username = :username"),
    {"username": username}
)

Java + JDBC

// OK: PreparedStatement
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();

対策2: ORM を使う

ORM(Object-Relational Mapping)は内部でプリペアドステートメントを使用するため、SQLインジェクションのリスクが大幅に低減されます。

Prisma

// OK: Prisma はパラメータを自動的にエスケープ
const user = await prisma.user.findMany({
  where: {
    username: req.query.username,
  },
});

// OK: 検索条件
const users = await prisma.user.findMany({
  where: {
    OR: [
      { name: { contains: searchTerm } },
      { email: { contains: searchTerm } },
    ],
  },
});

Drizzle ORM

import { eq } from 'drizzle-orm';

// OK: 安全なクエリ
const result = await db
  .select()
  .from(users)
  .where(eq(users.username, req.query.username));

ORMでも危険なケース

// NG: 生のSQLを使う場合は注意
const result = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE username = '${username}'`
);

// OK: パラメータ付きの生SQL
const result = await prisma.$queryRaw`
  SELECT * FROM users WHERE username = ${username}
`;

対策3: 入力値のバリデーション

プリペアドステートメントに加えて、入力値のバリデーションも行いましょう。

import { z } from 'zod';

// スキーマ定義
const searchSchema = z.object({
  username: z.string()
    .min(1)
    .max(50)
    .regex(/^[a-zA-Z0-9_-]+$/),  // 英数字とハイフン・アンダースコアのみ
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

// バリデーション
app.get('/users', async (req, res) => {
  const parsed = searchSchema.safeParse(req.query);
  if (!parsed.success) {
    return res.status(400).json({ error: parsed.error.issues });
  }

  const { username, page, limit } = parsed.data;
  // 安全なクエリ実行...
});

対策4: 最小権限の原則

データベースユーザーに必要最小限の権限だけを付与します。

-- アプリケーション用ユーザーを作成
CREATE USER app_user WITH PASSWORD 'secure_password';

-- 必要なテーブルへの権限のみ付与
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT ON products TO app_user;

-- DROP, CREATE, ALTER は付与しない
-- → 万が一のインジェクションでもテーブル削除は防げる

対策5: WAF(Web Application Firewall)

WAFはリクエストを検査し、SQLインジェクションのパターンを検出してブロックします。

WAFサービス特徴
Cloudflare WAFCDN統合、無料プランあり
AWS WAFAWS統合、マネージドルール
ModSecurityオープンソース、Nginx/Apache対応

WAFは多層防御の1つであり、プリペアドステートメントの代替にはなりません。

SQLインジェクションの種類

1. クラシック(In-band)

エラーメッセージやレスポンスで直接データが返される。

GET /users?id=1 UNION SELECT username, password FROM admin_users --

2. ブラインド(推測型)

レスポンスの違い(True/False、時間差)でデータを推測する。

-- Boolean-based
GET /users?id=1 AND (SELECT COUNT(*) FROM users) > 10

-- Time-based
GET /users?id=1; IF (SELECT COUNT(*) FROM users) > 10 WAITFOR DELAY '0:0:5'

3. Out-of-band

DNS やHTTPリクエストを通じてデータを外部に送出する。

4. 二次インジェクション

一度保存されたデータが別のSQLクエリで使われる際に発生。

// ユーザー登録時(安全にエスケープされて保存)
// name: "O'Malley" → DBに保存

// 後のクエリで未対策のまま使用(NG)
const query = `SELECT * FROM orders WHERE customer_name = '${user.name}'`;
// SQL: ... WHERE customer_name = 'O'Malley' → エラーまたは脆弱性

テスト方法

手動テスト

以下の文字列を入力フィールドに試してみましょう:

'
" OR "1"="1
' OR '1'='1' --
1; DROP TABLE test --
' UNION SELECT null, null --

自動テストツール

# sqlmap(SQLインジェクション自動検出ツール)
sqlmap -u "https://example.com/users?id=1" --batch

# OWASP ZAP
# GUIツールで総合的なセキュリティスキャン

ユニットテスト

describe('SQLインジェクション対策', () => {
  it('SQLインジェクションの文字列を安全に処理できる', async () => {
    const maliciousInput = "' OR '1'='1";
    const result = await getUser(maliciousInput);
    expect(result).toEqual([]);  // 結果が空であること
  });

  it('シングルクォートを含む正常な入力を処理できる', async () => {
    // O'Malley のような正当な名前も処理できること
    const result = await getUser("O'Malley");
    expect(result).toBeDefined();
  });
});

チェックリスト

対策優先度状態
プリペアドステートメントの使用必須
ORM の使用推奨
入力値のバリデーション必須
DB ユーザーの最小権限必須
エラーメッセージの非表示必須
WAFの導入推奨
定期的なセキュリティスキャン推奨

まとめ

SQLインジェクション対策で最も重要なのはプリペアドステートメントです。ORM を使えば多くの場合自動的に対策されますが、生SQLを書く場合は必ずパラメータバインディングを使いましょう。

多層防御の考え方で、バリデーション、最小権限、WAF を組み合わせることが重要です。


ユーザー入力のエスケープ処理の確認には、AssistyのHTMLエスケープツールが便利です。HTML特殊文字のエスケープ・アンエスケープをブラウザ上で実行できます。