SQL注入本质是应用层拼接SQL时未校验动态结构,修复核心是禁止用户可控值(含配置项)未经白名单校验进入SQL结构位置;配置表值因可被低权限攻击者篡改而不可信,必须在SQL构造前用硬编码白名单+严格正则校验表名等结构参数。

直接回答:系统配置表被恶意修改引发的 SQL 注入,本质不是“配置表被改”导致漏洞,而是应用层读取配置后拼接 SQL 时未校验、未隔离动态结构(如表名、列名)。修复核心只有一条:禁止任何用户可控值(含配置项)未经白名单校验就进入 SQL 结构位置。
为什么从配置表读出来的值一样会触发 SQL 注入?
很多人误以为“配置是后台管理员改的,所以可信”,但攻击者一旦获得低权限后台入口(如弱口令、未授权接口、XSS 后劫持 session),就能篡改配置表中类似 log_table_name、tenant_prefix 这类字段。后续代码若写成:SELECT * FROM ${config.tableName} WHERE user_id = ?,MyBatis 或 JDBC 模板字符串一拼,立刻执行恶意表名——比如 logs; DROP TABLE users --。
关键点:数据库不区分“谁写的配置”,只认最终拼出的 SQL 字符串。
MyBatis 中 ${} 读配置 + 动态表名 = 高危链路
典型错误模式:<select id="queryByConfig" resultType="map">SELECT * FROM ${config.tableName} WHERE id = #{id}</select>
这里 #{id} 安全,但 ${config.tableName} 是纯字符串替换,预编译完全不介入。
常见误区:
- 以为加了
WHERE条件或#{}就能“保护”前面的${}—— 不成立 - 试图用
mysql_real_escape_string()或正则过滤union|select|;—— 攻击者用user_logs%0aUNION%0aSELECT或大小写绕过 - 把校验逻辑放在 SQL 内(如存储过程里
IF @t NOT IN ('t1','t2'))—— 此时 SQL 已解析,晚了
必须在 Java 层硬编码白名单 + 正则兜底
校验动作必须落在 SQL 构造之前,且不可动态加载:
- 白名单必须是 Java
List.of("user_events", "order_logs", "pay_records")这种字面量枚举,禁用从数据库/配置中心/Redis 动态读取——否则白名单本身又被污染 - 正则需严格限制格式:
^[a-z][a-z0-9_]{2,31}$(小写字母开头,长度 3–32,仅允许字母、数字、下划线) - 校验失败立即抛
IllegalArgumentException,不进 DAO 层 - 通过后才用
String.format("SELECT * FROM %s WHERE id = ?", safeTableName)拼接,?只留给数据值
Node.js / Python 等非 Java 环境同样适用该逻辑
例如 Node.js mysql2:await conn.execute(`SELECT * FROM ${tableName} WHERE id = ?`, [id]) 是错的——模板字符串提前拼接,execute() 的预编译已失效。
正确做法:
- 先校验
tableName是否在["events_2024", "events_2025"]中 - 再用正则确认它符合
/^[a-z][a-z0-9_]+$/ - 最后调用
conn.execute(`SELECT * FROM ?? WHERE id = ?`, [tableName, id])—— 注意这里是??(mysql2 特有表名列名占位符),不是?
psycopg2 同理,要用 sql.SQL("SELECT * FROM {}").format(sql.Identifier(table_name)),而非字符串格式化。配置表本身不是问题源头,但它是攻击者最隐蔽的落脚点。所有从配置读出、用于 SQL 结构(表名、列名、ORDER BY 字段、GROUP BY 字段)的值,都必须走同一套硬校验逻辑——没有例外,没有“这个配置很安全所以跳过”。

















