在 C 語言中向 SQLite3 動態插入資料時,直接拼接 SQL 字串極易導致SQL 注入與特殊字元轉義錯誤。SQLite 官方文檔提供標準解法:sqlite3_mprintf() 格式化器,支援 %q 自動轉義單引號。
假設變數 zText 包含使用者輸入:
char *zText = "It's a happy day!"; // 含單引號
若直接拼接:
// 危險!單引號會破壞 SQL 語法
char *zSQL = sqlite3_mprintf("INSERT INTO table VALUES('%s')", zText);
生成的 SQL 為 INSERT INTO table VALUES('It's a happy day!'),語法錯誤。
%q 格式化符號char *zSQL = sqlite3_mprintf("INSERT INTO table VALUES('%q')", zText);
sqlite3_exec(db, zSQL, 0, 0, 0);
sqlite3_free(zSQL); // 必須釋放 mprintf 分配的記憶體
%q 會自動將字串中的單引號 ' 轉義為雙單引號 ''(SQL 標準轉義方式),生成安全 SQL:
INSERT INTO table VALUES('It''s a happy day!')
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
sqlite3 *db;
if (sqlite3_open("test.db", &db) != SQLITE_OK) {
fprintf(stderr, "Open failed: %s\n", sqlite3_errmsg(db));
return 1;
}
// 建表
sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS notes (content TEXT);", 0, 0, 0);
// 模擬使用者輸入
char *inputs[] = {
"Normal text",
"It's a quote",
"Special \"double\" quotes",
"Semicolon; DROP TABLE notes;--"
};
for (int i = 0; i < 4; i++) {
char *zSQL = sqlite3_mprintf("INSERT INTO notes VALUES('%q');", inputs[i]);
char *errMsg = NULL;
if (sqlite3_exec(db, zSQL, 0, 0, &errMsg) != SQLITE_OK) {
fprintf(stderr, "Insert failed: %s\n", errMsg);
sqlite3_free(errMsg);
}
sqlite3_free(zSQL);
}
// 驗證查詢
sqlite3_exec(db, "SELECT * FROM notes;",
[](void*, int argc, char** argv, char**) {
for (int i = 0; i < argc; i++) printf("%s ", argv[i] ? argv[i] : "NULL");
printf("\n");
return 0;
}, 0, NULL);
sqlite3_close(db);
return 0;
}
| 項目 | 說明 |
|---|---|
sqlite3_mprintf | 類 printf,但自動分配記憶體並支援 SQLite 專用格式符 |
%q | 核心安全機制:單引號 → 雙單引號,防注入、防語法錯誤 |
%Q | 同 %q,但 NULL 指標輸出為 NULL 關鍵字而非空字串 |
| 記憶體管理 | 回傳字串需呼叫 sqlite3_free() 釋放 |
| 參數化查詢 | 更推薦使用 sqlite3_prepare_v2 + sqlite3_bind_text,完全避免字串拼接 |
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, "INSERT INTO notes VALUES(?);", -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, zText, -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
參數化查詢從根本上分離代碼與資料,效能更高(可重複執行預編譯語句),是生產環境首選。