防止字符串 ID 隐式转换导致的数据越权漏洞

一、问题现象

在自动化测试中,向接口/getInvoiceInfo发送如下请求:json编辑

json 复制代码
1{
2  "invoice_id": "1abc",
3  "appkey": "5FD913AA74124F88B98E3705E9D29AEF",
4  "sys_code": 200,
5  "token": "335806_992b4e0a922a427e9297bd8454427a1d",
6  "lang": ""
7}

预期结果 :应返回参数格式错误。实际结果:接口成功返回了invoice_id = 1的发票详情!


二、根本原因分析

2.1 MySQL 隐式类型转换机制

  • 数据库表 sys_user_invoice的主键 id字段为 数值类型(如 BIGINT)。
  • 当执行 SQL:sql编辑
ini 复制代码
1SELECT * FROM sys_user_invoice WHERE id = '1abc';
  • MySQL 会自动将字符串 '1abc' 转换为数字

    • 规则:从左到右读取,直到遇到非数字字符为止。
    • '1abc' → 转换为 1
  • 因此,实际等价于:sql编辑

ini 复制代码
1SELECT * FROM sys_user_invoice WHERE id = 1;

验证方法:在 MySQL 客户端执行SELECT '1abc' + 0;,结果为1。

2.2 应用层缺失输入校验

  • Controller 直接将 requestMap传递给 Service 和 Mapper。
  • 未对 invoice_id 的格式做任何合法性校验
  • 导致非法输入绕过业务逻辑,直接进入 SQL 查询。

2.3 前端 ID 传输规范的影响

  • 由于 JavaScript 的 Number类型存在精度限制(最大安全整数为 2^53 - 1),长整型 ID 必须以字符串形式传输
  • 因此,前端传 "1234567890123456789"是正确且必要的行为。
  • 但这也意味着后端不能要求参数是 Long 对象 ,而应校验其字符串内容是否为合法数字

三、风险评估

表格

风险类型 描述
数据越权 用户 A 传"2xyz"可能查看用户 B 的id=2发票
逻辑错误 系统误认为"1abc"是有效 ID,导致后续流程异常
安全漏洞 可被用于探测数据库记录(如通过"1","2", ... 枚举)

四、解决方案

4.1 核心原则

在应用层(Java)对关键参数进行强格式校验,杜绝非法输入进入 SQL 层。

4.2 推荐方案:通用参数校验注解 + AOP 拦截器

步骤 1:定义参数类型枚举

java编辑

vbnet 复制代码
1public enum ParamType {
2    LONG,      // 表示"可转为 Long 的正整数字符串"
3    INTEGER,
4    UUID,
5    EMAIL,
6    PHONE
7}

步骤 2:创建校验注解

java编辑

java 复制代码
1@Target(ElementType.METHOD)
2@Retention(RetentionPolicy.RUNTIME)
3public @interface ValidateParam {
4    String paramName();          // 要校验的 Map key,如 "invoice_id"
5    ParamType type();            // 期望类型
6    boolean required() default true; // 是否必填
7}

步骤 3:实现校验工具类(关键修复点)

java编辑

typescript 复制代码
1public class ParamValidator {
2
3    private static final Pattern LONG_PATTERN = Pattern.compile("^[1-9]\d*$"); // 正整数,无前导零
4
5    public static void validate(String paramName, Object value, ParamType type, boolean required) {
6        // 1. 处理空值
7        if (value == null || (value instanceof String && ((String) value).trim().isEmpty())) {
8            if (required) {
9                throw new BusinessException("PARAM_MISSING", "参数 [" + paramName + "] 不能为空");
10            }
11            return;
12        }
13
14        String strValue = String.valueOf(value).trim();
15        Objects.requireNonNull(type, "参数类型不能为 null");
16
17        // 2. 按类型校验(修复:校验后必须 return)
18        if (type == ParamType.LONG) {
19            if (!LONG_PATTERN.matcher(strValue).matches()) {
20                throw new BusinessException("INVALID_PARAM", "参数 [" + paramName + "] 必须为正整数字符串");
21            }
22            try {
23                Long.parseLong(strValue); // 防溢出
24            } catch (NumberFormatException e) {
25                throw new BusinessException("INVALID_PARAM", "参数 [" + paramName + "] 超出 Long 范围");
26            }
27            return; // ✅ 关键修复:避免执行到最后的 throw
28        }
29
30        // 3. 不支持的类型
31        throw new IllegalArgumentException("不支持的参数类型: " + type);
32    }
33}

