参数化查询是防SQL注入的底线,必须配合明确类型约束与严格事务边界;事务隔离级别仅防并发篡改,无法阻止注入,因注入发生在SQL解析阶段而非执行锁阶段。

支付系统里一旦出现 SQL 注入,不是“可能丢数据”,而是“大概率已丢钱”。参数化查询是底线,不是可选项;事务隔离级别只是辅助,不能替代输入控制。真正在生产环境扛住攻击的,是参数化 + 明确类型约束 + 严格事务边界三者咬合,缺一不可。
所有数据库操作必须用 prepare + bind_param(PHP)或等效机制
拼接字符串构造 SQL(比如 "SELECT * FROM orders WHERE id = " . $_GET['id'])在支付逻辑中等于主动交出数据库密码。哪怕只有一处漏网,就可能被用于查订单、改金额、删流水。
- PHP MySQLi 必须用
$stmt = $mysqli->prepare("UPDATE orders SET status = ? WHERE id = ?"),再用bind_param("si", $status, $id)—— 类型标记"si"不可省,它强制校验变量类型 - Python 的
sqlite3或psycopg2同理:只接受cursor.execute("UPDATE ...", (status, order_id)),绝不用f"UPDATE ... {order_id}" - Java JDBC 中
PreparedStatement的setString()/setInt()方法必须显式调用,不能靠executeUpdate(sql)直接传字符串 - 常见错误:把
$_POST['amount']转成 float 后直接拼进 SQL —— 类型转换不等于参数化,恶意输入仍可闭合引号并注入
READ COMMITTED 以上隔离级别对防注入无效,但能防并发篡改
有人以为把事务设成 SERIALIZABLE 就能挡 SQL 注入,这是误解。隔离级别管的是“多个事务怎么读写同一行”,不干预 SQL 解析过程。注入发生在语句编译前,跟锁无关。
- 支付扣款必须包裹在事务中,且起始明确设为
START TRANSACTION ISOLATION LEVEL READ COMMITTED(PostgreSQL)或等效语句 - MySQL 默认
REPEATABLE READ可能导致幻读,例如并发查余额 → 判定够 → 扣款 → 另一事务又插入一笔支出 → 实际余额不足 - 关键点:事务要短,只包真正需要原子性的操作(如“查余额 → 判够 → 扣款 → 记日志”),别把 HTTP 请求处理、日志落盘全塞进去
支付字段必须双重校验:参数化 + 应用层白名单
参数化防止语法破坏,但拦不住业务逻辑错乱。比如攻击者传 amount=99999999.99,参数化照单全收,但明显超出商品价格上限。
- 金额字段必须校验范围:
is_numeric($amount) && $amount > 0 && $amount ,且小数位严格限制为 2 位(用 <code>number_format($amount, 2, '.', '')标准化) - 订单 ID 必须匹配正则
/^[a-zA-Z0-9]{16,32}$/,禁止任何符号,包括下划线和中划线(有些老系统用 UUIDv4,但支付 ID 最好自定义规则) - 状态字段(如
status)只能从预设值中取:in_array($status, ['pending', 'paid', 'refunded'], true),绝不接受任意字符串 - 错误示例:用
intval($_GET['id'])处理订单 ID —— 若原始值是123' OR '1'='1,intval返回 123,看似安全,但后续若该变量又被拼进另一条非参数化语句(如日志 SQL),漏洞复现
最危险的不是没写参数化,而是写了但绕过了类型绑定,或者把参数化当成万能盾,忽视业务字段的语义边界。支付系统里,数据库语句安全和业务规则安全必须同时卡死,少一层,钱就少一分。

















