03-代码审计报告.md 18 KB

软件项目资源登记平台 — 代码审计报告

审计范围:后端全部 Java 代码、前端全部 Vue/JS 代码、配置文件、数据库 DDL
严重等级:🔴 严重 (Critical) · 🟠 高危 (High) · 🟡 中危 (Medium) · 🔵 低危 (Low) · ⚪ 建议 (Info)


一、安全问题

AUDIT-001 🔴 配置文件明文存储敏感凭据

文件:backend/src/main/resources/application.yml

问题描述:数据库密码、邮箱密码、JWT Secret Key 均以明文形式直接写在配置文件中。一旦代码仓库泄露,攻击者即可获取数据库和邮箱的完全访问权限,并伪造任意 JWT Token。

# 当前问题代码
spring:
  datasource:
    password: Cmt123456          # 数据库密码明文
  mail:
    password: Bosind2023          # 邮箱密码明文

jwt:
  secret: HKG2fedqr1BmynJl...   # JWT密钥明文

修复建议:

  • 使用环境变量 ${DB_PASSWORD} 或 ${MAIL_PASSWORD} 替代明文
  • 生产环境使用配置中心(如 Nacos/Apollo)或 K8s Secret 管理敏感配置
  • 在 .gitignore 中排除本地开发用的配置文件,或使用 application-local.yml 分离

AUDIT-002 🔴 JWT Secret 硬编码且无轮换机制

文件:application.yml L38、security/JwtUtil.java

问题描述:JWT 签名密钥直接写死在配置中,无定期轮换机制。一旦密钥泄露(如通过 Git 历史),攻击者可以永久伪造合法 Token 访问系统。

修复建议:

  • 密钥从环境变量读取,且生产环境使用独立的强随机密钥
  • 考虑引入 Token 黑名单机制(如 Redis 存储已注销的 Token)
  • 缩短 Token 有效期(当前 24 小时偏长,建议 2-4 小时 + Refresh Token 机制)

AUDIT-003 🟠 无登录频率限制(暴力破解风险)

文件:controller/AuthController.java L21-24、service/impl/SysUserServiceImpl.java L35-48

问题描述:登录接口 /auth/login 无任何频率限制或账号锁定机制。攻击者可以无限次尝试暴力破解密码,尤其配合弱密码(如 admin123)时风险极高。

修复建议:

  • 引入验证码机制(连续失败 N 次后要求验证码)
  • 实现 IP/账号维度的登录频率限制(如 Bucket4j、Redis + Lua 限流)
  • 连续失败 5 次后临时锁定账号 15-30 分钟

AUDIT-004 🟠 密码修改/用户禁用后旧 Token 未失效

文件:service/impl/SysUserServiceImpl.java L50-61、security/JwtAuthenticationFilter.java

问题描述:

  • 修改密码后仅前端清除 Token,服务端未将旧 Token 加入黑名单,旧 Token 在过期前仍可使用
  • 禁用/删除用户后,已发放的 Token 仍然有效直到自然过期
  • JwtAuthenticationFilter 每次请求都会查询数据库获取最新用户信息(loadUserByUsername),所以禁用用户实际上会立即生效(因为 LoginUser.isEnabled() 检查 status),但删除用户后 loadUserByUsername 会抛出 UsernameNotFoundException,Filter 没有捕获这个异常

修复建议:

  • JwtAuthenticationFilter 中 loadUserByUsername 调用应捕获 UsernameNotFoundException,用户不存在时跳过认证设置
  • 修改密码时记录密码变更时间戳,Token 签发时间早于变更时间的视为无效
  • 或引入 Redis Token 黑名单
// JwtAuthenticationFilter.java - 建议增加异常处理
try {
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    // ... 设置认证
} catch (UsernameNotFoundException e) {
    // 用户已删除,Token 无效,不设置认证
}

AUDIT-005 🟠 用户列表接口返回密码字段

文件:controller/UserController.java L24-26

问题描述:/user/list 和 /user/enabled-list 接口直接返回 SysUser 实体,响应 JSON 中包含 password 字段(虽然是 BCrypt 哈希值),属于不必要的信息泄露。

// 当前代码 - 直接返回包含密码的实体
@GetMapping("/list")
public Result<List<SysUser>> list() {
    return Result.success(sysUserService.list());
}

修复建议:

  • 创建 UserVO 类,排除 password 字段
  • 或使用 @JsonIgnore 注解标记 password 字段
  • 或使用 Jackson 的 @JsonView 按需控制序列化

AUDIT-006 🟠 无密码复杂度校验

文件:dto/PasswordUpdateDTO.java、dto/UserDTO.java