🔥重点修复:在if (type == ParamType.LONG)块末尾添加return;,防止合法请求被误判。

步骤 4:AOP 切面拦截

java编辑

typescript 复制代码
1@Aspect
2@Component
3public class ParamValidationAspect {
4
5    @Around("@annotation(validateParam)")
6    public Object validate(ProceedingJoinPoint joinPoint, ValidateParam validateParam) throws Throwable {
7        String paramName = validateParam.paramName();
8        ParamType type = validateParam.type();
9        boolean required = validateParam.required();
10
11        for (Object arg : joinPoint.getArgs()) {
12            if (arg instanceof Map) {
13                @SuppressWarnings("unchecked")
14                Map<String, Object> requestMap = (Map<String, Object>) arg;
15                ParamValidator.validate(paramName, requestMap.get(paramName), type, required);
16                break;
17            }
18        }
19        return joinPoint.proceed(); // 校验通过,继续执行
20    }
21}

步骤 5:在 Controller 使用

java编辑

typescript 复制代码
1@ApiOperation(value = "查看发票详情")
2@RequestMapping(value = "/getInvoiceInfo", method = RequestMethod.POST)
3@ResponseBody
4@ValidateParam(paramName = "invoice_id", type = ParamType.LONG, required = true)
5public APIResponse getInvoiceInfo(@RequestBody Map<String, Object> requestMap) throws Exception {
6    // 原有业务逻辑,无需修改
7}

五、测试用例

表格

输入invoice_id required 预期结果
"1234567890123456789" true ✅ 成功
"1abc" true ❌INVALID_PARAM
null true ❌PARAM_MISSING
"" true ❌PARAM_MISSING
null false ✅ 跳过校验
"0" true ❌(因正则^[1-9]\d*$不匹配)

💡 注:若业务允许0,可调整正则为^[1-9]\d* <math xmlns="http://www.w3.org/1998/Math/MathML"> ∣ 0 |^0 </math>∣0。


六、为什么不用数据库或 SQL Mode 解决?

表格

方案 是否可行 原因
修改 MySQLsql_mode STRICT_TRANS_TABLES不影响WHERE中的隐式转换
在 SQL 中加类型转换 可能导致索引失效,性能下降
依赖数据库约束 无法阻止'1abc' → 1的语义转换

唯一可靠方案:应用层输入校验


七、总结

  • 问题根源:MySQL 隐式转换 + 应用层缺失校验。

  • 安全原则:永远不要信任前端输入,关键参数必须强校验。

  • 最佳实践

    • 长整型 ID 前后端均以字符串传输;
    • 后端校验字符串内容是否为合法数字;
    • 使用 AOP 实现通用、解耦的参数校验。

修复后效果

  • "1abc" → 拦截
  • "1234567890123456789" → 放行
  • 彻底杜绝因类型转换导致的数据越权风险。

附:相关代码文件清单

  • ParamType.java
  • ValidateParam.java
  • ParamValidator.java
  • ParamValidationAspect.java
  • Controller 方法添加注解
相关推荐
JavaGuide2 小时前
字节二面:Redis 能做消息队列吗?怎么实现?
redis·后端
暮色妖娆丶3 小时前
不过是吃了几年互联网红利罢了,我高估了自己
java·后端·面试
UrbanJazzerati3 小时前
Python Scrapling:小白也能轻松掌握的现代网页抓取工具
后端·面试
老张的码3 小时前
飞书 × OpenClaw 接入指南
人工智能·后端
希克厉4 小时前
记录安装wsl2踩的一个坑
后端
zone77394 小时前
004:RAG 入门-LangChain读取PDF
后端·python·面试
漫霂4 小时前
基于redis实现登录校验
redis·后端
zone77394 小时前
005:RAG 入门-LangChain读取表格数据
后端·python·agent
用户7344028193424 小时前
mysql如何存储boolean类型
后端