2019年,入防某头部电商平台的入防安全团队在例行巡检时发现,一个早已下线的入防旧版接口仍在被调用。攻击者通过这个接口的入防`id`参数,在两周内拖走了近三百万条用户订单记录。入防事后复盘,入防漏洞本身并不复杂——一个拼接SQL语句的入防查询,参数过滤形同虚设。入防真正让人后背发凉的入防是,这个接口的入防代码在五年前就已标记为“废弃”,却因为没人敢删,入防一直挂在服务器上。入防
SQL注入这件事,入防谈了几十年,入防依然在OWASP Top 10里稳坐前排。入防不是防御手段不够,而是开发节奏、历史包袱和人的惰性,总在制造新的缝隙。下面聊的几种防御方式,按实际落地时的优先级排列,不是教科书目录。
参数化查询:唯一值得当作信仰的东西
把用户输入和SQL语句彻底分开,让数据库知道“这是数据,不是代码”。Java里的`PreparedStatement`、Python的`cursor.execute(sql, params)`、PHP的PDO预处理,都是这个思路。它之所以排第一,不是因为先进,而是因为它是唯一从语法层面杜绝注入的手段。其他方法都是在“过滤”和“转义”上做文章,而参数化查询直接取消了拼接这个动作。
但有个细节常被忽略:不是用了`PreparedStatement`就万事大吉。如果SQL语句本身是动态拼接表名或列名,比如`ORDER BY ?`,参数化就失效了。这类场景需要白名单校验,而不是指望预编译。
输入验证:别把它当主角,但缺了它也不行
输入验证的问题在于,它永远在“猜”攻击者会输入什么。黑名单过滤`union`、`select`这些关键词,绕过方式一抓一大把:大小写混写、内联注释、编码变形。白名单相对靠谱,比如一个`page`参数只允许0到100的整数,那就强制转成`int`。但白名单的局限也很明显——不是所有字段都能穷举合法值。
我的判断是:输入验证是纵深防御里的一层,用来挡掉低质量的自动化扫描,但绝不能作为最后一道防线。把它当辅助,别当主力。
最小权限:让注入即使发生,也炸不出大坑
很多团队把数据库连接账号设成`root`或`sa`,图省事。一旦注入点被撕开,攻击者能直接`DROP TABLE`、读系统表、甚至通过`xp_cmdshell`执行系统命令。正确的做法是:Web应用连接数据库的账号,只给必要的`SELECT`、`INSERT`、`UPDATE`、`DELETE`权限,且只限于当前业务库。`DROP`、`FILE`、`EXECUTE`这些,能不给就不给。
这层防御不阻止注入发生,但它把“数据泄露”和“服务器沦陷”之间的路堵死了大半。成本极低,收益极高,却经常被跳过。
WAF和RASP:兜底的东西,别当第一道墙
WAF(Web应用防火墙)能拦掉不少已知的注入payload,部署也快。但它的问题和杀毒软件一样:规则滞后,误报烦人,绕过手法层出不穷。把WAF当唯一防线,等于把命交给规则库的更新速度。RASP(运行时应用自我保护)更贴近应用层,能在SQL执行前做拦截,但性能开销和兼容性需要实测。
我的看法是:WAF和RASP适合放在参数化查询之后,作为异常行为的监控和兜底。它们能帮你发现“哪里还有漏网的拼接”,而不是替你写安全的代码。
那些容易被忽略的角落
ORM框架不是免死金牌。Hibernate的HQL拼接、MyBatis的`${ }`占位符,照样能注入。存储过程如果内部动态拼SQL,同样中招。还有HTTP头、Cookie、JSON字段——凡是能进数据库的输入,都是潜在入口。
一个真实的教训:某金融系统在登录接口做了严格的参数化查询,但“忘记密码”功能里的邮箱参数直接拼进了`LIKE`语句。攻击者用`%' OR '1'='1`就绕过了验证。防御要覆盖所有输入路径,不能只盯着登录框。
SQL注入防御没有银弹。参数化查询是底线,最小权限是保险,输入验证和WAF是补充。真正难的不是知道这些方法,而是在赶工期、改需求、接遗留代码的时候,还愿意多写那一行`PreparedStatement`。