「SQLインジェクション」という言葉を聞いたことはありますか?「なんか危険そうだけど、詳しくは分からない…」という方も多いのではないでしょうか。
実は、SQLインジェクションはWebアプリケーションの最も危険な脆弱性の一つです。適切な対策をしないと、顧客情報の流出や、サイトの改ざんなど、深刻な被害を受ける可能性があります。
この記事では、SQLインジェクションの基本的な仕組みから、具体的な対策方法まで、初心者の方でも分かりやすく解説していきます。読み終わる頃には、あなたのWebアプリも安全性の高いシステムに変身しているはずです!
SQLインジェクションとは?基本的な仕組み

攻撃の基本原理
SQLインジェクションとは、Webアプリケーションの入力フォームやURLに、悪意のあるSQL文を混入させる攻撃手法のことです。
正常な処理の流れ:
- ユーザーがフォームに情報を入力
- アプリがその情報をデータベースに問い合わせ
- 正しい結果が返される
攻撃時の流れ:
- 攻撃者が特殊な文字列を入力
- 意図しないSQL文が実行される
- 機密情報が漏洩したり、データが改ざんされる
具体的な攻撃例
ログイン画面での攻撃:
正常な入力の場合:
- ユーザー名:
tanaka
- パスワード:
password123
攻撃者の入力:
- ユーザー名:
admin' --
- パスワード:
(何でも)
この場合、以下のようなSQL文が実行されてしまいます:
SELECT * FROM users WHERE username = 'admin' --' AND password = '(何でも)'
--
はSQLのコメント記号なので、パスワードチェックが無効化され、管理者権限で不正ログインされてしまうのです。
被害の深刻度
SQLインジェクション攻撃が成功すると、以下のような被害が発生する可能性があります:
情報漏洩:
- 顧客の個人情報
- クレジットカード情報
- 企業の機密データ
システム破壊:
- データベースの削除
- ウェブサイトの改ざん
- サービス停止
実際に大手企業でも被害事例が多数報告されており、決して他人事ではありません。次章では、攻撃手法を詳しく見ていきましょう。
主な攻撃手法と具体例
Union-based攻撃
仕組み: UNION文を使って、本来のクエリ結果に攻撃者が欲しい情報を追加で取得する手法です。
攻撃例: 商品検索で ' UNION SELECT username, password FROM users --
を入力すると:
SELECT product_name, price FROM products WHERE category = ''
UNION SELECT username, password FROM users --'
商品情報と一緒に、ユーザーの認証情報も表示されてしまいます。
Boolean-based攻撃
特徴: 真偽値(正しい・間違い)の応答の違いを利用して、少しずつ情報を抜き取る手法です。
攻撃の流れ:
admin' AND 1=1 --
→ 正常応答(真)admin' AND 1=2 --
→ エラー応答(偽)admin' AND LENGTH(password)>5 --
→ 応答から文字数を推測- 文字を一つずつ特定していく
Time-based攻撃
手法: 応答時間の違いを利用して情報を取得します。
攻撃例:
admin' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) --
パスワードの1文字目が’a’なら5秒待機、違えばすぐ応答することで、文字を特定していきます。
Error-based攻撃
原理: 意図的にエラーを発生させ、エラーメッセージから情報を取得する手法です。
攻撃の具体例:
' AND EXTRACTVALUE(1, CONCAT('~', (SELECT user()), '~')) --
データベースのユーザー名がエラーメッセージに含まれて表示されてしまいます。
これらの攻撃手法を理解することで、適切な対策の重要性が分かります。次は、具体的な脆弱性のあるコード例を見ていきましょう。
脆弱なコードの例と問題点

