为什么这里不能参数化
ORDER BY 的字段名、表名、列名属于 SQL 的结构位置,数据库不接受参数占位符,很多开发者于是退回字符串拼接,形成排序注入。正确做法是白名单映射:应用只接受预设键值,再把键映射到固定的 SQL 片段,白名单之外一律回退默认值。Java、Python、PHP、Go 各语言的处理方式相同。
基本信息
| 风险位置 | ORDER BY、GROUP BY、表名、列名、排序方向 |
|---|---|
| 成因 | SQL 结构位置不能用占位符,退回拼接 |
| 防护 | 键值白名单映射 + 默认值回退 |
| 禁止做法 | 关键字黑名单过滤、手工转义 |
| 适用平台 | 所有使用参数化查询但仍拼接结构片段的后端 |
典型使用场景
- 列表页排序:?sort=created_at&order=desc 这类参数最常被忽略
- 导出接口:导出时的字段与排序参数往往比页面接口校验更松
- 动态报表:用户自选统计列与分组维度
- 搜索接口:多字段模糊匹配时拼接排序表达式
- 分页与方向控制:asc/desc 方向值同样是注入点
白名单映射示例
白名单映射示例
SORT_WHITELIST = {
"name": "name", # 前端键 -> 数据库列
"created_at": "created_at",
"score": "score",
}
ORDER_WHITELIST = {"asc": "ASC", "desc": "DESC"}
col = SORT_WHITELIST.get(request.args.get("sort"), "created_at") # 未知键回退默认
way = ORDER_WHITELIST.get(request.args.get("order", "desc"), "DESC")
sql = f"SELECT id, name FROM items ORDER BY {col} {way} LIMIT %s OFFSET %s"
cursor.execute(sql, (page_size, offset)) # 值仍走参数绑定常见位置与处理对照
| 位置 | 可用占位符 | 正确做法 |
|---|---|---|
| WHERE 条件值 | 可以 | 参数绑定 |
| ORDER BY 字段名 | 不可以 | 白名单映射到固定列名 |
| 排序方向 asc/desc | 不可以 | 枚举校验,非法即回退默认 |
| 表名或列名 | 不可以 | 白名单映射,禁止直接拼接 |
| LIMIT/OFFSET | 多数驱动支持 | 仍建议整数强校验后再绑定 |
白名单之外一律回退默认值,不要回显用户输入的错误提示:把非法键返回 400 并附上允许的键列表,对定位问题更有帮助。
LIMIT 与 OFFSET 在多数数据库驱动中可以参数化,但排序方向和字段名不能;遇到驱动限制时,用整数强校验放在绑定之前,而不是退回字符串拼接。