问题描述:密码校验仅为 @NotBlank(不为空),允许设置 "1"、"a" 等极弱密码。

修复建议:

  • 添加 @Size(min = 8, message = "密码长度不能少于8位") 注解
  • 后端增加密码复杂度校验(包含大小写字母 + 数字 + 特殊字符,至少满足 2 种)
  • 前端增加密码强度提示

AUDIT-007 🟠 无细粒度数据权限控制

文件:config/SecurityConfig.java、所有 Controller

问题描述:除用户管理外,所有接口仅需"登录"即可访问。普通用户可以:

  • 删除其他人创建的资源/项目
  • 查看所有资源(包括不属于自己负责的)
  • 修改其他人负责的项目信息
  • 随意操作字典(新增/删除资源类型、服务商)

修复建议:

  • 资源/项目的修改和删除操作应校验当前用户是否为负责人或管理员
  • 字典管理建议限制为管理员操作
  • 长期考虑实现 RBAC 权限模型

AUDIT-008 🟡 CORS 配置过于宽松

文件:config/SecurityConfig.java L61-70

问题描述:allowedOriginPatterns 设为 *(允许任意域名),生产环境可能被恶意网站利用进行 CSRF 攻击。

// 当前代码
config.setAllowedOriginPatterns(List.of("*"));  // 允许所有来源

修复建议:

  • 生产环境改为具体的前端域名
  • 通过配置文件管理允许的来源列表

AUDIT-009 🟡 无接口限流防护

文件:所有 Controller

问题描述:所有 API 接口均无请求频率限制,面临以下风险:

  • 恶意用户大量调用导出接口消耗服务器资源
  • 暴力请求分页接口进行数据爬取
  • 资源列表导出可被用于 DDoS 攻击向量

修复建议:

  • 对敏感接口(导出、登录)添加限流注解
  • 使用 Bucket4j 或 Guava RateLimiter 进行全局限流
  • 考虑在 Nginx 层做请求限速

二、业务逻辑缺陷

AUDIT-010 🟠 删除字典项无引用检查

文件:controller/ResourceTypeController.java L29-32、controller/ProviderController.java L29-32

问题描述:删除资源类型或服务商时,未检查是否还有资源在使用该字典项。删除后会导致:

  • 已有资源的 typeId / providerId 指向不存在的记录
  • 资源列表的 VO 组装时 typeNameMap.get(r.getTypeId()) 返回 null,前端显示空值
  • 资源编辑时类型/服务商下拉选中项消失

修复建议:

// ResourceTypeController.java - 删除前检查引用
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
    long count = resourceService.count(
        new LambdaQueryWrapper<Resource>().eq(Resource::getTypeId, id));
    if (count > 0) {
        throw new BusinessException("该类型下还有 " + count + " 个资源,无法删除");
    }
    resourceTypeService.removeById(id);
    return Result.success();
}

AUDIT-011 🟠 删除用户无关联检查

文件:service/impl/SysUserServiceImpl.java L87-89

问题描述:删除用户时未检查:

  • 该用户是否为某些资源/项目的负责人
  • 删除后资源的 ownerId 指向不存在的用户,VO 组装时 ownerName 为 null
  • 到期提醒定时任务中 sysUserService.getById(resource.getOwnerId()) 返回 null,该资源的到期邮件将不再发送

修复建议:

  • 删除前检查关联的资源/项目数量,提示用户先转移负责人
  • 或改为"禁用"而非"删除"

AUDIT-012 🟡 费用统计全量加载到内存计算

文件:service/impl/ResourceServiceImpl.java L251-294

问题描述:costStatByProject() 和 costStatByType() 两个方法都执行 resourceMapper.selectList(null) 全量查询所有资源到内存中进行 Stream 分组聚合。当资源数量增长到万级时,会造成:

  • 大量内存消耗(加载全部实体对象)
  • 数据库 I/O 压力(全表扫描)
  • GC 压力增大

修复建议:

-- 改为 SQL 聚合查询
SELECT rt.name AS dimension_name, 
       COUNT(r.id) AS resource_count, 
       COALESCE(SUM(r.cost), 0) AS total_cost
FROM resource r
LEFT JOIN resource_type rt ON r.type_id = rt.id
WHERE r.deleted = 0
GROUP BY rt.name
ORDER BY total_cost DESC

AUDIT-013 🟡 无日期合理性校验

文件:dto/ResourceDTO.java、service/impl/ResourceServiceImpl.java

问题描述:资源表单中 expireDate(到期日期)可以早于 startDate(开通日期),系统不做校验,导致逻辑上的"已开通但已过期"异常数据。

