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 WAF | CDN統合、無料プランあり |
| AWS WAF | AWS統合、マネージドルール |
| 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特殊文字のエスケープ・アンエスケープをブラウザ上で実行できます。