动态拼接SQL不安全,参数化仅保值不保结构;标识符(表名、列名等)必须经白名单校验或系统视图验证后才可拼接,QUOTENAME仅转义无效于结尾注入;EXEC与sp_executesql均无法挽救拼接逻辑漏洞;权限控制与标识符校验必须双管齐下。

动态拼接本身不安全,参数化只保值不保结构
因为 sp_executesql 和 PREPARE ... EXECUTE 只能参数化**值**(如 @status、@id),不能参数化**标识符**(表名、列名、ORDER BY 字段、GROUP BY 表达式)。一旦你写 CONCAT('SELECT * FROM ', @table_name) 或 N'SELECT * FROM ' + @table_name,数据库就直接把 @table_name 当作语法一部分解析——它根本不会校验这个字符串是不是合法表名,更不会阻止你传入 'users; DROP TABLE logs--'。
白名单校验不是可选项,是强制前置步骤
所有可能被拼进 SQL 字符串的标识符,必须在拼接前完成硬性校验:
- 用
IF @sort_col NOT IN ('created_at', 'total', 'user_id')显式枚举,匹配失败立刻THROW或RETURN - 查系统视图比硬编码更灵活但更重:例如
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = @table_name AND schema_id = SCHEMA_ID('dbo')) -
QUOTENAME(@col)必须放在白名单之后,不是代替白名单——它只转义单引号和括号,对]; DROP TABLE--类结尾注入无效 - 禁止用
OBJECT_ID(@name)单独判断,它会截断分号前的内容,导致'logs; DROP TABLE users--'被当成'logs'通过
EXEC 比 sp_executesql 更危险,但两者都救不了拼接逻辑
EXEC 完全不支持参数绑定,纯字符串执行,等于裸奔;sp_executesql 虽然支持参数,但前提是 SQL 模板本身必须是静态或受控的。常见错误包括:
- 把用户输入塞进参数定义字符串:
N'@p nvarchar(50) = ''' + @user_input + ''''—— 这又回到了拼接老路 - 条件拼接时漏判空值:
IF @status != '' SET @sql += ' AND status = ''' + @status + '''',而前端传的是空格或NULL,校验失效 - 类型声明偷懒:
@p SQL_VARIANT或@p NVARCHAR(MAX),失去长度与类型约束,为隐式转换留后门 - 忽略
OPTION (RECOMPILE)的代价:用静态 SQL +WHERE (@status IS NULL OR status = @status)更稳,但若业务真要跳过索引字段,得权衡编译开销
权限与执行上下文常被当作“最后一道门”,其实它早该是第一道
即使代码里每个 @table_name 都过了白名单,如果存储过程的 DEFINER 是 root 或拥有 FILE 权限的账号,攻击者仍可能通过注入触发 INTO OUTFILE 或 LOAD DATA。关键点在于:
- 调用账号必须被
REVOKE FILE ON *.*,且secure_file_priv设为非空受限路径 - 存储过程定义时显式指定
SQL SECURITY DEFINER并确认该用户无SUPER、PROCESS、SHUTDOWN权限 - 禁用
local_infile,清理所有自定义函数(DROP FUNCTION IF EXISTS sys_exec) - 别忽略
CONTEXT_INFO、临时表名、OPENROWSET这些“非主干”拼接点——它们同样能构造可执行语句
真正容易被忽略的,是标识符校验和权限控制这两层必须同时生效。只做白名单但账号权限过大,等于锁了门却把钥匙交给黑客;只收权限但拼接逻辑松动,等于拆掉门锁却敞开窗户。

















