必须用 sp_executesql,不能用裸 exec;前者支持参数化绑定防注入,后者仅字符串拼接执行易遭注入;表名列名等标识符须白名单校验+quotename(),不可参数化。

存储过程里用 sp_executesql 还是 EXEC?
必须用 sp_executesql,不能用裸 EXEC。前者支持参数化绑定,后者只做字符串执行——哪怕你传的是变量,只要没进 sp_executesql 的 @params 和 @stmt 参数列表,SQL Server 就当它是代码片段。
常见错误:EXEC('SELECT * FROM users WHERE id = ' + @id),哪怕 @id 是 int 类型,拼接后也变成字符串,攻击者可注入 1; DROP TABLE users--。
正确姿势:
-
@sql只放硬编码或白名单校验后的结构部分(如固定表名、列名) - 所有用户输入必须作为独立参数,通过
sp_executesql的第三个及以上参数传入 -
@params字符串要显式声明类型,比如N'@status NVARCHAR(20)',不能只写@status
动态表名/列名怎么处理才不崩?
值(value)能参数化,标识符(identifier)不能。所以 WHERE status = @status 安全,但 FROM @table 语法错误,必须拼接——但拼接前必须过白名单。
别信“我用 QUOTENAME 就安全了”。QUOTENAME(@table) 防的是单引号闭合,防不了 users; DROP TABLE orders 这种语句级注入。
真正安全的做法:
- 用
IF @table NOT IN ('orders', 'users', 'products')硬校验 - 或查
INFORMATION_SCHEMA.TABLES做存在性验证(注意:查询本身带用户输入时也要白名单) - 禁止把前端传来的任意字符串直接喂给
QUOTENAME或REPLACE
MySQL 和 PostgreSQL 的 EXECUTE 差在哪?
MySQL 的 PREPARE + EXECUTE 不自动参数化值,必须手动用 USING;PostgreSQL 的 EXECUTE ... USING 更接近 SQL Server 模型,但仍有坑。
典型翻车点:
- MySQL:
SET @sql = CONCAT('SELECT * FROM t WHERE id = ', @id)→ 危险;应改用PREPARE stmt FROM 'SELECT * FROM t WHERE id = ?'; EXECUTE stmt USING @id - PostgreSQL:
EXECUTE 'SELECT * FROM users WHERE role = ''' || $1 || ''''→ 危险;必须写成EXECUTE 'SELECT * FROM users WHERE role = $1' USING user_role - PostgreSQL 的
quote_ident()只保标识符,不保值;quote_literal()也不能替代USING,它只是转义,不是参数化
为什么上线后还被扫出注入点?
问题往往不在存储过程内部,而在调用层:应用代码把未校验的字符串当参数传进来,或者前端绕过权限控制直连高危存储过程。
容易被忽略的点:
- 存储过程参数类型声明太宽泛,比如用
NVARCHAR(MAX)接收本该是整数的 ID - 前端 JS 校验被绕过,后端又没二次校验,恶意字符串一路透传到
sp_executesql - DBA 给应用账户开了
sysadmin或db_owner权限,一旦突破,后果远超数据查询
最麻烦的其实是“半安全”状态:存储过程本身写对了,但调用它的 Java 方法用了 Statement 拼接,再把结果喂给存储过程——防线从第一环就断了。










