リンドくんたなべ先生、「SQLインジェクション」って何ですか?
たなべSQLインジェクションは、データベースを操作するSQL文を不正に改ざんする攻撃なんだ。
実は、Webアプリケーションの脆弱性の中でも特に危険度が高くて、OWASP Top 10にも常にランクインしているんだよ。
Webアプリケーション開発において、データベースとのやり取りは避けて通れません。
しかし、そこには「SQLインジェクション」という深刻なセキュリティリスクが潜んでいます。
SQLインジェクション攻撃が成功すると、以下のような被害が発生する可能性があります。
- 個人情報の漏洩 - ユーザーのメールアドレスやパスワードが盗まれる
- データの改ざん・削除 - 重要なデータが書き換えられたり消去されたりする
- 不正なログイン - 認証を突破されて管理者権限を奪われる
- サーバーの乗っ取り - 最悪の場合、サーバー全体が制御される
この記事では、セキュリティ学習を始めたばかりの方でも理解できるよう、SQLインジェクションの仕組みから具体的な防御策まで、段階的にわかりやすく解説していきます。
SQLインジェクションとは何か?
リンドくん「インジェクション」って注入って意味ですよね?何を注入するんですか?
たなべその通り!攻撃者が悪意のあるSQL文を注入することで、本来の処理を改ざんするんだ。
まずは具体的な例を見てみよう。
SQLインジェクションの基本概念
SQLインジェクションは、Webアプリケーションのデータベース操作において、ユーザー入力を適切に処理しないことで発生する脆弱性です。
通常、Webアプリケーションはユーザーからの入力を受け取り、それをもとにデータベースへ問い合わせを行います。この際、入力値をそのままSQL文に組み込んでしまうと、攻撃者が不正なSQL文を実行できてしまうのです。
脆弱なコードの例
以下は、SQLインジェクションの脆弱性を持つ典型的なログイン処理のコード例です。
脆弱なPHPコード
<?php
// ユーザーからの入力を取得
$username = $_POST['username'];
$password = $_POST['password'];
// 危険:ユーザー入力を直接SQL文に組み込んでいる
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
echo "ログイン成功";
} else {
echo "ログイン失敗";
}
?>このコードの何が問題なのでしょうか?
ユーザーが普通にusernameに「tanabe」、passwordに「mypassword」と入力すれば、以下のようなSQL文が生成されます。
SELECT * FROM users WHERE username = 'tanabe' AND password = 'mypassword'これは正常な動作です。しかし、攻撃者が以下のような入力をした場合はどうなるでしょうか?
- username:
admin' -- - password: (任意の値)
すると、生成されるSQL文は以下のようになります。
SELECT * FROM users WHERE username = 'admin' --' AND password = '任意の値'SQL文における--はコメントを意味します。つまり、--以降の部分は無視されるため、実際には以下のSQL文が実行されます。
SELECT * FROM users WHERE username = 'admin'パスワードのチェックが完全に無視され、攻撃者はパスワードなしでadminとしてログインできてしまうのです。
より深刻な攻撃例
SQLインジェクションは、単なる認証回避だけでなく、さらに深刻な被害をもたらす可能性があります。
データベースの内容を全て取得する攻撃
攻撃者が以下のような入力をした場合を考えてみましょう。
- username:
' UNION SELECT username, password FROM users --
生成されるSQL文は以下です。
SELECT * FROM users WHERE username = '' UNION SELECT username, password FROM users --' AND password = ''この攻撃により、データベース内のすべてのユーザー名とパスワードが取得されてしまいます。
データを削除する攻撃
さらに悪質な例として、以下のような入力も可能です。
- username:
'; DROP TABLE users; --
生成されるSQL文:
SELECT * FROM users WHERE username = ''; DROP TABLE users; --' AND password = ''この攻撃が成功すると、usersテーブルが完全に削除されてしまいます。
このように、SQLインジェクションは単なるログイン回避にとどまらず、データベース全体を破壊する可能性を秘めた非常に危険な脆弱性なのです。
SQLインジェクションの防御策 - プリペアドステートメント
リンドくんこんな攻撃、どうやって防げばいいんですか!?怖すぎます...
たなべ安心して!プリペアドステートメントという強力な防御方法があるんだ。
これを使えば、ユーザー入力が絶対にSQL文として解釈されなくなるんだよ。
プリペアドステートメントとは
プリペアドステートメント(準備された文)は、SQL文の構造とデータを完全に分離する仕組みです。
この方式では、以下の2段階でSQL文を実行します。
- SQL文のテンプレートを準備 - データが入る場所をプレースホルダー(
?や:nameなど)で表す - データをバインド - プレースホルダーにデータを安全に割り当てる
重要なのは、バインドされたデータは常に「データ」として扱われ、決してSQL文の一部として解釈されないということです。
PHPでのプリペアドステートメント実装
PDOを使用した安全なコード
<?php
// ユーザーからの入力を取得
$username = $_POST['username'];
$password = $_POST['password'];
try {
// PDOインスタンスの作成
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'dbuser', 'dbpass');
// プリペアドステートメントを準備
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
// パラメータをバインド
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':password', $password, PDO::PARAM_STR);
// クエリを実行
$stmt->execute();
if ($stmt->rowCount() > 0) {
echo "ログイン成功";
} else {
echo "ログイン失敗";
}
} catch (PDOException $e) {
// エラーハンドリング
error_log($e->getMessage());
echo "エラーが発生しました";
}
?>このコードでは、攻撃者がusernameにadmin' --と入力しても、それは単なる文字列として扱われます。
つまり、「admin' --」という名前のユーザーを検索するだけで、SQL文の構造を変えることはできません。
MySQLiを使用した実装
<?php
$username = $_POST['username'];
$password = $_POST['password'];
// MySQLiインスタンスの作成
$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'mydb');
// プリペアドステートメントを準備
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
// パラメータをバインド("ss"は両方とも文字列を意味する)
$stmt->bind_param("ss", $username, $password);
// クエリを実行
$stmt->execute();
// 結果を取得
$result = $stmt->get_result();
if ($result->num_rows > 0) {
echo "ログイン成功";
} else {
echo "ログイン失敗";
}
$stmt->close();
$mysqli->close();
?>他の言語でのプリペアドステートメント
Python(SQLiteの例)import sqlite3
username = request.form['username']
password = request.form['password']
conn = sqlite3.connect('mydb.db')
cursor = conn.cursor()
# プリペアドステートメントを使用
cursor.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(username, password)
)
result = cursor.fetchone()
if result:
print("ログイン成功")
else:
print("ログイン失敗")
conn.close()const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'dbuser',
password: 'dbpass',
database: 'mydb'
});
const username = req.body.username;
const password = req.body.password;
// プリペアドステートメントを使用
connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password],
(err, results) => {
if (err) throw err;
if (results.length > 0) {
console.log('ログイン成功');
} else {
console.log('ログイン失敗');
}
}
);String username = request.getParameter("username");
String password = request.getParameter("password");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb", "dbuser", "dbpass"
);
// プリペアドステートメントを使用
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
System.out.println("ログイン成功");
} else {
System.out.println("ログイン失敗");
}
rs.close();
stmt.close();
conn.close();どの言語でも基本的な考え方は同じです。SQL文のテンプレートとデータを分離し、データは常にデータとしてのみ扱うことがポイントです。
エスケープ処理による防御
リンドくんプリペアドステートメントが使えない場合はどうすればいいんですか?
たなべ古いシステムやレガシーコードでは、エスケープ処理を使う方法もあるよ。
ただし、プリペアドステートメントほど安全ではないから、できれば避けたい方法なんだけどね。
エスケープ処理とは
エスケープ処理は、特殊文字を無害化することで、SQL文の構造が変わらないようにする方法です。
具体的には、SQL文で特別な意味を持つ文字(シングルクォート'やダブルクォート"など)の前にバックスラッシュ\を付けて、ただの文字として扱うようにします。
PHPでのエスケープ処理
<?php
$username = $_POST['username'];
$password = $_POST['password'];
// MySQLi接続
$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'mydb');
// エスケープ処理
$username = $mysqli->real_escape_string($username);
$password = $mysqli->real_escape_string($password);
// SQL文を構築
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $mysqli->query($sql);
if ($result->num_rows > 0) {
echo "ログイン成功";
} else {
echo "ログイン失敗";
}
?>この方法では、攻撃者がadmin' --と入力しても、エスケープ処理によりadmin\' --となり、シングルクォートがただの文字として扱われます。
エスケープ処理の限界と注意点
エスケープ処理には、以下のような問題点があります。
問題① エスケープ漏れのリスク
すべての入力値に対して確実にエスケープ処理を適用する必要があり、一箇所でも漏れがあると脆弱性が残ります。
問題② 文字エンコーディングの問題
特定の文字エンコーディング(特にマルチバイト文字)では、エスケープ処理が正しく機能しない場合があります。
// 文字エンコーディングを明示的に設定
$mysqli->set_charset("utf8mb4");問題③ 数値型のパラメータ
数値型のパラメータでは、クォートで囲まないため、エスケープだけでは不十分です。
脆弱な例
$user_id = $_GET['id'];
$user_id = $mysqli->real_escape_string($user_id);
// クォートで囲んでいないため脆弱
$sql = "SELECT * FROM users WHERE id = $user_id";攻撃者がid=1 OR 1=1と入力すると、すべてのユーザー情報が取得されてしまいます。
安全な対策
$user_id = $_GET['id'];
// 数値であることを確認
if (!is_numeric($user_id)) {
die("Invalid ID");
}
$user_id = intval($user_id); // 整数に変換
$sql = "SELECT * FROM users WHERE id = $user_id";このように、エスケープ処理は補助的な手段と考えるべきで、可能な限りプリペアドステートメントを使用することを強く推奨します。
その他の重要な防御策
リンドくんプリペアドステートメント以外にも、やっておくべきことはありますか?
たなべ多層防御の考え方が重要なんだ。
プリペアドステートメントだけでなく、いくつかの対策を組み合わせることで、より堅牢なシステムになるよ。
入力値のバリデーション
ユーザー入力が期待される形式に合っているか、事前にチェックすることも重要です。
バリデーションの例
<?php
// ユーザーIDは数値のみを許可
if (!preg_match('/^[0-9]+$/', $_GET['user_id'])) {
die("Invalid user ID");
}
// ユーザー名は英数字とアンダースコアのみを許可
if (!preg_match('/^[a-zA-Z0-9_]+$/', $_POST['username'])) {
die("Invalid username format");
}
// メールアドレスの形式チェック
if (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
die("Invalid email format");
}
?>ただし、バリデーションだけではSQLインジェクションを完全には防げません。あくまでプリペアドステートメントと組み合わせて使用する補助的な手段です。
最小権限の原則
データベースユーザーには、必要最小限の権限のみを付与することが重要です。
権限設定の例(MySQL)-- アプリケーション用のユーザーを作成
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'strong_password';
-- 必要なテーブルに対してのみ、必要な操作を許可
GRANT SELECT, INSERT, UPDATE ON mydb.users TO 'webapp'@'localhost';
GRANT SELECT, INSERT ON mydb.orders TO 'webapp'@'localhost';
-- DROP、CREATEなどの危険な権限は付与しない
-- すべての権限を確認
SHOW GRANTS FOR 'webapp'@'localhost';この設定により、たとえSQLインジェクション攻撃を受けても、以下のような被害を軽減できます。
- テーブルの削除(DROP TABLE)ができない
- データベースの変更(ALTER)ができない
- 他のデータベースへのアクセスができない
エラーメッセージの適切な処理
詳細なエラーメッセージは、攻撃者にとって貴重な情報源になります。
脆弱な例
<?php
$result = mysqli_query($conn, $sql);
if (!$result) {
// データベース構造が露出してしまう
die("Query failed: " . mysqli_error($conn));
}
?>安全な例
<?php
$result = mysqli_query($conn, $sql);
if (!$result) {
// ログにのみ詳細を記録
error_log("Database error: " . mysqli_error($conn));
// ユーザーには汎用的なメッセージのみ表示
die("An error occurred. Please try again later.");
}
?>ORMの活用
ORM(Object-Relational Mapping)ツールを使用すると、SQLインジェクションのリスクを大幅に軽減できます。
Laravelの例(PHP)<?php
use App\Models\User;
// 安全:内部でプリペアドステートメントを使用
$user = User::where('username', $username)
->where('password', $password)
->first();
if ($user) {
echo "ログイン成功";
} else {
echo "ログイン失敗";
}
?>from myapp.models import User
# 安全:内部でプリペアドステートメントを使用
user = User.objects.filter(
username=username,
password=password
).first()
if user:
print("ログイン成功")
else:
print("ログイン失敗")ORMを使用すると、直接SQL文を書く機会が減るため、SQLインジェクションのリスクが大幅に低下します。ただし、生のSQLを書く機能もあるため、その際は注意が必要です。
ウェブアプリケーションファイアウォール(WAF)
WAFは、Webアプリケーションへの攻撃を検知・ブロックするセキュリティツールです。
WAFができることは以下です。
- SQLインジェクション攻撃のパターンを検知してブロック
- 不審なリクエストをログに記録
- 既知の攻撃パターンから防御
ただし、WAFは万能ではありません。アプリケーション側での適切な対策(プリペアドステートメントなど)が最も重要で、WAFはあくまで追加の防御層として考えるべきです。
まとめ
リンドくんSQLインジェクション、怖いけどちゃんと対策すれば防げるんですね!
たなべその通り!プリペアドステートメントを正しく使うことが何より大切なんだ。
それに加えて、入力値のバリデーションや最小権限の原則など、複数の対策を組み合わせることで、より安全なアプリケーションを作れるよ。
この記事では、SQLインジェクションの仕組みから具体的な防御策まで解説してきました。
重要なポイントをおさらいしましょう。
- SQLインジェクションは非常に危険な脆弱性 - データ漏洩、改ざん、削除など深刻な被害をもたらす
- プリペアドステートメントが最も効果的な対策 - SQL文とデータを完全に分離することで安全性を確保
- エスケープ処理は補助的な手段 - プリペアドステートメントが使えない場合の次善策
- 多層防御の考え方が重要 - バリデーション、最小権限、適切なエラー処理などを組み合わせる
- ORMの活用も有効 - ただし生SQLを書く際は注意が必要
SQLインジェクション対策は、Webアプリケーション開発における最も基本的かつ重要なセキュリティ対策の一つです。
開発の初期段階から、これらの対策を意識してコーディングする習慣を身につけることが大切です。
「自分のコードは大丈夫だろうか?」と少しでも不安に感じたら、それは非常に良い兆候です。
セキュリティ意識の高い開発者は、常に自分のコードを疑い、改善し続けます。
安全なデータベース操作の第一歩として、今日からプリペアドステートメントを徹底してみましょう!





