多数据库适配下的SQL注入防护,难点从来不在“有没有过滤”,而在于不同数据库的语法、驱动、参数绑定方式、报错行为差异太大,同一套规则很难在所有库上同时生效。很多团队在MySQL上做了完整防护,一切换到PostgreSQL或SQL Server就出现绕过;或者反过来,为了兼容多个数据库把过滤写得极松,最后等于没防。真正可落地的统一策略,核心思路是把“输入处理”和“数据库交互”彻底分离:输入侧做类型约束和格式校验,交互侧强制参数化,数据库侧用最小权限兜底,再配合一套能识别多库差异的检测规则。

先明确多库环境下SQL注入的三个真实风险点

第一个风险点是拼接习惯在代码层扩散。很多业务为了兼容不同数据库的分页写法、函数名、字符串连接符,会写出类似“if mysql then concat else ||”的分支,然后直接把用户输入拼进SQL片段。这种代码一旦出现,任何上层的WAF或过滤都很难完全拦截,因为攻击载荷可能藏在数据库特有的语法结构里。

第二个风险点是参数绑定的库间差异被忽略。比如Oracle的参数前缀是冒号,PostgreSQL用$1、$2,SQL Server用@name,MySQL用问号。如果封装的数据访问层没有统一抽象,开发人员在不同模块里手写不同的占位符,就很容易在某个分支里漏掉绑定,直接拼接字符串。统一防护的第一件事,就是让业务代码里根本不允许出现手写占位符和拼接SQL。

第三个风险点是报错信息泄露和错误处理不一致。MySQL的报错注入、PostgreSQL的堆栈详情、SQL Server的严重级别提示,都可能被用来探测表结构或数据内容。多库环境下,如果每个库的错误处理策略不同,攻击者就能通过对比响应差异来判断后端数据库类型,并针对性地调整注入手法。统一关闭详细错误输出、统一返回模糊错误信息,是必须做的基础动作。

统一防护的第一层:输入侧做类型约束,不做“SQL关键词过滤”

很多防注入方案一上来就过滤select、union、drop、exec这些关键词。这种思路在多库场景下几乎必然失效。PostgreSQL里可以用注释符和换行绕过关键词匹配,SQL Server支持方括号和Unicode同形字符,MySQL对反引号和科学计数法的容忍度又不一样。与其跟攻击者拼关键词库,不如把输入按业务类型严格约束。

数字类型的参数,必须在进入SQL前用编程语言强转成整型或浮点型,转失败直接拒绝。字符串类型的参数,如果业务上只允许字母数字、邮箱、手机号、UUID等格式,就用正则白名单校验。白名单不通过就不进入数据库层,而不是“过滤掉危险字符后继续执行”。对于确实需要自由文本的字段,比如文章正文、评论内容,这类数据不应该参与SQL结构拼接,只能通过参数绑定传递。

日期、枚举、布尔类型同理。枚举值必须在服务端定义合法集合,传入值不在集合内就报参数错误。日期用语言内置的日期解析器解析成日期对象,再交给数据访问层。这样处理之后,能到达数据库交互层的输入,要么是强类型数据,要么是纯文本参数,SQL注入的载荷很难在结构上成立。

统一防护的第二层:数据访问层强制参数化,屏蔽数据库方言差异

参数化查询是防SQL注入的根基,但多库环境下要做对,必须有一个统一的数据访问抽象层。这个抽象层对外暴露的接口只接收“SQL模板”和“参数列表”,SQL模板里统一使用命名参数,比如:name、:age。抽象层内部再根据实际连接的数据库类型,把命名参数翻译成对应数据库的占位符。

以Java生态为例,JPA的JPQL和Hibernate的HQL本身就是数据库无关的,底层会自动处理方言。但如果项目里用了原生SQL,就必须通过EntityManager的createNativeQuery配合setParameter,而不是手动拼接。对于Node.js,可以用Knex或TypeORM这类查询构建器,它们在不同数据库驱动之上做了统一参数绑定。对于Go,database/sql本身就支持占位符,但要注意不同数据库驱动的占位符风格,最好封装一层rebind函数。

关键原则是:业务代码里不允许出现任何字符串拼接SQL的语句。代码审查时,只要看到SQL字符串里出现变量、加号、模板字符串插值,就应视为阻断项。对于必须动态变化的表名、列名、排序字段,这类标识符不能通过参数绑定处理,只能使用白名单映射。比如排序字段只允许id、create_time、update_time三个值,前端传入的值必须映射成后端定义好的列名常量,绝不能直接把用户输入拼进order by。

统一防护的第三层:针对多库差异的检测与拦截规则

即使输入侧和交互层都做对了,仍然需要一层运行时检测作为兜底。因为业务复杂时,难免有遗漏的拼接点,或者第三方组件、报表工具生成的SQL不在控制范围内。这层检测必须理解不同数据库的语法特征,而不是用一套通用正则匹配所有流量。

