Webアプリケーションにおいて、SQLインジェクションは最も危険で頻繁に悪用される脆弱性の一つです。OWASPのTop 10でも常に上位にランクインしており、データベースを扱うすべてのWeb開発者が理解しておくべきセキュリティ脅威です。
この記事では、SQLインジェクションとは何か、具体的な攻撃シナリオ、パラメータ化クエリによる対策、そしてOSコマンドインジェクションやLDAPインジェクションなどの注入攻撃の防御方法まで、実践的な観点から解説します。
SQLインジェクションとは#
SQLインジェクション(SQL Injection)は、攻撃者がWebアプリケーションの入力フィールドを通じて、悪意のあるSQL文をデータベースに注入する攻撃手法です。
SQLインジェクション攻撃の基本的な流れ#
sequenceDiagram
participant Attacker as 攻撃者
participant WebApp as Webアプリ
participant DB as データベース
Note over Attacker,WebApp: 1. 攻撃者が悪意のある入力を送信
Attacker->>WebApp: ユーザー名: admin'--
Note over WebApp,DB: 2. アプリケーションが安全でないクエリを構築
WebApp->>DB: SELECT * FROM users<br>WHERE name='admin'--'
Note over Attacker,WebApp: 3. 攻撃者の意図した操作が実行される
DB-->>WebApp: 認証バイパス成功
WebApp-->>Attacker: 管理者としてログインSQLインジェクション攻撃で何ができるのか#
SQLインジェクションが成功すると、攻撃者は以下のような深刻な被害をもたらすことができます。
| 攻撃の種類 |
影響 |
具体例 |
| データの窃取 |
機密情報の漏洩 |
ユーザー情報、クレジットカード番号、パスワードの取得 |
| 認証バイパス |
不正アクセス |
パスワードを知らずに管理者としてログイン |
| データの改ざん |
データの整合性破壊 |
ユーザー情報の変更、権限の昇格 |
| データの削除 |
サービス妨害 |
テーブルの削除、データベースの破壊 |
| サーバー制御 |
完全なシステム掌握 |
OSコマンドの実行、ファイルの読み書き |
SQLインジェクションの仕組みと具体例#
脆弱なコードの例#
以下は、SQLインジェクションに脆弱なJavaコードの典型例です。
1
2
3
4
5
6
7
|
// 危険なコード - 文字列連結でクエリを構築
String query = "SELECT * FROM users WHERE username = '"
+ request.getParameter("username")
+ "' AND password = '"
+ request.getParameter("password") + "'";
Statement statement = connection.createStatement();
ResultSet results = statement.executeQuery(query);
|
このコードでは、ユーザーからの入力をそのままSQL文に埋め込んでいます。
攻撃例1: 認証バイパス#
攻撃者がユーザー名フィールドに以下を入力した場合を考えてみましょう。
admin'--
構築されるSQL文は次のようになります。
1
|
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'
|
--はSQLのコメント構文です。これ以降の文字列はすべて無視されるため、パスワードチェックが完全にバイパスされます。
flowchart TD
subgraph Normal["正常なクエリ"]
A["SELECT * FROM users WHERE username = 'admin' AND password = 'correct_password'"]
B["ユーザー名とパスワードの両方を検証"]
end
subgraph Attack["攻撃後のクエリ"]
C["SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'"]
D["これだけが実行される"]
E["コメントとして無視される"]
end
A --> B
C --> D
C --> E攻撃例2: UNION攻撃によるデータ窃取#
より高度な攻撃として、UNION句を使用したデータ窃取があります。
' UNION SELECT id, username, password, email FROM users--
この入力により、本来のクエリ結果に加えて、usersテーブルのすべての情報が取得されます。
1
2
3
4
|
-- 構築されるクエリ
SELECT id, name, description, price
FROM products
WHERE category = '' UNION SELECT id, username, password, email FROM users--'
|
攻撃例3: 複数文の実行による破壊的攻撃#
データベースが複数のSQL文の実行を許可している場合、さらに破壊的な攻撃が可能です。
'; DROP TABLE users;--
1
2
|
-- 構築されるクエリ
SELECT * FROM users WHERE username = ''; DROP TABLE users;--'
|
この攻撃により、usersテーブルが完全に削除されます。
パラメータ化クエリによるSQLインジェクション対策#
SQLインジェクションを防ぐ最も効果的な方法は、パラメータ化クエリ(プリペアドステートメント)を使用することです。
パラメータ化クエリの仕組み#
パラメータ化クエリでは、SQL文の構造とデータを明確に分離します。
flowchart TD
subgraph Parameterized["パラメータ化クエリの動作原理"]
A["1. SQL文の構造を先に定義(プレースホルダーを使用)<br>SELECT * FROM users WHERE username = ? AND password = ?"]
B["2. パラメータ値を後からバインド<br>パラメータ1: admin<br>パラメータ2: password123"]
C["3. データベースがSQL構造とデータを別々に処理<br>→ ユーザー入力は常にデータとして扱われ、<br>SQL命令として解釈されることはない"]
end
A --> B --> CJavaでの安全な実装#
1
2
3
4
5
6
7
8
9
|
// 安全なコード - パラメータ化クエリ(PreparedStatement)を使用
String username = request.getParameter("username");
String password = request.getParameter("password");
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet results = pstmt.executeQuery();
|
攻撃者が admin'-- と入力しても、それは文字列データとして扱われ、SQL構文として解釈されません。
各言語でのパラメータ化クエリ実装例#
Python(SQLite)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import sqlite3
# 安全なコード
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
username = user_input_username
password = user_input_password
# プレースホルダーを使用
cursor.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(username, password)
)
|
PHP(PDO)#
1
2
3
4
5
6
7
8
9
10
11
12
|
<?php
// 安全なコード - PDOを使用したパラメータ化クエリ
$pdo = new PDO('mysql:host=localhost;dbname=mydb', $user, $pass);
$username = $_POST['username'];
$password = $_POST['password'];
$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();
?>
|
Node.js(MySQL)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 安全なコード - プレースホルダーを使用
const mysql = require('mysql2/promise');
async function authenticateUser(username, password) {
const connection = await mysql.createConnection({
host: 'localhost',
database: 'mydb'
});
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
return rows;
}
|
C# (.NET)#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 安全なコード - パラメータ化クエリを使用
string query = "SELECT * FROM users WHERE username = @username AND password = @password";
using (SqlCommand command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@username", username);
command.Parameters.AddWithValue("@password", password);
using (SqlDataReader reader = command.ExecuteReader())
{
// 結果の処理
}
}
|
ORMを使用した安全な実装#
ORMを使用する場合も、正しく使えばSQLインジェクションを防げます。
Hibernateの安全な使用例#
1
2
3
4
5
6
|
// 安全なコード - 名前付きパラメータを使用
String hql = "FROM User WHERE username = :username AND password = :password";
Query query = session.createQuery(hql);
query.setParameter("username", username);
query.setParameter("password", password);
List<User> results = query.list();
|
危険なORM使用例#
1
2
3
|
// 危険なコード - 文字列連結を使用
String hql = "FROM User WHERE username = '" + username + "'";
Query query = session.createQuery(hql); // SQLインジェクションに脆弱
|
ストアドプロシージャによる対策#
ストアドプロシージャも、正しく実装すればSQLインジェクション対策として有効です。
安全なストアドプロシージャの例#
1
2
3
4
5
6
7
8
9
10
|
-- SQL Server での安全なストアドプロシージャ
CREATE PROCEDURE sp_GetUser
@username NVARCHAR(50),
@password NVARCHAR(50)
AS
BEGIN
SELECT * FROM users
WHERE username = @username
AND password = @password;
END
|
1
2
3
4
5
|
// Javaからの安全な呼び出し
CallableStatement cs = connection.prepareCall("{call sp_GetUser(?, ?)}");
cs.setString(1, username);
cs.setString(2, password);
ResultSet results = cs.executeQuery();
|
ストアドプロシージャの注意点#
ストアドプロシージャ内で動的SQLを使用すると、脆弱性が発生する可能性があります。
1
2
3
4
5
6
7
8
9
|
-- 危険なストアドプロシージャ
CREATE PROCEDURE sp_GetUser
@username NVARCHAR(50)
AS
BEGIN
DECLARE @query NVARCHAR(MAX)
SET @query = 'SELECT * FROM users WHERE username = ''' + @username + ''''
EXEC(@query) -- 動的SQLの実行 - 脆弱
END
|
入力検証による多層防御#
パラメータ化クエリに加えて、入力検証を行うことで多層防御を実現できます。
ホワイトリスト検証#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// テーブル名など、パラメータ化できない値の検証
public String getSafeTableName(String userInput) {
switch (userInput) {
case "users":
return "users";
case "products":
return "products";
case "orders":
return "orders";
default:
throw new IllegalArgumentException("Invalid table name");
}
}
|
入力の型チェック#
1
2
3
4
5
6
7
|
def get_user_by_id(user_id):
# 数値であることを検証
if not isinstance(user_id, int):
raise ValueError("user_id must be an integer")
# パラメータ化クエリを使用
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
OSコマンドインジェクションとは#
OSコマンドインジェクションは、アプリケーションがシステムコマンドを実行する際に、攻撃者が悪意のあるコマンドを注入する攻撃です。
OSコマンドインジェクションの仕組み#
【OSコマンドインジェクション攻撃の概要】
正常な動作:
ユーザー入力: example.com
実行コマンド: ping example.com
攻撃例:
ユーザー入力: example.com; cat /etc/passwd
実行コマンド: ping example.com; cat /etc/passwd
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
正常なコマンド 注入されたコマンド
脆弱なコードの例#
1
2
3
4
5
6
|
<?php
// 危険なコード - ユーザー入力を直接コマンドに渡す
$host = $_GET['host'];
$output = shell_exec("ping -c 4 " . $host);
echo "<pre>$output</pre>";
?>
|
攻撃者が以下のように入力すると、任意のコマンドが実行されます。
example.com; rm -rf /
OSコマンドインジェクションの対策#
対策1: OSコマンドの直接呼び出しを避ける#
1
2
3
4
5
6
7
8
9
10
|
<?php
// 安全なコード - 組み込み関数を使用
$host = $_GET['host'];
// DNSルックアップには組み込み関数を使用
$ip = gethostbyname($host);
if ($ip !== $host) {
echo "IP Address: " . htmlspecialchars($ip);
}
?>
|
対策2: 入力のエスケープと検証#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
// escapeshellarg()を使用して入力をエスケープ
$host = $_GET['host'];
// 入力検証
if (preg_match('/^[a-zA-Z0-9.-]+$/', $host)) {
$safe_host = escapeshellarg($host);
$output = shell_exec("ping -c 4 " . $safe_host);
echo "<pre>" . htmlspecialchars($output) . "</pre>";
} else {
echo "Invalid hostname";
}
?>
|
対策3: Javaでの安全な実装#
1
2
3
4
5
6
7
8
9
|
// 安全なコード - ProcessBuilderを使用し、引数を分離
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", hostname);
pb.redirectErrorStream(true);
Process process = pb.start();
// 入力検証
if (!hostname.matches("^[a-zA-Z0-9.-]+$")) {
throw new IllegalArgumentException("Invalid hostname");
}
|
LDAPインジェクションとは#
LDAPインジェクションは、LDAPクエリを使用するアプリケーションに対して、悪意のあるLDAP文を注入する攻撃です。
LDAPインジェクションの仕組み#
【LDAPインジェクション攻撃の概要】
正常なLDAPクエリ:
(&(uid=user)(password=pass123))
攻撃例(認証バイパス):
入力: *)(uid=*))(|(uid=*
結果: (&(uid=*)(uid=*))(|(uid=*)(password=anything))
→ すべてのユーザーにマッチ
脆弱なコードの例#
1
2
3
4
5
6
7
|
// 危険なコード - ユーザー入力を直接LDAPクエリに埋め込む
String filter = "(&(uid=" + username + ")(password=" + password + "))";
NamingEnumeration<SearchResult> results = ctx.search(
"ou=users,dc=example,dc=com",
filter,
searchControls
);
|
LDAPインジェクションの対策#
対策1: LDAP特殊文字のエスケープ#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// LDAPフィルター用のエスケープ関数
public static String escapeLdapFilter(String input) {
StringBuilder sb = new StringBuilder();
for (char c : input.toCharArray()) {
switch (c) {
case '\\': sb.append("\\5c"); break;
case '*': sb.append("\\2a"); break;
case '(': sb.append("\\28"); break;
case ')': sb.append("\\29"); break;
case '\0': sb.append("\\00"); break;
default: sb.append(c);
}
}
return sb.toString();
}
// 安全な使用例
String safeUsername = escapeLdapFilter(username);
String safePassword = escapeLdapFilter(password);
String filter = "(&(uid=" + safeUsername + ")(password=" + safePassword + "))";
|
対策2: ホワイトリスト検証#
1
2
3
4
|
// 入力値のホワイトリスト検証
if (!username.matches("^[a-zA-Z0-9_]+$")) {
throw new IllegalArgumentException("Invalid username format");
}
|
その他の注入攻撃#
XPath インジェクション#
XMLデータに対するクエリを操作する攻撃です。
1
2
|
<!-- 脆弱なXPathクエリ -->
//users/user[username='input' and password='input']/account
|
<!-- 攻撃例 -->
入力: ' or '1'='1
結果: //users/user[username='' or '1'='1' and password='']/account
NoSQLインジェクション#
MongoDBなどのNoSQLデータベースに対する注入攻撃です。
1
2
3
4
5
|
// 危険なコード
db.users.find({
username: req.body.username,
password: req.body.password
});
|
1
2
3
4
5
|
// 攻撃例 - JSONオブジェクトを送信
{
"username": {"$gt": ""},
"password": {"$gt": ""}
}
|
1
2
3
4
5
|
// 安全なコード - 入力の型検証
if (typeof req.body.username !== 'string' ||
typeof req.body.password !== 'string') {
return res.status(400).send('Invalid input');
}
|
注入攻撃対策のベストプラクティス#
多層防御の実装#
注入攻撃を防ぐには、複数の防御層を組み合わせることが重要です。
【多層防御のアプローチ】
Layer 1: 入力検証(フロントエンド)
↓
Layer 2: 入力検証(バックエンド)
↓
Layer 3: パラメータ化クエリ / エスケープ処理
↓
Layer 4: 最小権限の原則(データベース権限)
↓
Layer 5: エラーハンドリング(詳細なエラーを隠す)
最小権限の原則#
データベースアカウントには、必要最小限の権限のみを付与します。
1
2
3
4
5
6
7
8
9
|
-- 読み取り専用のWebアプリケーション用アカウント
CREATE USER 'webapp_readonly'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON mydb.products TO 'webapp_readonly'@'localhost';
GRANT SELECT ON mydb.categories TO 'webapp_readonly'@'localhost';
-- 書き込みが必要なアカウント(限定的な権限)
CREATE USER 'webapp_write'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE ON mydb.orders TO 'webapp_write'@'localhost';
-- DELETEやDROPは付与しない
|
エラーメッセージの制御#
詳細なエラーメッセージは攻撃者に情報を与えます。
1
2
3
4
5
6
7
8
9
10
11
|
// 悪い例 - 詳細なエラーを表示
catch (SQLException e) {
response.getWriter().println("Error: " + e.getMessage());
// 攻撃者にテーブル構造やクエリ情報が漏洩
}
// 良い例 - 一般的なエラーメッセージ
catch (SQLException e) {
logger.error("Database error", e); // 内部ログに記録
response.getWriter().println("An error occurred. Please try again.");
}
|
セキュリティテストの実施#
| テスト手法 |
説明 |
ツール例 |
| 静的解析 |
コードを解析して脆弱性を検出 |
SonarQube, Checkmarx |
| 動的解析 |
実行中のアプリをテスト |
OWASP ZAP, Burp Suite |
| ペネトレーションテスト |
実際の攻撃をシミュレート |
sqlmap, Metasploit |
まとめ#
SQLインジェクションをはじめとする注入攻撃は、適切な対策を施すことで確実に防ぐことができます。
| 攻撃の種類 |
主な対策 |
| SQLインジェクション |
パラメータ化クエリの使用 |
| OSコマンドインジェクション |
OSコマンドの直接呼び出しを避ける、入力のエスケープ |
| LDAPインジェクション |
LDAP特殊文字のエスケープ、ホワイトリスト検証 |
重要なポイントをまとめます。
- パラメータ化クエリを常に使用する: 文字列連結でクエリを構築しない
- 入力検証を実施する: ホワイトリスト方式で許可する値を限定
- 最小権限の原則を適用する: データベースアカウントの権限を最小限に
- 多層防御を実装する: 単一の対策に依存しない
- 定期的なセキュリティテストを行う: 脆弱性を早期に発見
これらの対策を確実に実装することで、注入攻撃からアプリケーションを守ることができます。
参考リンク#