真正能杜绝sql注入的是全程不拼接、全参数化、调用层不传毒。存储过程内用exec(@sql)或sp_executesql拼接用户输入等同于未防护,必须用参数占位符(如?或using)传递值,表名列名等需白名单校验,调用层也须严格过滤。

不能靠“用了存储过程”就认为安全了,真正能杜绝SQL注入的,是全程不拼接、全参数化、且调用层不传毒。
存储过程里写 EXEC(@sql) 或 sp_executesql 拼接字符串,等于没防
很多团队上线后被扫出注入点,根源就在这里:在 CREATE PROCEDURE 里又用动态 SQL 把用户输入塞进字符串。比如:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE name = ''' + @name + ''''; EXEC sp_executesql @sql;
这和应用层拼 SQL 没本质区别——@name 被提前展开成字符串,数据库解析时已混入语法结构。只要 @name 是用户可控的(比如来自 $_POST),攻击者就能闭合引号、注入 OR 1=1 或 UNION SELECT。
- 必须把所有用户输入作为参数传给
sp_executesql,用USING机制(SQL Server)或USING/$1占位符(PostgreSQL) - 表名、列名等标识符无法参数化,若真需动态,只能走白名单校验:
IF @table NOT IN ('orders', 'products') THROW 50000, 'Invalid table', 1 -
CONCAT()、+、||这类拼接操作一旦出现,整条路径就失效
MySQL 存储过程中 PREPARE + EXECUTE 必须用 ? 占位符,不能拼值
MySQL 对存储过程参数化支持较弱,PREPARE 语句本身不自动绑定变量,开发者容易掉坑。错例:
SET @sql = CONCAT('SELECT * FROM users WHERE id = ', @id); PREPARE stmt FROM @sql; EXECUTE stmt;
这里 @id 被直接拼进字符串,PREPARE 完全没起隔离作用。正确做法是:
SET @sql = 'SELECT * FROM users WHERE id = ?'; PREPARE stmt FROM @sql; EXECUTE stmt USING @id;
注意两点:
-
?是唯一合法的参数占位符,不能写成$1或@id -
USING后面只能跟变量名,不能是表达式或函数调用(如USING UPPER(@name)不合法) - MySQL 不支持对表名做
USING,所以SELECT * FROM ?是语法错误,必须白名单硬编码或拒绝该需求
调用层传参不设防,存储过程再安全也白搭
常见误判:“存储过程里没拼 SQL,肯定没问题”。但漏洞常出现在调用侧——比如 PHP 代码把未过滤的 $_GET['status'] 直接当参数传进去:
$stmt = $pdo->prepare("EXEC GetOrdersByStatus @status = ?"); $stmt->execute([$_GET['status']]);
如果 $_GET['status'] 是 shipped'; DROP TABLE orders--,而存储过程内部又用了拼接,那就崩了。更隐蔽的是权限越界:
- 前端绕过 JS 校验,直接调用高权限存储过程(如
usp_DeleteAllLogs) - 应用账户拥有
EXECUTE权限却没限制到具体过程,攻击者可枚举过程名并暴力调用 - 没开
PDO::ATTR_EMULATE_PREPARES = false,PDO 会退化为字符串替换,失去参数化保护
最易被忽略的一点:存储过程只是执行容器,它不校验输入合法性。一个 @phone NVARCHAR(20) 参数仍可能传入 ' OR 1=1 --,只要过程里拿它去拼字符串,防护就归零。真正的防线在“参数声明 → 绑定执行 → 零拼接”这个铁三角,缺一不可。











