一、问题现象
在自动化测试中,向接口/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 方法添加注解