lim开源cms代码审计
字数 1679 2025-08-22 12:23:19
Lin-CMS-SpringBoot JWT安全漏洞分析与防御教学
1. 漏洞背景
Lin-CMS-SpringBoot是一套基于Java开发的开源内容管理系统。在代码审计过程中发现存在JWT(JSON Web Token)相关的验证逻辑问题,导致可以绕过权限控制访问后台接口,并且存在权限提升漏洞。
2. JWT机制分析
2.1 JWT基本结构
系统使用的JWT包含四个关键字段:
type: 固定值scope: 固定值identity: 用户身份标识(从数据库user表获取的user.getId())exp: JWT过期时间(标准JWT字段)
2.2 登录流程分析
-
用户访问
/cms/user/login接口进行登录 -
服务端验证流程:
- 检查用户名是否存在(不存在则抛出异常)
- 调用
verifyUsernamePassword方法验证用户名、密码和用户ID - 验证通过后调用
generateTokens方法生成JWT
-
generateTokens方法生成两个token:- access token
- refresh token
3. 漏洞详情
3.1 固定Secret漏洞(CVE-2022-32430)
问题描述:
- JWT的签名密钥(secret)在
application.yml中硬编码为固定值 - 所有使用相同secret的站点可以互相伪造token
影响:
- 攻击者获取任意一个站点的管理员token后,可以访问所有使用相同secret的站点
- 无需知道secret值,只需获取一个有效token即可
3.2 权限提升漏洞
问题描述:
identity字段直接使用数据库中的用户ID- 管理员通常为
identity=1 - 由于secret固定,可以伪造任意identity的token
攻击步骤:
- 获取低权限用户token
- 解码token获取结构
- 修改
identity为1(管理员) - 使用固定secret重新签名
- 使用伪造的token访问管理员接口
3.3 Token失效机制缺陷
问题描述:
- 系统仅验证token格式和签名有效性
- 不验证token是否在服务端存储的有效token列表中
- 即使用户主动退出登录,未过期的token仍然有效
4. 漏洞复现
4.1 跨站点token复用
- 准备两个使用相同secret的Lin-CMS站点(网站1和网站2)
- 在网站1上使用管理员账号登录,获取token
- 在网站2上抓取需要管理员权限的接口请求
- 将网站1的token添加到网站2的请求中
- 成功访问网站2的管理员接口
4.2 权限提升攻击
- 使用低权限账号登录,获取token
- 使用jwt.io等工具解码token
- 修改payload中的
identity为1 - 使用已知的secret重新签名
- 使用伪造的token访问管理员接口
5. 修复方案
5.1 安全配置建议
-
禁止使用固定secret:
- 每个部署实例应使用随机生成的唯一secret
- 可通过环境变量或配置中心动态获取secret
-
实现token黑名单机制:
// 示例:使用Redis存储失效但未过期的token @Component public class TokenBlacklist { @Autowired private RedisTemplate<String, String> redisTemplate; public void addToBlacklist(String token, long expiration) { redisTemplate.opsForValue().set(token, "invalid", expiration, TimeUnit.MILLISECONDS); } public boolean isBlacklisted(String token) { return redisTemplate.hasKey(token); } } -
增强identity验证:
// 在验证token时,额外检查用户状态和权限 public boolean validateToken(String token) { // 标准JWT验证 if (!Jwts.parser().setSigningKey(secret).parseClaimsJws(token)) { return false; } // 检查黑名单 if (tokenBlacklist.isBlacklisted(token)) { return false; } // 从token获取用户信息 Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); Long userId = claims.get("identity", Long.class); // 验证用户是否存在且状态正常 User user = userService.findById(userId); if (user == null || !user.isActive()) { return false; } return true; }
5.2 代码层修复
-
动态secret实现:
@Configuration public class JwtConfig { @Value("${jwt.secret}") private String secret; @Bean public String jwtSecret() { // 优先从环境变量获取 String envSecret = System.getenv("JWT_SECRET"); return StringUtils.isNotBlank(envSecret) ? envSecret : secret; } } -
增强token生成逻辑:
public Map<String, String> generateTokens(User user) { // 使用更复杂的identity构造方式 String identity = DigestUtils.md5Hex(user.getId() + System.currentTimeMillis() + randomSalt); long now = System.currentTimeMillis(); Date accessExpiration = new Date(now + accessTokenValidity); Date refreshExpiration = new Date(now + refreshTokenValidity); String accessToken = Jwts.builder() .setSubject(user.getUsername()) .claim("identity", identity) .claim("role", user.getRole()) .setIssuedAt(new Date(now)) .setExpiration(accessExpiration) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); // 存储identity到用户关联 userTokenService.storeTokenIdentity(user.getId(), identity); return Map.of( "accessToken", accessToken, "refreshToken", refreshToken ); }
6. 安全开发建议
-
JWT最佳实践:
- 使用足够强度的随机secret(至少256位)
- 设置合理的token过期时间
- 避免在token中存储敏感信息
- 实现token刷新机制
-
权限验证增强:
@PreAuthorize("hasPermission(#userId, 'USER', 'DELETE')") @DeleteMapping("/users/{userId}") public ResponseEntity deleteUser(@PathVariable Long userId) { // 方法实现 } -
安全审计要点:
- 定期检查secret管理方式
- 验证token失效机制有效性
- 检查权限验证是否全面
- 确保没有硬编码的敏感信息
7. 总结
Lin-CMS-SpringBoot的JWT实现存在多个安全隐患,主要源于固定secret和不足的验证逻辑。通过本教学文档的分析,开发者应当理解:
- 动态secret管理的重要性
- 完善的token验证机制的必要性
- 权限系统的严谨实现方式
- 安全审计的关键点
正确实现JWT认证需要综合考虑加密强度、状态管理、权限控制和失效机制等多个方面,才能构建真正安全的认证系统。