SQL字符串拼接危险因用户输入直接混入SQL,导致注入攻击;须用参数化查询并禁用模拟预处理,严格匹配占位符与参数类型及顺序。

为什么 string + SQL 拼接是危险的
因为用户输入直接混进 SQL 字符串里,数据库分不清哪部分是逻辑、哪部分是数据。比如用户搜 ' OR 1=1 --,拼出来就变成 SELECT * FROM users WHERE name = '' OR 1=1 --',整张表被拖走。
常见错误现象:mysql_query("SELECT * FROM user WHERE id = " . $_GET['id'])、f"SELECT * FROM log WHERE msg = '{user_input}'" 这类写法,哪怕加了 trim() 或 intval() 也拦不住多字段注入或盲注。
- 所有动态值——包括数字、字符串、列名(极少数必须动态时)、排序方向——都不能用字符串拼接进 SQL
- 列名和表名无法参数化,真要动态就得白名单校验,比如只允许
['name', 'email', 'created_at'] - 排序方向(
ASC/DESC)也不能用参数占位符,得用in_array($dir, ['ASC', 'DESC'])过滤
PHP 中用 PDO::prepare() 和 bindValue() 的硬性姿势
不是调了 prepare() 就安全,关键在「值不进 SQL 字符串」。PDO 默认开启模拟预处理(PDO::ATTR_EMULATE_PREPARES = true),某些版本下遇到 LIKE 或类型不匹配会退化成拼接,等于白做。
实操建议:
- 创建 PDO 实例时显式关闭模拟:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false) - 用
bindValue()而非bindParam(),除非你真需要变量引用;bindValue()更直觉,类型也更可控 - 明确指定参数类型,比如数字用
PDO::PARAM_INT,避免字符串被当数字解析出错 - 示例:
$stmt = $pdo->prepare("SELECT * FROM posts WHERE status = ? AND created_at > ?"); $stmt->bindValue(1, 'published', PDO::PARAM_STR); $stmt->bindValue(2, $since, PDO::PARAM_STR);
Python 的 sqlite3 和 psycopg2 参数占位符别混用
不同驱动用的占位符语法不同,写错一个字符就会变成拼接——而且不报错,只是失效。比如 sqlite3 只认 ? 或 :name,而 psycopg2 用 %s(注意是 %s,不是 %d 或 %f),用错就直接 SQL 注入。
常见翻车点:
- 在
psycopg2里写"WHERE id = %d" % user_id—— 这是 Python 字符串格式化,不是数据库参数化 - 用
f-string包WHERE name = '{name}'—— 即使后面跟cursor.execute(...)也晚了 -
sqlite3不支持命名参数嵌套,WHERE name IN (:names)是错的,得展开成WHERE name IN (?, ?, ?)再 bind - 正确示例(psycopg2):
cursor.execute("SELECT * FROM orders WHERE user_id = %s AND status = %s", (uid, 'shipped'))
Node.js 里 pg 和 mysql2 的参数位置不能手滑
JavaScript 没有编译期检查,占位符和参数数组对不上,轻则查不到数据,重则绕过参数化——比如你写了两个 $1,却只传一个值,PostgreSQL 会报错;但若你误写成字符串模板,它就静默执行拼接。
必须盯住的细节:
-
pg用$1,$2,顺序严格对应数组元素位置,不能跳号或重复 -
mysql2用?,也是从左到右依次替换,但不支持命名参数(mysql2的namedPlaceholders: true是模拟的,底层仍转为?,需确认版本兼容性) - 千万别在 SQL 字符串里出现
${}或+拼接,哪怕只是加个空格——`WHERE id = ${id} AND active = 1`是高危写法 - 示例(pg):
client.query('SELECT * FROM products WHERE category = $1 AND price
最易被忽略的是:ORM 也不绝对安全。比如 TypeORM 的 find({ where: { name: req.query.q } }) 看似安全,但如果 q 是 { $like: '%admin%' } 这种对象,且没关掉原始操作符支持,照样可能触发注入。参数化不是“用了 ORM 就万事大吉”,而是“每个动态值都必须走绑定路径”。