PHP での危険な実装例
最も危険なパターン:
<?php
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysql_query($sql);
?>
問題点:
- ユーザー入力をそのままSQL文に埋め込み
- エスケープ処理が一切なし
- SQLインジェクション攻撃に完全に無防備
Java での問題のあるコード
Statement使用の危険例:
String sql = "SELECT * FROM products WHERE category = '" + category + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
何が危険か:
- 文字列連結でSQL文を構築
- 入力値の検証なし
- パラメータバインディング未使用
JavaScript (Node.js) での脆弱性
危険なクエリ実行:
const query = `SELECT * FROM orders WHERE user_id = ${userId}`;
db.query(query, (err, results) => {
// 処理
});
リスク要因:
- テンプレートリテラルでの直接埋め込み
- 型チェックなし
- サニタイゼーション未実装
Python での問題例
format関数による危険な実装:
query = "SELECT * FROM customers WHERE email = '{}'".format(email)
cursor.execute(query)
脆弱性のポイント:
- format関数での直接展開
- パラメータ化クエリ未使用
- 入力検証の欠如
共通する問題点
これらのコードに共通する問題は以下の通りです:
技術的な問題:
- 動的SQL文の不適切な構築
- パラメータバインディングの未使用
- エスケープ処理の不備
設計上の問題:
- 入力値の検証不足
- セキュリティ意識の欠如
- 最小権限の原則違反
運用上の問題:
- コードレビューの不備
- セキュリティテストの未実施
- 開発者の知識不足
こうした脆弱性を根本的に解決するために、次章では効果的な対策方法を解説していきます。
効果的な対策方法
パラメータ化クエリ(プリペアドステートメント)
最も重要で効果的な対策がパラメータ化クエリの使用です。
PHP(PDO)での正しい実装:
<?php
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
$result = $stmt->fetchAll();
?>
Java(PreparedStatement):
String sql = "SELECT * FROM products WHERE category = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, category);
ResultSet rs = pstmt.executeQuery();
メリット:
- SQL文と データが明確に分離される
- データベースエンジンが安全に処理
- パフォーマンスも向上
入力値検証とサニタイゼーション
ホワイトリスト方式での検証:
<?php
// 数値のみを許可
if (!is_numeric($user_id)) {
throw new InvalidArgumentException("Invalid user ID");
}
// 特定の値のみを許可
$allowed_categories = ['electronics', 'books', 'clothes'];
if (!in_array($category, $allowed_categories)) {
throw new InvalidArgumentException("Invalid category");
}
?>
文字長制限:
// Node.js での例
function validateInput(input) {
if (typeof input !== 'string' || input.length > 100) {
throw new Error('Invalid input');
}
return input.trim();
}
最小権限の原則
データベースユーザーの権限制限:
アプリ用ユーザーの設定:
-- 読み取り専用ユーザー
CREATE USER 'app_read'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON myapp.products TO 'app_read'@'localhost';
-- 一般アプリユーザー
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE ON myapp.orders TO 'app_user'@'localhost';
-- DELETE権限は付与しない
メリット:
- 攻撃成功時の被害を最小限に抑制
- 意図しない操作の防止
- セキュリティインシデント時の影響範囲限定
エラーハンドリングの適切な実装
危険なエラー表示:
// NG例:詳細すぎるエラー情報
catch (Exception $e) {
echo "Database error: " . $e->getMessage();
}
安全なエラー処理:
// OK例:一般的なエラーメッセージ
catch (Exception $e) {
error_log("Database error: " . $e->getMessage());
echo "An error occurred. Please try again later.";
}
これらの対策を組み合わせることで、SQLインジェクション攻撃を効果的に防ぐことができます。次は、開発環境でのテスト方法を見ていきましょう。
セキュリティテストと脆弱性検査
手動テストの基本手法
基本的な入力パターン:
シングルクォートテスト:
- 入力値:
'
- 期待結果:エラーが発生せず、正常に処理される
SQLキーワードテスト:
- 入力値:
' OR '1'='1
- 期待結果:意図しない結果が返されない
コメント記号テスト:
- 入力値:
admin' --
- 期待結果:認証が突破されない
自動化ツールの活用
SQLMap(オープンソース):
# 基本的な使用例
sqlmap -u "http://example.com/login.php" --data="username=test&password=test"
# Cookieを使用した検査
sqlmap -u "http://example.com/profile.php" --cookie="session=abc123"
OWASP ZAP:
- GUI操作で簡単に脆弱性スキャン
- Webアプリの全ページを自動検査
- 詳細なレポート機能
Burp Suite:
- プロ向けの高機能ツール
- インターセプト機能でリクエスト改ざん
- 手動テストとの組み合わせが効果的
CI/CDパイプラインでの自動検査
GitHub Actionsでの実装例:
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run OWASP ZAP
uses: zaproxy/action-full-scan@v0.4.0
with:
target: 'http://localhost:3000'
検査項目の網羅:
- SQLインジェクション
- XSS(クロスサイトスクリプティング)
- CSRF(クロスサイトリクエストフォージェリ)
- 認証・認可の不備
コードレビューでのチェックポイント
重点確認項目:
- SQL文の構築方法
- パラメータ化クエリの使用確認
- 文字列連結の有無をチェック
- 入力値検証
- バリデーション処理の実装状況
- ホワイトリスト方式の採用
- エラーハンドリング
- 詳細エラー情報の非表示
- ログ出力の適切性
- 権限設定
- データベース接続ユーザーの権限
- 最小権限原則の遵守
定期的なテストと継続的な改善により、セキュリティレベルを維持向上できます。次は、具体的なフレームワーク別の対策を見ていきましょう。
フレームワーク別対策の実装例

