sql注入的漏洞
2011年,漏洞一个叫“小敖”的漏洞初中生,在搜索引擎里输入“怎么入侵网站”,漏洞翻到第三页时看到一篇讲SQL注入的漏洞帖子。他照着教程,漏洞在某个政府网站后台的漏洞搜索框里敲下 ' or '1'='1,页面刷新,漏洞整张用户表弹了出来。漏洞那台服务器跑的漏洞是SQL Server 2000,数据库账号还是漏洞sa,密码为空。漏洞这事当时在圈子里传开,漏洞没人觉得惊讶——那个年代,漏洞随便一个带参数的漏洞URL,手动加个单引号,漏洞报错信息就能把数据库版本、当前库名、甚至表结构全吐出来。
SQL注入的本质,是应用程序没有把用户输入的数据和SQL代码分清楚。数据库在执行SQL语句时,本来应该只把用户输入当作“值”,结果因为拼接字符串的写法,输入里的引号、分号、注释符被数据库当成了命令的一部分。比如登录验证的代码写成 SELECT * FROM users WHERE username='$name' AND password='$pass',攻击者在用户名框里填 admin' --,后面的密码判断直接被注释掉,登录逻辑瞬间瓦解。这不是什么高深技术,就是字符串拼接的必然结果。
我见过一个真实的案例。某地方医院的挂号系统,URL里有个 doctor_id=12 的参数。测试时把12改成 12 AND 1=1,页面正常;改成 12 AND 1=2,页面空白。再进一步,用 UNION SELECT 猜列数,最后把 user_name、password 从 admin 表里拖了出来。整个过程中,WAF没有任何反应,因为请求里没有 select 这种关键词的明文——攻击者把关键字拆成 se/**/lect,或者用大小写混写 SeLeCt,规则库根本拦不住。
很多人以为用了预编译语句就万事大吉。预编译确实能解决绝大多数拼接问题,但前提是“所有”用户输入都走参数化。我遇到过用MyBatis的团队,在XML里写 ${ } 而不是 #{ } ,结果照样注入。${ } 是直接拼字符串,#{ } 才是预编译占位符。更离谱的是,有人把 ORDER BY 后面的字段名也交给用户控制——预编译对表名、列名、排序方向无能为力,因为那些是SQL语法结构的一部分,不是数据。这时候只能靠白名单校验,比如 ORDER BY 后面只允许 id、name、create_time 这几个固定值。
还有一个坑:二次注入。用户注册时把用户名写成 admin'--,存进数据库时转义了,看起来安全。但程序在另一个地方从数据库读出这个用户名,直接拼进新的SQL语句里查询,转义后的内容又被还原成原始字符,注入就触发了。这种漏洞靠输入过滤根本防不住,因为第一次写入时数据是“干净”的,问题出在第二次使用时的上下文变化。
现在SQL注入比以前少了,但没绝迹。2023年某省医保系统被通报,漏洞点在一个导出Excel的功能里,参数 start_date 直接拼进 WHERE 子句。攻击者用时间盲注,通过 IF(ASCII(SUBSTRING(...))>100,SLEEP(5),0) 这种 payload,一个字符一个字符地把数据库里的身份证号、手机号全跑了出来。整个过程用了三天,服务器日志里全是正常的200响应,因为盲注不报错,只靠响应时间差判断。
防御SQL注入,说到底是两件事:一是让数据和代码彻底分离,能用预编译就用预编译,别偷懒;二是对无法参数化的部分——表名、列名、排序方向——做严格的白名单匹配。别指望WAF,别迷信输入过滤,更别觉得“内网系统没人攻击”。我见过太多内网系统,开发时觉得安全,结果一个弱口令加一个注入点,整个域控就被拿下了。
最后说个细节。有些ORM框架看起来安全,但底层还是拼SQL。Hibernate的HQL注入、JPA的JPQL注入,本质和SQL注入一样。只要用户输入能影响查询语句的结构,就有风险。写代码时多问一句:这个输入,到底是在当“值”用,还是在当“语法”用?答案如果是后者,就得打起十二分精神。
```(责任编辑:探索)














