第三章:输入验证与过滤——白名单与黑名单策略、正则表达式安全写法、参数化查询防SQL注入、XSS过滤库(OWASP Java Encoder)
大家好,我是你们的老朋友。今天我们来聊聊支付系统里最基础、也最容易翻车的一个环节——输入验证与过滤。
说实话,我见过太多因为输入没处理好导致的安全事故了。有一次,一个同事在支付接口里直接拼接用户输入的订单号到SQL里,结果被注入了,整个订单表差点被删光。嗯,从那以后,我对输入验证这件事就特别较真。
一、白名单 vs 黑名单:选哪个?
先问大家一个问题:你更相信“允许什么”还是“禁止什么”?
我个人习惯用白名单。为什么呢?因为黑名单永远有漏网之鱼。你想想看,你禁止了 <script>,但攻击者可以用 <ScRiPt>、<img src=x onerror=alert(1)>,甚至用编码绕过。黑名单就像打地鼠,永远打不完。
白名单策略:只允许你明确指定的字符或格式。比如订单号只允许数字和字母,那就只匹配 [a-zA-Z0-9]+。
黑名单策略:禁止已知的危险字符。比如过滤掉 <script>、--、OR 1=1 等。
核心原则:能用白名单,就别用黑名单。白名单是“我知道你要什么”,黑名单是“我猜你不要什么”。
我在项目中遇到过这样一个案例:一个支付回调接口,接收商户的回调参数。最初用的是黑名单,过滤了 alert、script 等关键词。结果攻击者用 <svg onload=alert(1)> 绕过了。后来改成白名单,只允许数字、字母和特定符号,问题就解决了。
二、正则表达式安全写法
正则表达式是个好东西,但写不好就是灾难。我见过有人写 .* 来匹配所有输入,这跟没过滤有什么区别?
写正则的时候,记住几个要点:
- 明确边界:用
^和$锚定开头和结尾,防止部分匹配绕过。 - 避免贪婪匹配:比如
.*会匹配尽可能多的内容,容易导致性能问题甚至 ReDoS(正则拒绝服务攻击)。 - 不要用正则解析复杂结构:比如解析 HTML、JSON,正则搞不定的,用专门的解析器。
举个例子,验证一个手机号:
// 错误写法:没有边界,可能匹配到中间的数字
String regex = "\\d{11}";
// 正确写法:明确开头和结尾
String regex = "^\\d{11}$";
我曾经踩过一个坑:写了一个正则来匹配邮箱,结果因为没加边界,用户输入 abc@test.com 居然通过了。嗯,从那以后我写正则必加 ^ 和 $。
避坑指南:我曾经用了一个复杂的正则来匹配 URL,结果在生产环境导致 CPU 飙升。后来发现是正则回溯太多,攻击者构造了一个特殊字符串让正则引擎陷入死循环。所以,正则越简单越好,复杂逻辑交给专门的解析器。
三、参数化查询防SQL注入
SQL注入,老生常谈的问题了。但为什么到现在还有人在犯?说白了,就是图省事,直接拼接字符串。
我见过最离谱的代码:
String sql = "SELECT * FROM orders WHERE order_id = '" + orderId + "'";
这种写法,攻击者只要传入 ' OR 1=1 --,就能把整个订单表拉出来。你说危不危险?
正确的做法是使用参数化查询(PreparedStatement):
// Java 示例
String sql = "SELECT * FROM orders WHERE order_id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, orderId);
ResultSet rs = pstmt.executeQuery();
参数化查询的原理是:SQL 语句的结构和参数是分开的,数据库会先把 SQL 语句编译好,再把参数当成纯数据传进去。这样,即使用户输入了 ' OR 1=1 --,它也只是个字符串,不会改变 SQL 的结构。
小技巧:在支付系统里,我习惯把所有数据库操作都写成参数化查询,哪怕只是一个简单的查询。这不是过度设计,而是习惯成自然。你想想看,万一哪天有人改代码,忘了加参数化,那不就出事了?
另外,存储过程也要小心。有些存储过程内部还是拼接字符串的,那参数化查询也救不了你。所以,存储过程里也要用参数化。
四、XSS过滤库:OWASP Java Encoder
XSS(跨站脚本攻击)在支付系统里特别危险。攻击者可以在支付页面注入恶意脚本,窃取用户的银行卡信息、支付密码等。
我见过有人自己写过滤函数,把 < 替换成 <,把 > 替换成 >。但这样够吗?不够。因为 XSS 的上下文不同,过滤方式也不同。
- 在 HTML 标签里,需要转义
<、>、&、"等。 - 在 JavaScript 字符串里,需要转义
'、"、\等。 - 在 URL 参数里,需要 URL 编码。
所以,我推荐使用 OWASP Java Encoder 这个库。它提供了针对不同上下文的编码函数:
// HTML 上下文
String safeHtml = Encode.forHtml(userInput);
// JavaScript 上下文
String safeJs = Encode.forJavaScript(userInput);
// URL 参数
String safeUrl = Encode.forUriComponent(userInput);
这个库的好处是:你不需要自己写过滤逻辑,它已经帮你考虑好了各种边界情况。而且它是 OWASP 官方维护的,更新及时。
核心原则:永远不要相信用户的输入。输出到哪,就按哪的规则编码。HTML 里用 HTML 编码,JavaScript 里用 JavaScript 编码,URL 里用 URL 编码。
我在项目中遇到过这样一个场景:支付成功页面需要显示用户的昵称。昵称是用户自己填的,可能包含 <script> 等恶意内容。我用了 Encode.forHtml() 来编码,确保昵称里的 HTML 标签不会被浏览器解析。这样,即使用户填了 <script>alert('xss')</script>,它也只是显示成普通文本,不会执行。
五、知识体系总览
下面这张图是我自己画的,把本章的核心知识点串起来了。你可以看到,输入验证和过滤是一个层层递进的过程:
从这张图你可以看到,整个流程是从用户输入开始,经过策略选择、技术实现,最终安全地输出到数据库或页面。每一层都不可或缺。
六、总结
好了,今天的内容就到这里。我最后再啰嗦几句:
- 白名单优先:能明确允许什么,就别去猜禁止什么。
- 正则要严谨:加边界、避免贪婪、防止 ReDoS。
- 参数化查询是底线:任何拼接 SQL 的行为都是危险的。
- XSS 过滤用专业库:OWASP Java Encoder 是个好选择,别自己造轮子。
记住,支付系统里每一笔钱都关系到用户的切身利益。输入验证和过滤,就是保护这些钱的第一道防线。别偷懒,别侥幸。
公众号:蓝海资料掘金营,微信deep3321