去年帮一个朋友看他的入漏电商后台,登录框里输入 ' or 洞字1=1 --,直接进了管理员页面。符过他一脸懵,入漏问我是洞字不是密码太简单。我说跟密码没关系,符过是入漏你根本没过滤单引号。这类事情到今天还在发生,洞字说明字符过滤这件事,符过很多人压根没做对。入漏
SQL注入的洞字本质,是符过用户输入被当成SQL代码执行了。字符过滤的入漏目标就一个:让输入里的危险字符失去语法意义。但问题在于,洞字哪些字符危险?符过怎么处理才算安全?这两个问题没有标准答案,取决于你的数据库类型、编程语言、SQL写法。
单引号:最经典的那个
单引号是SQL注入的万能钥匙。字符串拼接的SQL里,攻击者用一个单引号就能闭合原查询,然后追加自己的语句。过滤方式看起来很简单——把 ' 转义成 \' 或者 ''。但这里有个坑:不同数据库的转义规则不一样。MySQL默认用反斜杠转义,SQL Server用两个单引号。你如果只做一种,换了个数据库就废了。
更麻烦的是宽字节注入。GBK编码下,%df%27 经过 addslashes() 处理后变成 %df%5c%27,而 %df%5c 在GBK里是一个合法汉字,剩下的单引号就逃逸出来了。这个案例我见过至少三次,每次都是老系统迁移时踩的坑。
注释符和分号
--、#、/* */ 这些注释符能帮攻击者截断后面的查询。; 则用来执行多条语句。过滤它们听起来合理,但实际开发中你会遇到麻烦——用户正常输入里也可能有这些符号。比如商品描述里写“价格--面议”,你全给过滤了,用户体验直接崩。
我的做法是:注释符和分号不在输入层过滤,而是在SQL执行层限制。用参数化查询,这些东西天然就是普通字符,根本不会被执行。过滤是补丁,参数化才是根治。
联合查询关键词
union、select、from 这些词被很多WAF拉黑。但攻击者用 un/**/ion、sel/**/ect 就能绕过。你过滤了空格,他用 %09、%0a 代替。你过滤了关键词,他用大小写混合 UnIoN。这场猫鼠游戏没有尽头。
我见过一个系统,过滤规则写了三百多行正则,结果还是被一个 /*!50000union*/ 绕过了。MySQL特有的版本注释语法,直接把WAF干沉默。所以别迷信黑名单,黑名单永远列不全。
数字型注入的盲区
很多人觉得数字参数不用过滤,因为不是字符串拼接。错。id=1 and 1=1 这种数字型注入照样能跑。你如果只过滤字符串参数,数字参数就是后门。正确做法是强制类型转换,intval() 一下,非数字直接变0,干净利落。
我的实际建议
字符过滤要做,但别把它当主力。第一层用参数化查询或预编译语句,这是最有效的防线。第二层做输入验证,白名单比黑名单靠谱——比如id参数只允许数字,用户名只允许字母数字下划线。第三层才是字符转义,针对那些实在没法参数化的场景,比如动态表名、动态排序字段。
还有一点,别自己写过滤函数。mysql_real_escape_string() 这种内置函数比你手写的正则可靠得多。我见过有人用 str_replace("'", "", $input) 来过滤,结果攻击者用 %27 编码就绕过了,因为PHP自动解码后单引号已经没了,但SQL里 %27 又被解码成单引号。这种双重解码的坑,手写过滤根本防不住。
最后说个真事。某次渗透测试,目标系统把所有危险字符都转义了,看起来无懈可击。但我发现它排序参数没过滤,order by 后面直接拼接用户输入。用 order by updatexml(1,concat(0x7e,database()),1) 直接报错注入出库名。字符过滤做得再好,漏了一个点就是全盘皆输。
所以别问“过滤哪些字符够了”,要问“我有没有用参数化查询”。前者是修修补补,后者才是正经事。