参数化查询是唯一安全解法;手动转义单引号极易遗漏注入点且跨库不一致,而参数化由驱动自动完成类型绑定与转义,彻底隔离数据与sql结构。

直接用参数化查询,别碰单引号转义——所有含单引号的字符串(比如 O'Connor、Don't panic)只要走正规参数传入,SQL Server 就自动处理,根本不需要你手动加引号、替换两个单引号或拼字符串。
为什么不能在存储过程里给变量加单引号拼接
常见错误是写成这样:
SET @sql = 'SELECT * FROM users WHERE name = ''' + @name + ''''
这看似“加了单引号”,实则已打开注入大门。原因有三:
-
@name是用户可控输入,拼进字符串后,整个@sql变成纯代码片段,SQL Server 不做任何数据隔离 - 哪怕你用
REPLACE(@name, '''', '''''')修补单引号,也防不住; DROP TABLE users--这类语句级注入 - 类型丢失:如果
@name原本是NVARCHAR(50),拼接后变成无类型字符串,执行计划可能劣化
正确做法:静态 SQL + 参数占位符
只要查询结构固定,就完全没必要动态拼接。把含单引号的值当普通参数用:
SELECT * FROM users WHERE name = @name AND status = @status
这段 SQL 放在存储过程里,@name 传入 N'O''Connor' 完全合法,SQL Server 在编译阶段就把值和语法分开处理。你不需要、也不应该在存储过程里对 @name 做任何字符串操作。
关键点:
- 存储过程参数声明必须明确类型和长度,例如
@name NVARCHAR(50),不能用NVARCHAR(MAX)或SQL_VARIANT - 调用该存储过程时,应用层必须用
SqlParameter(.NET)、PreparedStatement(Java)等机制传参,禁止字符串拼接 - 前端传来的
O'Connor到数据库驱动层会自动转为O''Connor,你完全感知不到
真要动态拼表名/列名时,单引号问题只是表象,白名单才是命门
如果你非得根据输入决定查哪张表(比如多租户场景),那 @table_name 确实不能参数化,但此时单引号根本不是重点——重点是你正在拼接的是语法结构,不是数据。
错误示范:
SET @sql = 'SELECT * FROM ' + QUOTENAME(@table_name) + ' WHERE name = ''' + REPLACE(@name, '''', '''''') + ''''
这依然危险:QUOTENAME 防不住 users; DROP TABLE config-- 这种注入,因为分号和注释在标识符校验之后才起作用。
安全做法只有两个:
- 硬编码白名单:
IF @table_name NOT IN ('users', 'orders', 'products') THROW 50000, 'Invalid table', 1; - 查系统视图确认存在且合法:
IF NOT EXISTS (SELECT 1 FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = 'dbo' AND t.name = @table_name)
注意:OBJECT_ID(@table_name) 单独用不安全,它不校验 schema,恶意输入可能被截断后绕过。
真正容易被忽略的,是那些你以为“只是格式问题”的地方:比如把 @name 声明成 NVARCHAR(MAX),再在存储过程里用 LEN(@name) > 100 做长度检查——攻击者早就在上游传入超长畸形字符串,而你的检查根本拦不住拼接逻辑本身。防注入不是加一层转义,而是从参数声明、调用链路、执行模型三个层面切断“数据变代码”的路径。










