第三章:输入验证与过滤——白名单与黑名单策略、正则表达式安全写法、参数化查询防SQL注入、XSS过滤库(OWASP Java Encoder)

大家好,我是你们的老朋友。今天我们来聊聊支付系统里最基础、也最容易翻车的一个环节——输入验证与过滤。

说实话,我见过太多因为输入没处理好导致的安全事故了。有一次,一个同事在支付接口里直接拼接用户输入的订单号到SQL里,结果被注入了,整个订单表差点被删光。嗯,从那以后,我对输入验证这件事就特别较真。

一、白名单 vs 黑名单:选哪个?

先问大家一个问题:你更相信“允许什么”还是“禁止什么”?

我个人习惯用白名单。为什么呢?因为黑名单永远有漏网之鱼。你想想看,你禁止了 <script>,但攻击者可以用 <ScRiPt><img src=x onerror=alert(1)>,甚至用编码绕过。黑名单就像打地鼠,永远打不完。

白名单策略:只允许你明确指定的字符或格式。比如订单号只允许数字和字母,那就只匹配 [a-zA-Z0-9]+

黑名单策略:禁止已知的危险字符。比如过滤掉 <script>--OR 1=1 等。

核心原则:能用白名单,就别用黑名单。白名单是“我知道你要什么”,黑名单是“我猜你不要什么”。

我在项目中遇到过这样一个案例:一个支付回调接口,接收商户的回调参数。最初用的是黑名单,过滤了 alertscript 等关键词。结果攻击者用 <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(跨站脚本攻击)在支付系统里特别危险。攻击者可以在支付页面注入恶意脚本,窃取用户的银行卡信息、支付密码等。

我见过有人自己写过滤函数,把 < 替换成 &lt;,把 > 替换成 &gt;。但这样够吗?不够。因为 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>,它也只是显示成普通文本,不会执行。

五、知识体系总览

下面这张图是我自己画的,把本章的核心知识点串起来了。你可以看到,输入验证和过滤是一个层层递进的过程:

输入验证与过滤知识体系 用户输入 白名单 vs 黑名单策略 正则表达式安全写法 | 参数化查询 | XSS过滤库 安全输出到数据库/页面 防止SQL注入、XSS等攻击 输入层 策略层 技术层 输出层 目标层

从这张图你可以看到,整个流程是从用户输入开始,经过策略选择、技术实现,最终安全地输出到数据库或页面。每一层都不可或缺。

六、总结

好了,今天的内容就到这里。我最后再啰嗦几句:

  • 白名单优先:能明确允许什么,就别去猜禁止什么。
  • 正则要严谨:加边界、避免贪婪、防止 ReDoS。
  • 参数化查询是底线:任何拼接 SQL 的行为都是危险的。
  • XSS 过滤用专业库:OWASP Java Encoder 是个好选择,别自己造轮子。

记住,支付系统里每一笔钱都关系到用户的切身利益。输入验证和过滤,就是保护这些钱的第一道防线。别偷懒,别侥幸。

公众号:蓝海资料掘金营,微信deep3321