MySQL的注入特征包括:注释符“#”、“-- ”、“/*!”,反引号包裹系统表,load_file、into outfile、information_schema等敏感标识,以及0x十六进制字符串。PostgreSQL的注入特征包括:pg_sleep、pg_read_file、current_database、information_schema,以及“::”类型转换和“;”多语句执行。SQL Server的特征包括:xp_cmdshell、sys.tables、waitfor delay、openrowset、以及“--”注释和分号多语句。Oracle的特征包括:utl_http、dbms_pipe、all_tables、dual表,以及“||”字符串连接和“--”注释。

统一的检测规则可以按数据库类型分组成策略集。当系统识别到当前请求连接的是MySQL,就加载MySQL策略集;连接PostgreSQL就加载PostgreSQL策略集。识别方式可以来自数据源配置、连接字符串、或者ORM方言设置。检测点放在数据访问层出口,对最终生成的SQL文本做一次扫描。扫描规则要同时覆盖:敏感系统表或系统函数、多语句分隔符、注释符与关键字的异常组合、超长十六进制或编码载荷、以及SQL结构与参数数量的不匹配。

这里有一个容易被忽视的点:检测规则不能只看关键词,要看“关键词是否出现在可执行位置”。比如用户输入里包含“select”这个词,但它是作为字符串参数通过绑定传递的,就不应该拦截。因此检测最好在参数绑定之后、SQL发送到数据库之前进行,此时用户输入已经被驱动转义或分离,真正有威胁的拼接内容才会暴露在SQL模板结构里。如果检测到异常,直接中断执行并记录完整上下文,包括数据库类型、SQL模板、参数类型、调用堆栈。

统一防护的第四层:数据库账号最小权限与对象隔离

应用连接数据库的账号,不应该拥有DDL权限、文件读写权限、系统存储过程执行权限。MySQL里去掉FILE权限,PostgreSQL里限制pg_read_file等函数的执行权限,SQL Server里禁用xp_cmdshell,Oracle里限制utl_http和dbms_pipe。即使注入发生,攻击者能做的事情也会被压缩到最小。

多库环境下,最小权限策略需要按库分别配置,但可以在运维层面用统一的权限模板管理。比如所有业务账号默认只有SELECT、INSERT、UPDATE、DELETE,且只授予业务表,不授予系统表。对于需要执行存储过程的场景,单独创建专用账号,并限制可执行的存储过程列表。对于跨库查询,避免使用具有跨库权限的账号,改为在应用层分别查询后组装结果。

另外,不同数据库的权限粒度不同。MySQL的权限到库和表,PostgreSQL可以到schema和函数,SQL Server可以到对象和语句,Oracle可以到列和存储过程。统一策略的核心是:业务账号的权限范围,以“完成业务所需的最小集合”为基准,逐库评审,而不是简单套用一个通用角色。

统一防护的第五层:错误处理与日志脱敏

多库环境下,数据库返回的错误信息格式差异很大。MySQL报错可能包含SQL片段,PostgreSQL报错可能包含表名和列名,SQL Server报错包含严重级别和行号,Oracle报错包含ORA编号和对象名。这些信息对攻击者极有价值,对正常用户毫无意义。统一做法是:在数据访问层捕获所有数据库异常,对外只返回一个通用的“数据操作失败”提示,同时生成内部错误码用于排查。详细的数据库错误信息只写入服务端日志,且日志内容要经过脱敏,不记录完整的SQL语句和参数值,或者至少对敏感字段做掩码。

日志脱敏本身也有多库适配的问题。比如记录SQL语句时,不同数据库的绑定参数格式不同,直接记录可能把用户输入和SQL结构混在一起。建议在日志里分开记录SQL模板和参数列表,参数值做哈希或部分掩码。这样既能满足排障需要,又不会在日志泄露时成为注入的辅助信息。

统一防护的落地顺序与验证方法

第一步,梳理所有数据库连接点和数据访问代码,找出直接拼接SQL的位置。第二步,引入或封装统一的数据访问层,把所有原生SQL改成参数化调用。第三步,对动态标识符建立白名单映射。第四步,部署按数据库类型分组的运行时检测规则。第五步,收紧数据库账号权限,统一错误处理和日志脱敏。第六步,用多库专用的注入测试用例做回归验证。

验证时不能只测MySQL。针对PostgreSQL,要测试注释绕过、多语句执行、类型转换注入。针对SQL Server,要测试堆叠查询、openrowset、时间盲注。针对Oracle,要测试字符串连接、子查询注入、报错注入。每个库至少准备二十条以上的注入用例,覆盖数字型、字符型、搜索型、排序型、分页型等不同注入位置。同时要验证正常业务请求不受影响,尤其是包含特殊字符的合法输入,比如用户昵称里带单引号、文章内容里带SQL关键词。

多数据库适配的SQL注入统一防护,本质上是一个分层治理的工程问题。输入侧做类型约束,交互侧做参数化,检测侧做方言感知,权限侧做最小化,错误侧做信息收敛。五层之间互相配合,任何一层单独拿出来都不够,但合在一起能把注入风险压到极低。真正难的不是技术实现,而是把“不允许拼接SQL”变成团队里不可逾越的代码规范,并在多库差异面前保持每一层策略的持续更新。