修复建议:

  • Service 层增加校验:if (expireDate != null && startDate != null && expireDate.isBefore(startDate)) throw new BusinessException("到期日期不能早于开通日期")

AUDIT-014 🟡 操作日志 userId 可能为 null

文件:service/impl/OperationLogServiceImpl.java L16-24

问题描述:record() 方法通过 SecurityUtils.getCurrentUserId() 获取操作人 ID。在以下场景中会返回 null:

  • 定时任务上下文中执行(无 HTTP 请求上下文)
  • 系统内部调用的批量操作

虽然当前定时任务未调用 record(),但如果后续在 refreshExpireStatus() 中增加日志记录,会导致 user_id 为 null 违反数据库非空约束。

修复建议:

  • operation_log.user_id 字段改为允许 NULL(或使用一个系统用户 ID 如 0)
  • record() 方法增加 userId 参数,调用方显式传入

AUDIT-015 🟡 项目/资源删除时关联表物理删除

文件:service/impl/ResourceServiceImpl.java L215-222、service/impl/ProjectServiceImpl.java L122-128

问题描述:resource 和 project 表使用逻辑删除(deleted 字段),但删除时对应的 project_resource 关联记录是物理删除。如果后续恢复已删除的资源/项目,关联关系已经永久丢失。

修复建议:

  • 不删除关联记录,改为在查询时根据两端的 deleted 状态过滤
  • 或关联表也引入逻辑删除

三、性能问题

AUDIT-016 🟡 前端全量加载下拉选项

文件:

  • views/project/ProjectDetail.vue L89-91:pageSize: 1000 加载全部资源
  • views/resource/ResourceForm.vue L138-145:每次打开弹窗加载 4 个接口的全量数据
  • views/resource/ResourceList.vue L160-164:页面加载时全量加载类型和用户列表

问题描述:多处使用全量加载方式获取下拉选项数据。虽然代码注释中提到"内部系统数据量级不大",但随着业务增长会成为瓶颈。

修复建议:

  • 对资源/项目选择改为远程搜索 + 分页加载(Element Plus 的 el-select 支持 remote + remote-method)
  • 对字典类数据(类型、服务商、用户)可引入前端缓存(Pinia Store + TTL)

AUDIT-017 🔵 工作台仪表盘查询可优化

文件:service/impl/DashboardServiceImpl.java L32-64

问题描述:工作台接口执行了 4 次数据库查询:

  1. projectMapper.selectCount(null) — 项目总数
  2. resourceMapper.selectCount(null) — 资源总数
  3. resourceMapper.selectCount(status=2) — 即将到期数
  4. resourceMapper.selectCount(status=3) — 已过期数
  5. resourceMapper.selectList(status in 2,3) — 关注列表
  6. resourceMapper.selectList(null) — 全量资源用于类型分布统计

可以合并优化为 1-2 次查询。


四、配置与部署问题

AUDIT-018 🟡 生产环境不应开启 SQL 日志和 debug 日志

文件:application.yml L28-29、L49-51

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 每条 SQL 打印到 stdout

logging:
  level:
    com.example.resourceplatform: debug  # debug 级别日志

问题描述:

  • SQL 日志输出到 stdout 会严重影响性能(同步 I/O),且可能泄露敏感数据(查询中的参数值)
  • debug 级别日志量大,产生大量磁盘 I/O

修复建议:

  • 使用 Spring Profile 分离:application-dev.yml 开 debug + SQL 日志,application-prod.yml 关闭
  • 生产环境日志级别设为 info 或 warn

AUDIT-019 🔵 Vite 开发服务器 host: true 的安全风险

文件:frontend/vite.config.js L14

host: true,  // 监听 0.0.0.0,局域网内其他设备也能访问

问题描述:开发服务器监听 0.0.0.0,在同一网络下的任何设备都可以访问开发环境,如果开发环境连接的是生产数据库则风险极高。

修复建议:仅在需要局域网调试时开启,平时使用默认的 localhost


五、代码质量问题

AUDIT-020 🔵 状态码使用魔法数字,缺乏枚举管理

文件:多处

问题描述:代码中大量使用魔法数字表示状态,分散在各处,容易不一致:

位置 含义
Resource.status 1-使用中 2-即将到期 3-已过期 0-已停用
Project.status 1-进行中 0-已下线
SysUser.role 1-管理员 2-普通用户
SysUser.status 1-启用 0-禁用
Resource.costCycle 1-月付 2-年付 3-一次性
ExpireNotifyLog.notifyStage 1-提前30天 2-提前7天 3-已过期

