去年有个数据:OWASP把"注入类漏洞"连续钉在Top 10榜首十年。但真到自己写代码时,多数人还是栽在同一个坑里——比如把用户输入直接塞进URL。
今天这个故事的主角是个Spring Boot接口,看起来人畜无害,直到用户输入了一个"&"符号。
01 接口设计:一个看似合理的决定
开发者最初的写法很直观。项目ID和任务描述一起放在URL里,RESTful风格,路径清晰:
@PostMapping("/addScopeOfWork/{projectId}/{scopeOfWork}")
测试阶段一切正常。"修复登录模块"——通过。"更新数据库配置"——通过。代码评审也没人挑刺,毕竟这符合"资源定位"的直觉。
问题出在真实用户手里。有人输入了这么一段任务描述:Fix login & payment issues / urgent
斜杠被当成路径分隔符,"&"被解析为查询参数起始符。服务器收到的请求支离破碎,任务描述被腰斩,后面的"urgent"变成了不存在的子路径。
更隐蔽的是错误表现:有些场景下返回404,有些场景下静默截断字符串,还有些代理服务器直接拒绝请求。开发者花了两小时抓包,才发现URL解码后的内容早已面目全非。
02 为什么URL是用户输入的禁区
HTTP协议对URL字符有严格限制。RFC 3986明确规定:只有字母、数字和少数符号可以裸奔出现,其余必须Percent-Encoding编码。
但编码这件事,客户端和服务器经常各说各话。前端用encodeURIComponent,网关用Nginx重写规则,Spring再用自己的解码器——三层转换后,一个"%2F"可能变成斜杠,也可能变成"%252F"。
开发者后来复盘:把可变内容放在URL里,等于同时踩了三个雷区。
第一,长度限制。IE时代的2083字符天花板虽已远去,但某些日志系统和监控工具仍会截断超长URL。任务描述动辄几百字,挤在路径里本身就是隐患。
第二,敏感信息泄露。URL会出现在浏览器历史、服务器日志、Referer头里。如果任务描述包含客户名称或内部代号,这些痕迹很难清理。
第三,缓存污染。CDN和反向代理按URL做缓存键,一个带参数的URL可能让缓存策略彻底失效。
03 三分钟修复与背后的一堂课
解法简单粗暴:把scopeOfWork从路径变量挪到请求体。
@PostMapping("/addScopeOfWork/{projectId}")
POST方法的语义本就是"提交数据",请求体才是它的主战场。JSON格式天然支持任意Unicode字符,不需要编码解码的俄罗斯轮盘。
这个改动花了180秒,但开发者后来承认:如果项目已经上线,回滚成本远不止三分钟。URL是API的契约,一旦发布就有外部依赖在调用。修改路径结构等于breaking change,需要版本迁移、文档更新、逐个通知接入方。
更深层的问题在于设计惯性。很多人学RESTful时背过"资源名放URL,动作放方法",却没人提醒过"可变内容放body"。教程里的示例总是用userId、orderId这种干净的无符号整数,让人误以为所有参数都该一视同仁。
真实世界的数据是脏的。用户会从PDF里复制带换行符的文本,会用全角符号,会在描述里贴URL。API设计的第一课,是假设输入充满恶意;第二课,是假设输入充满意外。
04 这个bug在行业里的镜像
类似的陷阱反复出现。2021年Log4j漏洞,本质是JNDI查询字符串里的特殊字符被错误解析;2023年某头部云厂商的API网关故障,原因是路径参数里的emoji触发了正则回溯。
OWASP MASVS(移动应用安全验证标准)把"输入验证"列为核心控制项,不是小题大做。攻击者寻找的往往不是惊天动地的0day,而是开发者对边界情况的忽视。
这个案例的开发者后来在技术博客里写了一段话,被大量转发:
「Just because it works with simple input doesn't mean it works in real-world scenarios.」
翻译过来很朴素:别用测试数据骗自己。
你的API接口里,还有多少参数躺在URL里睡觉?下次code review,要不要专门扫一眼@PathVariable的用法——特别是那些可能包含空格、符号或非ASCII字符的字段?
热门跟贴