字段名拼进sql危险因无法参数化,易遭注入攻击;必须用硬编码白名单校验,禁止转义或黑名单,排序、分组等结构参数同理需独立白名单。

导出Excel时字段名拼进SQL为什么危险
导出功能常允许用户选择要导出的列,比如通过 URL 传参 fields=name,age,email,后端直接拼进 SELECT name,age,email FROM users。这种写法看似方便,但字段名无法用参数化查询(? 或 $1 占位符只适用于值,不适用于列名、表名、ORDER BY 子句)。攻击者只要把 fields 改成 name,age,(SELECT password FROM admins LIMIT 1),就能执行子查询;或者填 name; DROP TABLE users-- 触发语句截断。
字段名必须走白名单,不能靠转义或正则过滤
常见错误是试图用 preg_replace 去掉特殊字符,或用 addslashes 转义单引号——这完全无效。因为字段名不在字符串上下文中,不需要引号闭合,也不受 SQL 字符串转义规则约束。更糟的是,正则黑名单(如禁止 union、select)容易被绕过:UNIunionON、大小写混写、注释符插入等都能逃逸。
- 只保留预定义的合法字段名,其余一律拒绝
- 白名单应硬编码在代码里,或从配置中心加载不可篡改的数组
- 不要动态生成白名单(比如从数据库元数据查出所有列名再放行),那等于没设防
- 区分大小写:MySQL 默认不区分,但 PostgreSQL 区分,白名单需统一小写或按实际 DB 规范校验
具体怎么实现字段白名单校验
以 Python Flask + SQLAlchemy 为例,核心逻辑是:解析 fields 参数 → 拆成列表 → 逐个比对白名单 → 任一不匹配就 400 错误。不要尝试“修复”非法字段名,直接拒掉。
ALLOWED_EXPORT_FIELDS = {"id", "name", "email", "created_at", "status"}
<p>fields_param = request.args.get("fields", "")
if not fields_param:
return jsonify({"error": "missing fields"}), 400</p><p>fields = [f.strip() for f in fields_param.split(",")]
if not all(f in ALLOWED_EXPORT_FIELDS for f in fields):
return jsonify({"error": "invalid field in fields"}), 400</p><h1>安全拼接</h1><p>columns_sql = ", ".join(fields)
sql = f"SELECT {columns_sql} FROM users WHERE active = true"</p>
其他语言同理:
- PHP:用
in_array($field, $allowedFields, true)严格校验 - Java(Spring Boot):定义
Set<string> allowedFields = Set.of("id", "name", ...)</string>,用allowedFields.contains(field) - Node.js:用
new Set(["id", "name"]) .has(field)
排序、分组、条件字段也得同样处理
导出常附带 sort=created_at、group_by=department、filter_status=active 等参数,这些都属于 SQL 结构部分,一样不能参数化。它们和字段名一样,必须各自维护独立白名单。
-
sort白名单示例:["created_at", "name", "score"],且需额外限制是否允许DESC(比如只允许sort=name:asc格式) -
group_by白名单必须比SELECT更窄,避免用户诱导执行GROUP BY (SELECT ...) -
WHERE条件字段(如filter_email)若用于动态构建WHERE email LIKE ?,字段名本身仍需白名单,值才走参数化
最容易被忽略的是多层嵌套场景:比如导出接口调用了另一个封装了动态 SQL 的 DAO 方法,而那个方法又没做白名单——结果整个链路就破防了。白名单校验必须落在最外层入口,且不可委托给下游组件代劳。