修复建议:

  • 为每组状态值创建枚举类(如 ResourceStatus、ProjectStatus、UserRole)
  • 在 Entity 中使用枚举类型字段
  • 前后端共享统一的常量定义

AUDIT-021 🔵 前端表单编辑时可能提交多余字段

文件:views/project/ProjectList.vue L133

function openEdit(row) { Object.assign(form, row); formVisible.value = true }

问题描述:将列表行数据(包含 resourceCount、ownerName 等展示字段)整体拷贝到表单对象中。提交时这些额外字段会一并发送到后端。虽然后端使用 ProjectDTO 接收不会造成错误,但属于不规范的数据传递。

修复建议:

function openEdit(row) {
  Object.assign(form, { id: row.id, name: row.name, ownerId: row.ownerId, status: row.status, remark: row.remark })
  formVisible.value = true
}

AUDIT-022 🔵 前端多处重复的 statusClass 函数

文件:Dashboard.vue L78-80、ResourceList.vue L109-111、ProjectDetail.vue L79-81

问题描述:相同的 statusClass 函数在多个组件中重复定义,违反 DRY 原则。

修复建议:抽取为全局工具函数或 composable,如 utils/status.js。


AUDIT-023 🔵 操作日志缺少筛选功能

文件:controller/OperationLogController.java、views/system/OperationLog.vue

问题描述:操作日志仅支持分页,不支持按模块、操作类型、操作人、时间范围等筛选,日志量增大后难以定位特定操作。

修复建议:增加查询参数:module、action、userId、startTime、endTime


AUDIT-024 ⚪ 前端 handleSubmit 缺少 catch 错误处理

文件:views/project/ProjectList.vue L143-150、views/CostStat.vue L41-49

问题描述:部分表单提交仅使用 try/finally 而无 catch,虽然 Axios 拦截器已做全局错误提示,但 finally 中重置 loading 状态可能在网络超时时产生误导。

修复建议:保持统一的 try/catch/finally 模式。


AUDIT-025 ⚪ @EnableScheduling 在测试环境会触发定时任务

文件:ResourcePlatformApplication.java L8

问题描述:@EnableScheduling 在应用启动时即生效,如果有集成测试启动完整 Spring Context,定时任务也会被触发执行。

修复建议:

  • 将 @EnableScheduling 改为 @Profile("!test") 条件化启用
  • 或在测试中使用 @MockBean 替换定时任务

六、SQL 注入与数据访问安全

✅ 无 SQL 注入风险

所有数据库查询均使用 MyBatis-Plus 的 LambdaQueryWrapper 或 ServiceImpl 内置方法,参数通过预编译方式绑定,不存在 SQL 注入风险。

✅ 无 XSS 风险

前端使用 Vue 3 模板语法,默认对所有插值表达式进行 HTML 转义,且未使用 v-html 指令,不存在 XSS 风险。

✅ 事务使用正确

ResourceServiceImpl.saveOrUpdate()、delete()、relateToProject() 和 ProjectServiceImpl.saveOrUpdate()、delete() 等涉及多表操作的方法均正确使用了 @Transactional 注解,保证数据一致性。

✅ 全局异常处理完善

GlobalExceptionHandler 正确捕获了 BusinessException、参数校验异常、认证异常、权限异常和未知异常,不会将原始堆栈信息泄露给前端。


七、审计结果汇总

严重等级 数量 编号
🔴 严重 2 AUDIT-001, AUDIT-002
🟠 高危 7 AUDIT-003~007, AUDIT-010, AUDIT-011
🟡 中危 5 AUDIT-008~009, AUDIT-012~014, AUDIT-018
🔵 低危 5 AUDIT-015~017, AUDIT-020~023
⚪ 建议 2 AUDIT-024, AUDIT-025

修复优先级建议

第一优先级(立即修复):

  1. AUDIT-001 — 敏感凭据外部化
  2. AUDIT-002 — JWT 密钥安全
  3. AUDIT-005 — 用户列表排除密码字段
  4. AUDIT-004 — JwtAuthenticationFilter 异常处理

第二优先级(近期迭代):

  1. AUDIT-003 — 登录频率限制
  2. AUDIT-006 — 密码复杂度校验
  3. AUDIT-007 — 数据权限控制
  4. AUDIT-010/011 — 删除操作引用检查
  5. AUDIT-018 — 生产环境配置分离

第三优先级(持续优化):

  1. AUDIT-008/009 — CORS + 限流
  2. AUDIT-012 — 费用统计性能优化
  3. AUDIT-016 — 前端下拉远程搜索
  4. 其余低危和建议项