説明
信頼できない値をSQLのコマンド文字列に組み込むと、データではなくSQL構文として解釈されるおそれがあります。攻撃者は引用符、演算子、コメント、文の区切りを挿入して、条件や実行する文を変更できます。sqlite3_exec() や PQexec() のような即時実行関数だけでなく、外部入力から組み立てたSQLを sqlite3_prepare_v2()、PQprepare()、mysql_stmt_prepare()、SQLPrepare() に渡す場合も同様です。プリペアドステートメントというAPI名だけで安全になるわけではなく、SQLのテンプレートを固定して値を別にバインドする必要があります。
文字列の置換とパラメーターバインドは、異なる保護手段です。バインドはプロトコルやドライバーのレベルでSQLコードとデータ値を分離します。引用符を手動で置換したり、sanitize_sql() という関数を呼び出したりするだけでは、データベースの種類、接続の文字コード、値と識別子の位置、周囲の引用符を正しく扱えるとは限りません。
想定される影響
- 認証やアクセス制御の条件を回避され、他のユーザーのデータが読み取られるおそれがあります。
- データの作成、変更、削除や、データベースのスキーマ・設定の改変につながる可能性があります。
- データベースアカウントの権限とサーバー機能によっては、ファイルへのアクセス、OSコマンドの実行、サービス停止につながります。
- エラーメッセージや応答の違いから、データベース構造や機密情報が漏えいするおそれがあります。
対処方法
SQLのテンプレートを定数として保持し、外部から受け取るすべての値を、データベースのパラメーターAPIに渡してください。
- SQLiteでは、
?、?NNN、:name、@name、$nameを含む固定SQLをsqlite3_prepare_v2()またはsqlite3_prepare_v3()で準備し、sqlite3_bind_*()で値をバインドしてください。 - PostgreSQLのlibpqでは、1回だけ実行するクエリには
PQexecParams()を使うか、PQprepare()で固定クエリを準備してPQexecPrepared()で値を渡してください。非同期コードでは対応するパラメーターAPIを使ってください。 - MySQL 8.4 C APIでは、
mysql_stmt_prepare()、mysql_stmt_bind_named_param()、mysql_stmt_execute()の順に使用してください。互換クライアントで従来のmysql_stmt_bind_param()を使う場合も、固定SQL内の各?に対応する値をバインドする必要があります。 - ODBCでは
SQLPrepare()、SQLBindParameter()、SQLExecute()を使用してください。1回のSQLExecDirect()でも、パラメーターマーカーと事前にバインドした値を使用できますが、SQL自体は固定する必要があります。 - パラメーターは通常、値を表します。テーブル名、列名、ソート方向、演算子、SQLキーワードなどの構造はバインドできません。構造を選ぶ必要がある場合は、入力を少数の許可した識別子と比較し、コードに固定したSQL断片に対応付けてください。
- 準備、バインド、実行の各戻り値を確認し、文や結果のオブジェクトを適切なAPIで解放してください。データベースアカウントには、業務に必要な最小限の権限だけを付与してください。
動的SQLが避けられない場合も、データベースが提供する文脈に合ったエスケープ関数を使い、結果を指定された構文位置だけに挿入してください。SQLiteの %q は単一引用符を重ねるだけで、周囲の引用符は追加しません。%Q はSQL文字列リテラル用の引用符を追加します。libpqの PQescapeLiteral() と PQescapeIdentifier() も、対象とする構文位置が異なります。別の位置や既に開かれた引用符内で結果を再利用すると保護が失われるため、汎用の無害化関数として扱わないでください。OWASPも、すべての入力をエスケープする方法より、パラメーター化クエリを優先しています。
例
C
変更前
#include <stddef.h>
#include <sqlite3.h>
int find_user_unsafe(sqlite3 *database, const char *name) {
char *sql = sqlite3_mprintf(
"SELECT id FROM users WHERE name = '%s'", name);
if (sql == NULL) {
return SQLITE_NOMEM;
}
int result = sqlite3_exec(database, sql, NULL, NULL, NULL);
sqlite3_free(sql);
return result;
}
変更後
#include <stddef.h>
#include <sqlite3.h>
int find_user(sqlite3 *database, const char *name) {
static const char query[] =
"SELECT id FROM users WHERE name = ?";
sqlite3_stmt *statement = NULL;
int result = sqlite3_prepare_v3(
database, query, -1, 0, &statement, NULL);
if (result != SQLITE_OK) {
return result;
}
result = sqlite3_bind_text(
statement, 1, name, -1, SQLITE_TRANSIENT);
if (result != SQLITE_OK) {
sqlite3_finalize(statement);
return result;
}
result = sqlite3_step(statement);
int finalize_result = sqlite3_finalize(statement);
if (result != SQLITE_ROW && result != SQLITE_DONE) {
return result;
}
return finalize_result;
}
解説:
- 変更前: 外部の値が
%sで直接SQL文字列に挿入され、引用符やSQL構文を変更できます。 - 変更後: SQL構造と
?パラメーターを固定し、nameをsqlite3_bind_text()でデータとして渡します。sqlite3_prepare_v3()を呼ぶだけでなく、準備するSQLを固定することが重要です。
C++
変更前
#include <sqlite3.h>
#include <string>
int find_user_unsafe(sqlite3 *database, const std::string &name) {
const std::string sql =
"SELECT id FROM users WHERE name = '" + name + "'";
return sqlite3_exec(
database, sql.c_str(), nullptr, nullptr, nullptr);
}
変更後
#include <memory>
#include <sqlite3.h>
#include <string>
struct StatementDeleter {
void operator()(sqlite3_stmt *statement) const noexcept {
sqlite3_finalize(statement);
}
};
int find_user(sqlite3 *database, const std::string &name) {
constexpr char query[] =
"SELECT id FROM users WHERE name = ?";
sqlite3_stmt *raw_statement = nullptr;
int result = sqlite3_prepare_v3(
database, query, -1, 0, &raw_statement, nullptr);
if (result != SQLITE_OK) {
return result;
}
const std::unique_ptr<sqlite3_stmt, StatementDeleter>
statement{raw_statement};
result = sqlite3_bind_text(
statement.get(), 1, name.c_str(), -1, SQLITE_TRANSIENT);
if (result != SQLITE_OK) {
return result;
}
result = sqlite3_step(statement.get());
if (result == SQLITE_ROW || result == SQLITE_DONE) {
return SQLITE_OK;
}
return result;
}
解説:
- 変更前:
std::stringを使っても、外部の値をSQLに連結すればコードとデータは分離されません。c_str()は表現を変えるだけで、SQLインジェクションを防ぎません。 - 変更後: 固定SQLを準備して
nameを値としてバインドします。std::unique_ptrのカスタムデリーターは、どの経路で戻る場合もsqlite3_finalize()で文を解放します。
識別子など、バインドできない構造は固定の対応表から選択してください。
#include <sqlite3.h>
#include <string.h>
int list_users(sqlite3 *database, const char *sort_key) {
const char *query = NULL;
if (strcmp(sort_key, "name") == 0) {
query = "SELECT id, name FROM users ORDER BY name";
} else if (strcmp(sort_key, "created") == 0) {
query = "SELECT id, name FROM users ORDER BY created_at";
} else {
return SQLITE_MISUSE;
}
return sqlite3_exec(database, query, NULL, NULL, NULL);
}
参考資料
- SQLite
sqlite3_exec() - SQLiteの準備関数
- SQLiteの値のバインド
- SQLiteの従来の
sqlite3_get_table() - SQLite組み込みの
printf()と%q・%Q - PostgreSQL 18 libpqのコマンド実行
- PostgreSQL 18 libpqの非同期コマンド処理
- MySQL 8.4 C APIのプリペアドステートメント
- MySQL 8.4
mysql_stmt_bind_named_param() - MySQL 8.4
mysql_stmt_prepare() - MySQL 8.4の非同期C API
- ODBC
SQLExecDirect() - ODBCのパラメーターマーカーのバインド
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP ASVS 5.0.0 V1.2.4
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- OWASP Top 10:2025 A05 Injection
- OWASP Top 10:2021 A03 Injection