Laravel(PHP)での実装
Eloquent ORMを使用:
<?php
// 安全な実装例
class UserController extends Controller
{
public function login(Request $request)
{
// バリデーション
$request->validate([
'email' => 'required|email|max:255',
'password' => 'required|min:8'
]);
// Eloquentを使用(自動的にパラメータ化される)
$user = User::where('email', $request->email)->first();
if ($user && Hash::check($request->password, $user->password)) {
Auth::login($user);
return redirect()->dashboard();
}
return back()->withErrors(['Invalid credentials']);
}
}
?>
クエリビルダーでの安全な実装:
<?php
// パラメータバインディングを使用
$users = DB::table('users')
->where('status', '=', $status)
->where('created_at', '>', $date)
->get();
// 名前付きバインディング
$results = DB::select('SELECT * FROM users WHERE id = :id', ['id' => $userId]);
?>
Django(Python)での対策
Django ORMの活用:
from django.contrib.auth import authenticate
from django.db import models
class UserManager:
def get_user_by_email(self, email):
# ORMは自動的に安全なクエリを生成
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None
def search_products(self, category, price_min):
# パラメータは自動的にエスケープされる
return Product.objects.filter(
category=category,
price__gte=price_min
)
生SQLを使う場合の安全な書き方:
from django.db import connection
def get_custom_data(user_id, status):
with connection.cursor() as cursor:
# パラメータ化クエリを使用
cursor.execute(
"SELECT * FROM orders WHERE user_id = %s AND status = %s",
[user_id, status]
) return cursor.fetchall()
Express.js(Node.js)での実装
Sequelize ORMの使用:
const { User, Product } = require('./models');
// 安全なユーザー検索
async function getUserByEmail(email) {
return await User.findOne({
where: { email: email } // 自動的にエスケープされる
});
}
// 複雑な条件での検索
async function searchProducts(category, minPrice) {
return await Product.findAll({
where: {
category: category,
price: { [Op.gte]: minPrice }
}
});
}
生クエリでの安全な実装:
const mysql = require('mysql2/promise');
async function getOrderHistory(userId, limit) {
const connection = await mysql.createConnection(dbConfig);
// パラメータ化クエリを使用
const [rows] = await connection.execute(
'SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[userId, limit]
);
await connection.end();
return rows;
}
Spring Boot(Java)での対策
JPA Repository の活用:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// メソッド名からクエリが自動生成(安全)
List<User> findByEmailAndStatus(String email, String status);
// @Queryアノテーションでも安全
@Query("SELECT u FROM User u WHERE u.createdAt > :date AND u.role = :role")
List<User> findRecentUsersByRole(@Param("date") LocalDateTime date,
@Param("role") String role);
}
JDBC Template での実装:
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Order> getOrdersByUser(Long userId, String status) {
String sql = "SELECT * FROM orders WHERE user_id = ? AND status = ?";
return jdbcTemplate.query(sql,
new Object[]{userId, status},
new OrderRowMapper());
}
}
各フレームワークには、SQLインジェクションを防ぐための強力な機能が標準で用意されています。これらを正しく使用することで、安全なアプリケーションを構築できます。
よくある疑問Q&A
Q1. ORMを使えば完全に安全ですか?
A. ORMを使っても、間違った使い方をすると脆弱性が生まれる可能性があります。
危険な例:
# Django での危険な使用例
User.objects.extra(where=["name = '%s'" % user_input]) # NG
ORMでも生SQLを直接書く機能があるため、基本原則を理解した上で使用することが大切です。
Q2. 既存システムの改修はどこから始めるべきですか?
A. 重要度と影響度の高い部分から優先的に対応しましょう。
改修の優先順位:
- ログイン・認証機能
- 個人情報を扱う機能
- 管理者機能
- 一般的な検索・表示機能
リスクの高い部分を特定して、段階的に改善していくのが現実的です。
Q3. パフォーマンスへの影響はありますか?
A. パラメータ化クエリは、パフォーマンスにも良い影響を与えます。
理由:
- データベースがクエリ実行プランをキャッシュできる
- 同じSQL文の再利用が効率的
- セキュリティと速度の両方が向上
一時的に実装コストはかかりますが、長期的にはメリットの方が大きいです。
Q4. 入力値検証だけでは不十分ですか?
A. 入力値検証は多層防御の一部として重要ですが、それだけでは不十分です。
必要な対策の組み合わせ:
- パラメータ化クエリ(最重要)
- 入力値検証
- 最小権限の原則
- エラーハンドリング
複数の対策を組み合わせることで、より堅牢なセキュリティを実現できます。
Q5. 古いシステムでパラメータ化クエリが使えない場合は?
A. エスケープ処理を適切に実装し、段階的に改修を進めましょう。
暫定対策例:
<?php
// PHP での暫定対策
$username = mysqli_real_escape_string($connection, $username);
$sql = "SELECT * FROM users WHERE username = '$username'";
?>
ただし、これは一時的な対策です。可能な限り早期にパラメータ化クエリへの移行を計画しましょう。
まとめ
今回はSQLインジェクション対策について、基本から実践まで詳しく解説しました。
重要なポイントをおさらい:
- 脅威の理解:SQLインジェクションは深刻な被害をもたらす危険な攻撃
- 根本対策:パラメータ化クエリの使用が最も効果的
- 多層防御:入力検証・権限制限・エラー処理の組み合わせ
- 継続的改善:定期的なテストとコードレビューの実施
- フレームワーク活用:各フレームワークの安全な機能を正しく使用
SQLインジェクション対策は、一度実装すれば終わりではありません。新機能の追加や要件変更の際にも、常にセキュリティを意識した開発を心がけることが大切です。
今日からさっそく、あなたのWebアプリケーションのセキュリティ状況を見直してみてください。小さな改善の積み重ねが、大きなセキュリティインシデントを防ぐことにつながります。
安全で信頼性の高いWebアプリケーションを目指して、一緒にセキュリティ対策を強化していきましょう!
コメント