代码审计里发现的“隐藏安全隐患”,绝大多数不是那种写在明面上的高危函数,而是开发过程中为了方便、为了绕过某一处过滤、或者为了临时修复一个业务问题留下的逻辑后门。这类问题扫描器很难直接报红,但一旦被摸清业务逻辑的攻击者利用,造成的权限绕过、数据越权或支付篡改往往比一个显眼的SQL注入更致命。下面直接说几类我在实际审计中反复遇到、且容易被忽视的隐藏问题,以及对应的定位和修复方法。
一、调试接口未清理导致的未授权访问很多项目上线前会删除后台的调试路由,但总有一些藏在不起眼的控制器里。比如用ThinkPHP、Laravel或Spring Boot开发时,开发人员会写一些临时接口用于测试支付回调、短信发送、用户状态切换。这些接口通常不经过完整鉴权中间件,或者只校验了一个固定token。审计时不要只看路由文件,要全局搜索“debug”“test”“tmp”“callback_test”等关键词,再结合控制器方法逐一确认是否暴露。曾经在一个电商项目里发现过 /api/order/testConfirm 这样的接口,可以直接把任意订单状态改为已支付,而该接口在正式环境被保留了下来。修复方式很直接:删除所有非业务必需接口,如果确需保留,必须纳入统一鉴权和权限体系,并增加操作日志。
二、越权漏洞藏在“看似有校验”的查询条件里水平越权是最常见也最容易被低估的问题。很多开发者认为只要查询时带了用户ID就安全了,但实际上用户ID是从前端传入的。比如查看订单详情的代码写成:
SELECT * FROM orders WHERE order_id = #{orderId} AND user_id = #{userId}
前端传了userId,后端也校验了userId与当前登录用户一致,看起来没问题。但问题在于,如果userId这个参数可以被篡改,而校验逻辑又只比较了传参是否等于当前会话用户,那么攻击者改掉userId后校验失败,但改回自己的userId又只能查自己的订单。真正危险的是那些“部分校验”的场景,例如只校验了订单存在、只校验了订单属于某个商户、或者用订单号做了模糊匹配。审计时要重点检查所有“根据ID查详情”的接口,确认查询条件中的用户维度是否来自服务端会话,而不是来自请求参数。更隐蔽的是批量查询接口,例如传入idList,后端只过滤了第一个ID的归属,后续ID未做逐条校验。
三、文件上传的“二次利用”风险现在大多数系统对上传文件做了扩展名白名单和内容校验,但隐藏风险在于上传后的文件被其他功能二次处理。比如一个只允许上传图片的接口,攻击者上传了一个内容为图片但文件名带特殊字符的文件,或者上传了一个SVG文件。SVG本身可以包含脚本,如果系统后续提供了“图片预览”“图片裁剪”“生成缩略图”功能,而处理时没有重新编码或过滤,就可能导致存储型XSS。更常见的是上传的图片被用于邮件模板、PDF导出或页面展示,如果图片URL可被控制为外部地址,就可能造成SSRF或信息泄露。审计时不要只看上传点本身,要追踪上传文件的完整生命周期:存储路径是否可预测、是否会被其他服务读取、是否会被模板引擎渲染、是否会被包含进压缩包或导出文件。
四、加密与签名逻辑中的“假安全”很多系统对敏感参数做了加密传输,但加密密钥硬编码在前端JavaScript里,或者使用了一个固定IV的AES-CBC。这种情况下加密只是增加了攻击者的分析成本,并不能真正防止篡改。审计时如果发现前端有加密逻辑,一定要找到对应的解密代码,确认密钥来源。如果密钥写死在前端,等同于明文。另一个高频问题是签名校验不完整,例如只对金额做了签名,没有对商品ID、数量、用户ID做签名,攻击者可以保持金额不变,篡改其他字段。支付回调接口尤其要检查:是否验证了签名、是否验证了订单状态、是否验证了金额与订单一致、是否处理了重复通知。很多支付篡改漏洞的根本原因是后端只信任了回调中的total_amount字段,而没有与本地订单金额做强一致校验。
五、SQL注入藏在排序、分页和报表参数里主流的ORM框架已经能防住大部分显式注入,但动态排序字段、分页参数、报表筛选条件仍然是重灾区。比如下面这种写法:
String sql = "SELECT * FROM users ORDER BY " + sortField + " " + sortOrder;
开发者可能认为sortField来自前端下拉框,只有固定几个值,但攻击者可以直接抓包修改。即使前端限制了可选值,后端如果没有白名单校验,注入就成立。类似的还有使用MyBatis的${}拼接、JPA的nativeQuery拼接、以及报表工具中直接拼接用户输入的过滤条件。审计时搜索所有SQL拼接点,特别是order by、group by、limit、like后面的参数。修复方式统一为:排序字段使用白名单映射,分页参数强制转为整数,过滤条件使用预编译参数绑定。
六、逻辑漏洞藏在状态机的“跳跃”里订单、工单、审批流这类有状态流转的业务,最容易出现状态跳跃。比如订单状态从“待支付”到“已支付”再到“已完成”,攻击者可能直接从“待支付”跳到“已完成”,绕过支付环节。审计时要画出完整的状态机图,逐一检查每个状态变更接口是否校验了前置状态。很多系统只校验了当前用户是否有权限操作该订单,却没有校验订单当前是否处于可操作的状态。另一个典型问题是并发条件下的条件竞争,例如同时发起多个“领取优惠券”请求,后端先查询库存再扣减,没有加锁或使用原子操作,导致超发。这类问题静态审计很难发现,需要结合代码逻辑判断是否存在“先查后改”且无锁的临界区。
七、隐藏的第三方组件和遗留代码很多项目引用了大量第三方SDK、内部公共库、以及历史版本遗留的旧模块。这些代码往往不在主业务审计范围内,但漏洞风险极高。比如一个已经下线的活动模块,其路由仍然存在,或者一个旧的图片上传接口使用了已经废弃的上传类库。审计时建议先梳理完整的路由清单和依赖清单,找出那些“没有入口但代码还在”的模块。对于第三方组件,重点检查是否有已知CVE漏洞版本,特别是fastjson、log4j、shiro、struts2这类高频组件。不要只看pom.xml或package.json里的直接依赖,还要看传递依赖,很多漏洞藏在间接引入的版本里。
八、敏感信息泄露藏在报错、日志和接口返回里很多系统在调试阶段会输出详细的异常堆栈,上线时虽然关闭了前端展示,但后端日志仍然记录了完整的SQL语句、请求参数、甚至用户密码。日志文件如果被其他接口读取、或者被错误地放在了Web目录下,就会造成严重泄露。审计时要检查全局异常处理器的返回内容,确认是否包含SQL语句、文件路径、内部类名、堆栈信息。同时检查日志配置,确认敏感字段是否做了脱敏。另一个容易被忽略的是接口返回了过多字段,比如用户列表接口返回了password_hash、salt、token等字段,前端虽然不展示,但攻击者直接看响应就能拿到。审计时对每个接口的返回结构做一次梳理,确保只返回前端必需的字段。
九、权限校验的“粒度”问题很多系统的权限控制只做到了菜单级,即用户能否访问某个页面,但页面内的按钮、接口、数据范围没有做细粒度控制。比如一个普通用户可以通过修改角色ID参数来访问管理员接口,或者通过修改部门ID来查看其他部门的数据。审计时不要只看是否有登录校验,要看每个接口是否校验了当前用户的操作权限和数据权限。特别是涉及“根据ID操作”的接口,要确认这个ID是否属于当前用户的数据范围。对于后台管理系统,建议统一使用注解或中间件做权限校验,避免在每个方法里手动判断,手动判断很容易遗漏。
十、前端校验与后端校验的“信任边界”问题前端的所有校验都可以被绕过,这是常识,但实际项目中仍然大量存在只在前端做校验的情况。比如注册时前端校验了手机号格式,后端直接入库;提交订单时前端校验了库存充足,后端直接扣减;修改资料时前端校验了文件大小,后端直接保存。审计时要把前端代码当作“不可信输入源”,逐项确认每个输入点在后端是否有对应的校验逻辑。特别是涉及金额、数量、权限、文件类型的字段,后端必须有独立校验。一个实用的审计方法是:用抓包工具直接修改请求参数,观察后端是否仍然正常处理,如果正常处理,说明该参数缺乏后端校验。
隐藏安全隐患的本质是“信任了不该信任的输入”和“忽略了不该忽略的边界”。代码审计不能只依赖自动化扫描器的规则库,必须结合业务逻辑做人工推演。一个有效的审计路径是:先梳理所有外部输入点,再追踪每个输入点在代码中的流转过程,最后确认每个流转节点是否做了充分的校验和过滤。对于发现的每一个问题,不要孤立地看,要思考它能否与其他问题组合形成攻击链。只有把隐藏的逻辑漏洞挖出来,才能真正降低系统被深入渗透的风险。
