SQL SQL 注入防护策略与实践

sql注入可绕过简单拼接校验,因单引号闭合、注释符、union select等属sql解析器正常行为;前端过滤和js校验无效,参数化查询须确保驱动将输入当数据而非sql片段处理,表名、排序字段等动态语法位置必须白名单限制。

SQL SQL 注入防护策略与实践

SQL 注入是怎么绕过简单拼接校验的

只要用户输入进了 query 字符串拼接,就大概率能被绕过。比如用单引号闭合、注释掉后面校验逻辑、或用 UNION SELECT 拖库——这些不是“高级技巧”,而是 SQL 解析器的正常行为。

常见错误现象:' OR '1'='1 成功登录;admin' -- 跳过密码检查;后端日志里出现 mysql_affected_rows() returned -1 却没报错。

  • 别信“我过滤了单引号和分号”——/**/%00、宽字节(如 %df%27)都能绕过
  • 前端 JS 校验完全无效,攻击者直接发 POST 请求
  • ORM 的 .filter(name__icontains=xxx) 看似安全,但如果传入的是 raw string 而非参数化表达式,照样中招

参数化查询在不同语言里怎么写才真正生效

参数化不是加个问号就行,关键看驱动是否把值当“数据”而非“SQL 片段”送进执行器。Python 的 sqlite3?:name 是安全的,但若用 f"WHERE id = {user_id}",前面全白搭。

使用场景:所有带用户输入的 SELECTINSERTUPDATEDELETE,包括分页偏移、排序字段(需白名单限制)、LIKE 模糊匹配。

  • PHP PDO:$stmt = $pdo->prepare("SELECT * FROM users WHERE status = ?"); $stmt->execute([$status]); —— 必须用 execute() 传参,不能 bindValue() 后再拼字符串
  • Java JDBC:PreparedStatement ps = conn.prepareStatement("SELECT * FROM log WHERE level = ?"); ps.setString(1, level); —— setString() 才触发转义,ps.executeQuery("SELECT ...") 是错的
  • Node.js pg:client.query('SELECT * FROM items WHERE id = $1', [id]) —— 不能用模板字符串插值,$1 必须原样保留

哪些地方看似用了参数化,其实还是有注入风险

表名、列名、ORDER BY 字段、LIMIT 数值——这些语法位置不接受参数占位符,硬塞 ? 会直接报错:ERROR: syntax error at or near ""。这时候如果靠字符串拼接,就等于开门揖盗。

性能影响不大,但逻辑漏洞极难审计。线上出问题往往就卡在这类“边缘位置”。

  • 动态表名:必须走白名单校验,比如 if table_name not in ['users', 'orders']: raise ValueError
  • 排序字段:只允许 ['created_at', 'score'],且映射到固定 SQL 片段,不拼接原始输入
  • LIKE 模糊匹配:WHERE name LIKE '%' || ? || '%'(PostgreSQL)或 CONCAT('%', ?, '%')(MySQL),不能写成 "'%"+input+"%'"
  • MySQL 的 SET NAMES utf8mb4 这类语句,绝不能把编码名从 URL 取来拼进去

预编译语句失效的两个隐蔽原因

即使写了 prepare + execute,也可能因环境配置或驱动行为退化为字符串拼接。最典型的是 MySQL 的 sql_mode 和连接参数。

兼容性影响明显:某些旧版 PHP + mysqlnd 驱动在未开启 MYSQLI_OPT_PREPEND_STMT 时,会静默降级;Go 的 database/sql 默认启用预编译,但 mysql 驱动若设了 interpolateParams=true,就等于自废武功。

  • MySQL 连接字符串里含 useServerPrepStmts=false(Java)或 client_flag=CLIENT_PROTOCOL_41(Python PyMySQL)会导致预编译被忽略
  • PostgreSQL 的 pgbouncer 在 transaction pooling 模式下不支持预备语句,PREPARE 会报 prepared statement "xyz" does not exist
  • 调试方法:开启数据库通用日志(general_log=ON),看实际执行的 SQL 是否含用户输入原文

真正难的不是写对一行 prepare,而是确保整个调用链路——从 HTTP 参数解析、中间件透传、ORM 封装层、驱动配置,到数据库服务端模式——没有一处偷偷把参数又拼回了 SQL 字符串里。