不通过(安全维度)。 存在 5 项阻断级问题:①88/120 个后台 Controller 完全无鉴权且匿名可达;②admin.skip-captcha=true 使管理员登录验证码校验整体失效;③wechat.test-mode=true(含生产 profile)使验证码固定为 123456、微信手机号可直出,构成任意账号接管;④数据库口令 / JWT 密钥 / 微信 appsecret / 支付私钥 / 京东云 AK-SK / LLM Key 等明文密钥连同微信支付商户私钥证书被提交进 Git(含历史);⑤/api/stats/** 无鉴权且 /api/stats/child/overview 无归属校验,匿名可读全平台经营数据、用户手机号与任意儿童私有数据。安全扫描未实测(离线档未获授权,未运行 SAST/SCA/DAST 工具),本报告全部结论来自静态证据,不断言"系统不存在其他漏洞"。
| # | 证据 | 判定 |
|---|---|---|
| A1 | cfc-backend/src/main/java/com/etotem/cfc/config/JwtConfig.java:62-77 — signWith(getSigningKey(), HS256) + parseClaimsJws(token) |
正面:alg=none/算法混淆被正确拒绝 |
| A2 | JwtConfig.java:21-25 — secret 取自 ${jwt.secret};cfc-backend/src/main/resources/application.yml:68 明文默认密钥;cfc-backend/config/application-prod.yml:17 明文 prod 密钥 |
阻断 密钥入库 |
| A3 | JwtConfig.java:79-91 + application.yml:73(expiration: 2592000000 = 30 天) |
无吊销/刷新/黑名单,30 天令牌不可回收 |
| A4 | JwtInterceptor.java:37-82 — PUBLIC_PATHS 实为 45 条(任务书"8 个公开路径"与实际不符),含 /api/mcp/**、/api/growth/**、/api/articles/** 全量 |
白名单远大于预期 |
| A5 | JwtInterceptor.java:139-149 — 无 token 且路径不在 LOGIN_REQUIRED_PATHS → 降级为匿名(userId=-1)并放行,非 401 |
匿名可达面 = 全部未列入 LOGIN_REQUIRED 的端点 |
| A6 | JwtInterceptor.java:85-101 — LOGIN_REQUIRED_PATHS 仅 15 条,不含 /api/admin/**、不含 /api/stats/** |
匿名可达后台与统计 |
| A7 | JwtInterceptor.java:240-241 — 解析失败时 log.warn(... token={}) 打印 token 前 30 字符 |
token 片段入日志 |
| A8 | JwtInterceptor.java:127 — token = request.getParameter("token") |
token 可走 URL → 落入 access log / Referer |
| A9 | AuthController.java:120-140 — /api/auth/auto-login(裸 openid 换 token)已返回 410 |
正面 已修复 |
| # | 证据 | 判定 |
|---|---|---|
| B1 | cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ 共 120 个 Controller;grep -LE 'RequestAttribute("role")\|PreAuthorize\|AdminRoles\.\|isAdmin' 命中 88 个完全无任何角色校验 |
阻断 |
| B2 | controller/admin/AdminEarningsController.java:33-39 — familyEarningsMapper.selectList(null),无 userId/role 参数 |
匿名可读全部家庭收益 |
| B3 | controller/admin/AdminSystemPromptController.java:33-52(save)、:65-73(delete)、:85-97(rollback)— 无鉴权 |
匿名可篡改/删除/回滚系统提示词 → AI 行为劫持 |
| B4 | controller/stats/StatsController.java:89-90(/dashboard)、:122(/overview)、:191(/revenue)、:225(/membership)、:257(/trend)、:420(/traffic) — 均无 @RequestAttribute("role") |
匿名可读平台经营数据 |
| B5 | StatsController.java:669-676 — 用户名回落到 u.getPhone(),放进 userDwellList |
匿名可拉全站用户手机号(PII 泄露) |
| B6 | StatsController.java:292-294 — /child/overview 仅接 memberId,无 userId/familyId 归属校验,直查 familyMemberMapper.selectById(memberId) + pointsLogMapper + taskService.getAllTasksByChild(memberId) |
IDOR:任意 memberId 可读他人孩子积分/任务/游戏轨迹 |
| B7 | controller/family/FamilyMembersController.java:237-248 → service/FamilyMemberService.java:952-963 — isEditable(Long memberId) 不接受 userId |
跨家庭成员存在性/绑定关系探测 |
| B8 | config/SecurityConfig.java:30-41 — authorizeRequests().anyRequest().permitAll() + csrf().disable() |
Spring Security 形同虚设,鉴权 100% 依赖手写代码 |
| B9 | config/WebConfig.java:145-179 — FamilyAccessInterceptor 对 /api/admin 整体跳过(FamilyAccessInterceptor.java:42,52-54) |
后台端点不受家庭隔离保护 |
| B10 | JwtInterceptor.java:37-82 — /api/admin-auth/login、/api/admin-auth/login-by-password 在公开白名单 |
登录接口本身匿名可达(配合 D2 成灾) |
| # | 证据 | 判定 |
|---|---|---|
| C1 | cfc-backend/src/main/resources/mapper/**.xml 检索 ${ → 0 命中;util/SortUtil.java:35,170-184 用白名单正则 ^[a-zA-Z_][a-zA-Z0-9_]*$ 校验排序列后才 wrapper.last() |
正面 SQL 注入面收敛 |
| C2 | controller/HealthReportController.java:820-831(/report/upload)— 仅 filename.endsWith(".pdf") OR contentType=="application/pdf",无 %PDF- 魔数校验,随后 pdfParseService.parse(file.getInputStream()) 直接解析 |
上传即解析,类型可伪造 |
| C3 | controller/HealthReportController.java:914-945(/report/upload-only)— 图片分支仅靠 isImageContent(contentType),无服务端白名单 |
类型可由客户端伪造 |
| C4 | service/impl/LocalDiskStorageService.java:66-75 与 :97-105 — 扩展名取自 file.getOriginalFilename() 未剥离路径分隔符,uploadPath.resolve(filename) 未做 normalize() 越界校验 |
理论路径穿越 |
| C5 | 后端 无 Jsoup / OWASP Java HTML Sanitizer / AntiSamy(pom.xml 无相关依赖,grep -rni "jsoup\|antisamy\|htmlsanitiz" 0 命中);前端 无 DOMPurify(cfc-web/package.json、cfc-frontend/package.json 均无) |
无 HTML 净化 |
| C6 | controller/admin/AdminArticleController.java:104、:197 — article.setContent((String) body.get("content")) 原样入库;cfc-web/src/views/admin/review/ArticleAudit.vue:80 — v-html="currentRow.content" |
存储型 XSS |
| C7 | cfc-web/src/views/admin/ProxyReportUpload.vue:59、:121(v-html="analysisResult" / v-html="detailReport.aiAnalysis")、cfc-web/src/views/teacher/ProxyReport.vue:75、:158 |
AI 报告内容(源自用户上传 PDF)未净化即渲染 → XSS |
| C8 | config/SecurityHeadersFilter.java:26-28 — CSP script-src 'self' 'unsafe-inline' 'unsafe-eval' |
削弱 XSS 缓解,注入可直接执行 |
| # | 证据 | 判定 |
|---|---|---|
| D1 | git ls-files --error-unmatch cfc-backend/src/main/resources/application.yml → 已跟踪;下列明文口令全部入库:application.yml:24(DB 口令)、:49(sfms DB 口令)、:68(JWT 密钥默认)、:89(微信 appsecret)、:91-92(mch-key/api-v3-key)、:100-101(虚拟支付 appkey)、:103(虚拟支付 token)、:116-117(dify api-key)、:124(callback-secret)、:150(opencode 管理口令)、:179-180(京东云 access-key/secret-key) |
阻断(值已脱敏,不在此复述) |
| D2 | cfc-backend/src/main/resources/cert/apiclient_key.pem、apiclient_cert.pem、apiclient_cert.p12 — git ls-files cfc-backend/src/main/resources/cert/ 全部已跟踪;.gitignore 未覆盖 cert/(grep -n "cert\|pem\|p12" .gitignore 无命中) |
阻断 微信支付商户私钥入库 |
| D3 | git ls-files cfc-langgraph/ → .env 已跟踪,含 LLM_API_KEY / EMBEDDING_API_KEY / LANGCHAIN_API_KEY |
阻断 LLM 凭据入库 |
| D4 | git log --all --diff-filter=A -- cfc-backend/config/application-prod.yml → 389615495 已新增入历史;该版本含明文 DB 口令、JWT 密钥、test-mode: true |
阻断 生产配置进 git 历史(当前工作树已 untracked,但历史仍在) |
| D5 | application.yml:98 — test-mode: true(无 ${WECHAT_TEST_MODE:...} 占位);cfc-backend/config/application-prod.yml:37 — 生产 profile 同样 test-mode: true |
阻断 |
| D6 | service/WechatService.java:115-124 — 启动守卫 checkTestModeAtStartup() 仅当 spring.profiles.active=="prod" 才抛异常;application.yml 未设 spring.profiles.active(grep 无命中);cfc-backend/startup.sh:17 — PROFILE="${1:-dev}";cfc-backend/start.sh:11 — ENV="dev" |
阻断 守卫默认不触发 |
| D7 | service/VerificationCodeService.java:78-83 — if (testMode && "123456".equals(code)) return true; |
阻断 验证码固定 |
| D8 | service/WechatService.java:209-220 — testMode 下 getPhoneNumber 把 code 原样当手机号(test_<手机号> → 真实手机号) |
阻断 |
| D9 | controller/auth/AuthController.java:67-84(/api/auth/phone-login,公开白名单 JwtInterceptor.java:39)→ VerificationCodeService.verifyCode |
阻断 任意手机号 + 123456 → 账号接管 |
| D10 | application.yml:212 — admin.skip-captcha: true;controller/admin/AdminAuthController.java:107-114 — if (!skipCaptcha) { verifyCode(...) },skipCaptcha 时校验分支整体被跳过 |
阻断 仅凭手机号即得 admin JWT |
| D11 | controller/admin/AdminAuthController.java:337-355 — /api/admin-auth/account-roles 公开(JwtInterceptor.java:62),按手机号返回后台角色列表 |
严重 管理员账号枚举 |
| D12 | controller/admin/AdminAuthController.java:374-383 与 config/RateLimitInterceptor.java:174-186 — 直接采信客户端 X-Forwarded-For / X-Real-IP |
严重 限流与登录锁定可绕过 |
| D13 | config/RateLimitInterceptor.java:165-172 — path.contains(entry.getKey()) 子串匹配 + ConcurrentHashMap 迭代顺序不确定 → /api/auth/send-code(60s/1 次)可能被 /api/auth/(1s/10 次)规则抢占 |
严重 验证码防爆破强度不确定 |
| D14 | config/WebConfig.java:44-52 — CORS 显式域名白名单(非 *,可接受),但 allowedHeaders("*") + allowCredentials(true),且混入多个 http:// 明文源与 http://111.228.6.214(裸 IP) |
一般 |
| D15 | config/WebConfig.java:55-68 — 全局 Access-Control-Allow-Private-Network: true 响应头 |
一般 放宽 Chrome PNA |
| D16 | pom.xml/application.yml 无 actuator、无 h2console 依赖或配置(grep 0 命中) |
正面 |
| D17 | controller/admin/AdminAuthController.java:410-425 — 兼容旧 MD5 密码哈希(xzyj_salt_ / xzyj_admin_salt_) |
严重 弱哈希存量口令风险 |
| D18 | application.yml:22,47 — JDBC URL 含 useSSL=false&allowPublicKeyRetrieval=true;config/application-prod.yml:17-21 sfms 指向公网 RDS bianwoyou.mysql.rds.aliyuncs.com |
严重 传输无 TLS |
| # | 证据 | 判定 |
|---|---|---|
| E1 | cfc-backend/pom.xml:18 — spring-boot-starter-parent 2.7.18 |
严重 OSS 支持已于 2023-11 结束,2.7.x 存在多条已知 CVE |
| E2 | pom.xml:171 — pdfbox 2.0.31;且 C2 显示上传 PDF 直接解析该库 |
严重 陈旧 + 处于攻击面 |
| E3 | pom.xml:152 fastjson 2.0.40;:145 okhttp 3.14.9;:185 aws-java-sdk-s3 1.11.136(v1 线已 EOL);:178 springdoc-openapi-ui 1.8.0;:24 mybatis-plus 3.5.3.1;:25 jjwt 0.11.5 |
一般/严重 陈旧 |
| E4 | cfc-web/package-lock.json 解析版本:vue 2.7.16(Vue 2 已 EOL)、element-ui 2.15.14、jsplumb 2.15.6、axios 1.16.0 |
一般 |
| E5 | cfc-frontend/package.json — postcss 7.0.39(CVE-2023-44270 影响 <8.4.31)、jest 26.6.3 |
一般 |
| E6 | cfc-langgraph/requirements.txt — chromadb==0.5.23、deepface>=0.0.90、faster-whisper==1.2.1、av==18.1.0、PyPDF2>=3.0 |
严重 深度学习/媒体栈陈旧且体量大 |
| E7 | 无 CI 流水线:.github/、.gitlab-ci.yml、Jenkinsfile、.circleci/、azure-pipelines.yml 均不存在(ls -d 全部未命中);对全部 git 跟踪文件的穷举复核:git ls-files \| grep -iE "^\.github/\|\.gitlab-ci\|Jenkinsfile\|^\.circleci/\|azure-pipelines" → 0 命中 |
阻断(安全测试完全未接入研发流程) |
| E7b | 穷举复核全部被跟踪的 yml/yaml/xml/json/sh/bat/ps1/gradle 文件,grep -liE "semgrep\|gitleaks\|snyk\|trivy\|dependency-check\|codeql\|spotbugs\|sonar" → 0 命中(唯一相关脚本 tools/check-test-mode.ps1 见 E9) |
无任何 SAST/SCA/密钥扫描工具被引用 |
| E8 | githooks/pre-push — 仅执行 mvn -q -DskipTests compile;且需人工 git config core.hooksPath githooks 才生效;XAF_CI_GATE=0 可跳过 |
严重 无任何测试/安全扫描闸门 |
| E9 | tools/check-test-mode.ps1:13-18 — 扫描路径写死 cfc-backend/src/main/resources/,而实际 prod 配置在 cfc-backend/config/;src/main/resources/ 下无 application-prod*.yml → 命中 if ($prodYamlFiles.Count -eq 0) { "Skipping check"; exit 0 } |
严重 test-mode 守卫恒为假通过 |
| E10 | 锁文件:cfc-web/package-lock.json ✓、cfc-frontend/package-lock.json ✓(均存在且被跟踪) |
正面 |
| E11 | 后端 189 个测试类中仅 ~15 个涉及鉴权关键词(FamilyAccessInterceptorTest、JwtInterceptorLoginRequiredTest、GuideRolePermissionTest 等),无一条覆盖 admin 端点鉴权 / 匿名可达 / 密钥泄露 |
严重 安全测试覆盖缺口 |
| # | 证据 | 判定 |
|---|---|---|
| F1 | service/VerificationCodeService.java:67、:88、:112 — 验证码与手机号明文入日志 |
严重 |
| F2 | service/WechatService.java:364、:384 — log.error("...{}", json.toJSONString()) 打出微信响应(含 access_token) |
严重 |
| F3 | config/JwtInterceptor.java:240-241 — token 前 30 字符入日志 |
严重 |
| F4 | service/impl/LocalDiskStorageService.java 依赖客户端文件名扩展名;upload.allowed-types(application.yml:172)与 FileStorageService.java:22 的默认值不一致(yml 含 pdf/audio,代码默认仅 4 种图片) |
一般 配置漂移 |
① 问题描述:/api/admin/** 下 120 个 Controller 中 88 个完全没有角色校验;JwtInterceptor 对无 Token 请求降级为匿名(userId=-1)并放行,且 /api/admin/** 不在 LOGIN_REQUIRED_PATHS 内。Spring Security 又配置为 anyRequest().permitAll()。三者叠加使后台端点对互联网匿名开放。
② 测试 bug 点:
cfc-backend/src/main/java/com/etotem/cfc/config/SecurityConfig.java:30-41cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java:85-101、:139-149cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminEarningsController.java:33-39cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSystemPromptController.java:33-52、:65-73、:85-97AdminCategoryController / AdminInventoryController / AdminNoticeController / AdminOrderController / AdminProductController / DataMigrationController / MigrationController / SysMenuController / OperationLogController / KnowledgeBaseController 等 88 个③ 最优修改方案:
JwtInterceptor 中新增统一后台网关:路径以 /api/admin/ 开头时,token 缺失或 roles 不含任一 AdminRoles.BACKEND_ROLES → 直接 writeUnauthorized/sendForbidden,绝不降级匿名。JwtInterceptor 的"降级匿名"改为默认拒绝:LOGIN_REQUIRED_PATHS 改为默认全部需要登录 + 显式白名单(当前是反的),把 45 条 PUBLIC_PATHS 收敛为真正的公开接口。SecurityConfig 追加 authorizeRequests().antMatchers("/api/admin/**").hasAnyRole(...)(或最简:全部 authenticated()),并开启 @EnableGlobalMethodSecurity 的方法级校验。@PreAuthorize("hasAnyRole('admin','operator','finance','butler','knowledge_admin','teacher','nutritionist','supplier','activity_manager','org_admin')")(按最小权限细分),或引入 AdminRoles.hasAnyBackendRole(roles) 守卫。WebConfig.addInterceptors 增加一个 AdminAuthInterceptor,按前缀拦截,不依赖逐个改 Controller。④ 修改目的:把"认证+授权"从散落的手写 if 收敛为默认拒绝的单一闸门,达成"漏洞可见 → 不可利用";消除匿名可达后台这一最宽的暴露面。
⑤ 修改后能否做问题闭环:能。复测方式:① 新增 AdminEndpointAuthTest,对 /api/admin/** 全量路由做参数化扫描,断言「无 Token → 401」「普通 parent Token → 403」「admin Token → 2xx」;② 在 CI 中加一条"路由 × 角色矩阵"守卫测试,新增 Controller 未声明角色即失败。通过标准:/api/admin/** 匿名 100% 返回 401/403,角色矩阵全覆盖无空缺。
admin.skip-captcha=true 使管理员登录验证码校验整体失效① 问题描述:AdminAuthController.login 中验证码校验被 if (!skipCaptcha) 包裹,而主配置 application.yml:212 为 admin.skip-captcha: true。结果:只要知道任意一个管理员手机号,提交任意 code 即可换到带 admin 角色的 JWT,验证码形同不存在。该端点本身在公开白名单(JwtInterceptor.java:59-62)。
② 测试 bug 点:
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java:94-158,特别是 :107-114cfc-backend/src/main/resources/application.yml:212cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java:59-62cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java:268-331(login-by-password 走 MD5 兼容,见 P10/D17)③ 最优修改方案:
admin.skip-captcha 改为 ${ADMIN_SKIP_CAPTCHA:false},且生产 profile 强制 false;application.yml 不得出现 true。skipCaptcha 分支从"跳过验证码"改为"跳过图形验证码但仍必须校验短信验证码"——即 skipCaptcha 只控制图形挑战,绝不能控制短信码比对。skipCaptcha=true 时禁止发放令牌,只允许"降级为短信码登录"(短信码仍校验)。/api/admin-auth/login 增加独立的强限流(见 P10)与"同 IP 多手机号尝试"告警。④ 修改目的:消除"知道手机号即接管后台"这一完整认证绕过,使后台入口具备真正的持有性证明(短信码/密码)。
⑤ 修改后能否做问题闭环:能。复测方式:新增 AdminAuthCaptchaTest —— 生产配置下 POST /api/admin-auth/login {phone:"<任一后台账号手机号>", code:"000000"} 必须返回"验证码错误",且 skipCaptcha 只影响图形挑战、短信码仍校验;再用 code=真实短信码 断言可成功。通过标准:无效 code 100% 被拒,且不存在任何 code 值可绕过。
wechat.test-mode=true(生产 profile 亦为 true)→ 验证码固定 123456 + 手机号直出 = 任意账号接管① 问题描述:生产 profile 与主配置都是 test-mode: true。该开关一旦生效:① 任何手机号 + 验证码 123456 即通过 /api/auth/phone-login;② /api/auth/wechat-phone-login 的 code 可直接写成 test_<受害者手机号> 换取该手机号;③ 支付/订阅走 mock 不真实扣费。启动期守卫 checkTestModeAtStartup() 只在 spring.profiles.active=="prod" 时抛异常,而配置里从未设置 spring.profiles.active,启动脚本还默认 dev。
② 测试 bug 点:
cfc-backend/src/main/resources/application.yml:98(test-mode: true,无环境变量占位)cfc-backend/config/application-prod.yml:37(生产 profile 同为 true)cfc-backend/src/main/java/com/etotem/cfc/service/VerificationCodeService.java:78-83("123456" 直通)、:66-70cfc-backend/src/main/java/com/etotem/cfc/service/WechatService.java:115-124(守卫条件)、:209-220(手机号直出)cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java:67-84、:90-101cfc-backend/startup.sh:17、cfc-backend/start.sh:11cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java:95-96,111,278,436,479,504③ 最优修改方案:
test-mode 改为 ${WECHAT_TEST_MODE:false},默认值 false;application-prod.yml 显式 test-mode: false。spring.profiles.active ∈ {dev,local,test} 且 testMode=true 时才允许启动;其它 profile(含 prod / 未设置 profile / 拼写错误)一律 fail-fast 抛异常。同时把 activeProfile 默认值从 dev 改为空并对空值 fail-fast。VerificationCodeService.verifyCode 的 123456 直通分支删除,或收紧为"仅当 wechat.test-mode=true 且 spring.profiles.active ∈ {dev,local,test}";建议直接删除并改由测试 profile 的 mock provider 提供。WechatService.getPhoneNumber 的 testMode 直出分支同样收紧到 dev/local/test。tools/check-test-mode.ps1:13-18 的扫描路径为 cfc-backend/config/,且找不到 prod 配置文件时必须 exit 1(当前 exit 0 是假通过根源);并把该脚本接入 CI 闸门。startup.sh:17 / start.sh:11 的默认 profile 改为 prod(或启动即校验 profile 合法性)。④ 修改目的:让"测试开关"在生产结构性地不可能开启,堵住 C 端账号接管与支付绕过两条链路;同时让 CI 守卫从"假通过"变为"真闸门"。
⑤ 修改后能否做问题闭环:能。复测方式:① 启动期测试——以 SPRING_PROFILES_ACTIVE=prod + wechat.test-mode=true 启动,必须启动失败并打印明确错误;② 接口测试——/api/auth/phone-login 用任意手机号 + 123456 必须被拒;③ CI 闸门测试——在 config/ 下放一个含 test-mode: true 的 prod 文件,check-test-mode.ps1 必须 exit 1。通过标准:三条全绿,且 grep -rn "test-mode: true" cfc-backend/config cfc-backend/src/main/resources 仅出现在非 prod 文件。
① 问题描述:数据库口令、JWT 签名密钥、微信 appsecret、微信支付 mch-key / api-v3-key、虚拟支付 appkey/token、Dify API Key、回调密钥、opencode 管理口令、京东云 AK/SK 全部以明文写在已跟踪的 application.yml 中;微信支付商户私钥 apiclient_key.pem 与证书、.p12、以及含 LLM Key 的 cfc-langgraph/.env 均被 Git 跟踪;cfc-backend/config/application-prod.yml 曾于 commit 389615495 进入历史且含明文口令与 test-mode: true。值本身不在本报告复述。
② 测试 bug 点:
cfc-backend/src/main/resources/application.yml:24、:49、:68、:89、:91-92、:100-101、:103、:116-117、:124、:150、:179-180cfc-backend/src/main/resources/cert/apiclient_key.pem、apiclient_cert.pem、apiclient_cert.p12(git ls-files 全部命中;.gitignore 未覆盖 cert/)cfc-langgraph/.env(git ls-files 命中;含 LLM_API_KEY / EMBEDDING_API_KEY / LANGCHAIN_API_KEY)cfc-backend/config/application-prod.yml(git log --all --diff-filter=A → 389615495)③ 最优修改方案:
application.yml 中所有敏感值改为无默认值的占位(password: ${DB_PASSWORD},无 : 默认值 → 缺失即启动失败),密钥统一走环境变量 / 配置中心 / KMS。.gitignore 增加 *.pem、*.p12、*.jks、cert/、.env(保留 .env.example)。git filter-repo --path cfc-backend/src/main/resources/cert --path cfc-langgraph/.env --invert-paths,并强制推送 + 通知所有协作者重新 clone;历史清理后仍需完成第 1 步轮换。gitleaks detect --redact(或 trufflehog)接入 CI pre-commit 与 pre-push,命中即阻断。src/main/resources 与 config/ 下 yml,禁止出现 password: <非 ${...} 占位>、secret: <非 ${...}>。④ 修改目的:防止密钥泄露——让凭据只存在于受控的密钥管理系统与运行环境中,仓库历史不再持有任何可用凭据。
⑤ 修改后能否做问题闭环:能。复测方式:① gitleaks detect --no-git + gitleaks detect(含历史)均 0 命中;② git log --all --diff-filter=A -- cfc-backend/config/application-prod.yml 后历史已无该文件;③ 新增单测 NoPlaintextSecretTest 扫描全部 yml/env 断言无明文口令。通过标准:三条全绿 + 轮换记录留痕。
/api/stats/** 无鉴权 + /child/overview 无归属校验 → 匿名可读全平台经营数据、用户手机号与任意儿童私有数据① 问题描述:统计 Controller 的 8 个端点全部没有角色校验(既无 @RequestAttribute("role") 也无 @PreAuthorize),且路径不在 LOGIN_REQUIRED_PATHS,因此匿名可调。其中 /api/stats/traffic 的用户停留时长榜把用户名回落到手机号;/api/stats/child/overview 只凭一个 memberId 就返回该成员的积分、活跃天数、任务完成率、游戏最高分与近 7 日积分曲线,完全没有校验该成员是否属于调用者家庭。
② 测试 bug 点:
cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java:89-90、:122、:191、:225、:257、:420(无鉴权)StatsController.java:669-676(用户名回落手机号)StatsController.java:292-357(/child/overview,memberId 无归属校验;:299 familyMemberMapper.selectById(memberId)、:303-306 pointsLog、:315 taskService.getAllTasksByChild(memberId)、:322 gameRecordService.listByMemberId(memberId))cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java:85-101(/api/stats 不在 LOGIN_REQUIRED)③ 最优修改方案:
/api/stats/dashboard|overview|revenue|membership|trend|traffic 全部加 @PreAuthorize("hasAnyRole('admin','operator','finance')")(或复用 AdminRoles.hasAnyPlatformRole(roles)),并把 /api/stats/** 加入 LOGIN_REQUIRED_PATHS。/child/overview 必须补归属校验:引入 FamilyMemberService.isCallerInSameFamily(memberId, userId)(该方法已存在于 FamilyMemberService.java:1431-1444)或 FamilyMemberService.getMemberLogs 同款 user.getFamilyId() 比对,不通过返回 403;memberId 为空时直接 400。userDwellList 的 userName 禁止回落到手机号,统一用脱敏昵称或 userId。StatsController 的平台级查询(selectCount(null) 等)与 C 端家庭级查询(/family/summary、/points/ranking)拆成两个 Controller,避免权限模型混淆。④ 修改目的:漏洞可见 → 不可利用:把"平台经营数据"与"家庭私有数据"各自收敛到正确的权限边界;杜绝 PII(手机号)与未成年人健康数据外泄。
⑤ 修改后能否做问题闭环:能。复测方式:新增 StatsAuthzTest:① 无 Token 调 6 个平台级端点 → 全部 401;② parent Token 调 → 403;③ admin Token → 200;④ parent A 用家庭 B 的 memberId 调 /child/overview → 403;⑤ 断言响应 JSON 中不出现手机号形态字符串(正则 1[3-9]\d{9})。通过标准:五条全绿。
① 问题描述:令牌有效期 30 天;没有 jti、没有黑名单、没有刷新令牌与吊销接口;用户改密码/退出登录/被踢出家庭后,已签发令牌依然有效;签名密钥在配置中是可预测明文;JwtInterceptor 还接受 ?token= 查询参数(令牌会进入 nginx access log、浏览器历史与 Referer 头)。
② 测试 bug 点:
cfc-backend/src/main/java/com/etotem/cfc/config/JwtConfig.java:58-69(无 jti)、:79-91(无吊销查询)cfc-backend/src/main/resources/application.yml:67-73(30 天)cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java:127(query token)、:123-128cfc-backend/src/main/java/com/etotem/cfc/config/JwtConfig.java:27-44(短密钥 SHA-256 扩展,无最小强度校验)③ 最优修改方案:
jti(UUID),有效期降到 2 小时(30 天仅保留 refresh token)。jti 写入 Redis 黑名单(TTL = 剩余有效期);JwtInterceptor 校验时检查黑名单。secret 长度 < 32 字节时启动失败(不再 SHA-256 兜底);密钥来自环境变量/密钥管理,轮换有版本号(kid)支持双密钥并存平滑切换。request.getParameter("token") 兜底,仅接受 Authorization: Bearer;若小程序确有需要,走专用 /api/auth/refresh 端点用 refresh token 换。X-User-Id / X-User-Role(JwtInterceptor.java:168-169)对外回显用户身份,建议移除。④ 修改目的:让会话可回收、密钥强度可度量、令牌不落 URL,降低令牌泄露后的爆炸半径。
⑤ 修改后能否做问题闭环:能。复测方式:新增 JwtLifecycleTest:① 签发 → 登出 → 携带旧令牌访问 → 401;② 改密后旧令牌 → 401;③ 无效 alg=none / alg=RS256 混淆令牌 → 401(parseClaimsJws 已具备,补回归锁定);④ ?token= 形式请求 → 401;⑤ 短密钥启动 → 启动失败。通过标准:五条全绿。
check-test-mode.ps1 守卫因路径写错而恒为假通过① 问题描述:仓库不存在任何 CI 配置(.github/、.gitlab-ci.yml、Jenkinsfile、.circleci/、azure-pipelines.yml 全部缺失)。唯一的闸门是本地 githooks/pre-push,只跑 mvn compile(-DskipTests),需人工 git config core.hooksPath 才生效,且可用 XAF_CI_GATE=0 绕过。仓库内唯一的"安全脚本" tools/check-test-mode.ps1 扫描 cfc-backend/src/main/resources/ 下的 application-prod*.yml,但该目录根本没有 prod 配置(真实位置是 cfc-backend/config/),于是走 if ($prodYamlFiles.Count -eq 0) { "Skipping check"; exit 0 } —— 守卫永远返回成功。也就是说:安全检查在流程中"存在但无效"。
② 测试 bug 点:
.github/、无 .gitlab-ci.yml、无 Jenkinsfilegithooks/pre-push:1-20(仅 compile;XAF_CI_GATE=0 可跳过)cfc-backend/tools/check-test-mode.ps1:13-18(扫描路径错误 + 缺文件即 exit 0)——注意脚本实际路径为 d:/cfc/tools/check-test-mode.ps1:13-18cfc-backend/pom.xml:spring-boot-starter-parent 2.7.18(pom.xml:18)依赖 logback-classic(BOM 管理)③ 最优修改方案:
semgrep --config=p/java(或 p/secrets)→ 高危 阻断;gitleaks detect --redact → 命中 阻断;mvn org.owasp:dependency-check-maven:check + pip-audit -r cfc-langgraph/requirements.txt + npm audit --audit-level=high → 高危 阻断,中危告警。tools/check-test-mode.ps1 扫描路径改为 cfc-backend/config/,且"找不到 prod 配置"时 exit 1;同款思路覆盖 wechat.test-mode、admin.skip-captcha、jwt.secret 三项。githooks/pre-commit 加"密钥与 test-mode 快速扫描",让本地也能兜住。pom.xml 升级至受支持的 Spring Boot 3.x(或至少 2.7.18→最新 2.7.x 补丁线并接受 EOL 风险登记);pdfbox 升至 2.0.35+,aws-java-sdk-s3 迁 v2。npm ci 必须成功(禁止 npm install 漂移),pip install -r requirements_frozen.txt(该文件已存在)。④ 修改目的:把安全测试真正接入研发流程(SAST/SCA/密钥扫描进 CI 且有阻断策略),让漏洞在合并前可见;同时修掉"假通过"的假闸门。
⑤ 修改后能否做问题闭环:能。复测方式:① 造一个含 test-mode: true 的 prod 配置提交,CI 必须红;② 造一个含 password: abc123 的 yml,gitleaks 必须红;③ 注入一个已知 CVE 的依赖,dependency-check 必须红;④ 故意让 mvn compile 失败,CI 必须红(证明闸门真的在跑)。通过标准:四个负向用例全部被拦截。
v-html 渲染 → 存储型 XSS① 问题描述:后端无 Jsoup / OWASP Java HTML Sanitizer / AntiSamy,前端无 DOMPurify。文章 content 原样入库后,管理端与教师端用 v-html 渲染;AI 生成的报告解读(内容源自用户上传的 PDF)同样 v-html 渲染。攻击者可在文章内容或 PDF 中植入 <img src=x onerror=...>,当管理员/规划师打开审核页或报告页时脚本以该用户域内权限执行——可进一步调用后台 API 提权(叠加 P1 缺乏鉴权则可直接造成数据篡改)。CSP 含 'unsafe-inline' 'unsafe-eval',无二次缓解。
② 测试 bug 点:
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java:104、:197cfc-web/src/views/admin/review/ArticleAudit.vue:80 — v-html="currentRow.content"cfc-web/src/views/admin/ProxyReportUpload.vue:59、:121cfc-web/src/views/teacher/ProxyReport.vue:75、:158cfc-web/src/views/admin/HealthPlanManage.vue:95 — v-html="renderPlanContent(currentPlan.planContent)"cfc-backend/src/main/java/com/etotem/cfc/config/SecurityHeadersFilter.java:26-28pom.xml 无 jsoup/antisamy;cfc-web/package.json、cfc-frontend/package.json 无 dompurify③ 最优修改方案:
org.jsoup:jsoup,用 Jsoup.clean(html, Safelist.relaxed().removeTags("script","style","iframe","object","embed").addTags("img").addAttributes("img","src","alt","title")),在 AdminArticleController 保存/更新前统一过一道;同样处理 planContent 与 AI 报告字段。v-html 渲染前再过 DOMPurify(双保险),cfc-web 加 dompurify 依赖并在 renderPlanContent、报告渲染处接入。'unsafe-eval',script-src 移除 'unsafe-inline' 改用 nonce/hash;img-src 去掉 data: 或改用 blob:。aiAnalysis)视为不可信外部输入,落库前同样净化。<script / onerror= 不存在。④ 修改目的:消除存储型 XSS,防止恶意内容在管理员/规划师会话中执行并借权限完成横向移动。
⑤ 修改后能否做问题闭环:能。复测方式:新增 XssSanitizationTest(后端)与前端 Jest 用例——提交含 <script>alert(1)</script>、<img src=x onerror=alert(1)>、javascript: 链接的文章与报告,断言落库内容与渲染结果中不含可执行脚本;同时断言 CSP 头不含 unsafe-eval。通过标准:全部断言通过 + CSP 收紧项生效。
① 问题描述:健康报告 PDF 上传只校验"文件名以 .pdf 结尾 或 Content-Type 为 application/pdf",二者是 OR 关系,且不校验文件魔数,随后立即交由 PDFBox 解析——把"类型可伪造的文件"直接送进解析器。图片分支仅依赖客户端提供的 Content-Type。LocalDiskStorageService 把客户端文件名后缀直接拼进落盘路径,未剥离路径分隔符。同时 pdfbox 2.0.31 属陈旧版本且正处攻击面。
② 测试 bug 点:
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java:820-831(/report/upload,:829 filename.endsWith(".pdf") OR contentType)、:832-833(直接 pdfParseService.parse)HealthReportController.java:914-945(/report/upload-only,:939 isImageContent(contentType))cfc-backend/src/main/java/com/etotem/cfc/service/impl/LocalDiskStorageService.java:66-75(扩展名取自 getOriginalFilename())、:97-105cfc-backend/src/main/java/com/etotem/cfc/service/FileStorageService.java:36-47(仅按 contentType 白名单,未做魔数校验)cfc-backend/pom.xml:171(pdfbox 2.0.31)③ 最优修改方案:
%PDF-;图片用 ImageIO.read() 实际解码验证(解码失败即拒绝),不再信任 Content-Type。size <= 10MB;PDFBox 解析前限制页数(如 <= 50)与解压后体积,防解压炸弹;解析在独立进程/线程池 + 超时中执行(当前 pdfParseService.parse 在请求线程内同步执行)。LocalDiskStorageService 改用 Paths.get(originalFilename).getFileName().toString() 剥离目录成分,扩展名用白名单正则 ^[A-Za-z0-9]{1,8}$ 重写,落盘前 normalize() 后校验仍以 uploadPath 为前缀(防 .. 穿越)。pdfbox 至 2.0.35+;spring-boot-starter-parent 升级到受支持版本线(见 P7)。④ 修改目的:把上传从"信任客户端声明"改为"服务端实证",消除恶意文件解析(DoS / 解析器漏洞)面与路径穿越。
⑤ 修改后能否做问题闭环:能。复测方式:新增 UploadSecurityTest:① 上传名为 x.pdf 但内容是 PNG → 拒绝;② 上传内容是 %PDF- 但名为 x.png → 拒绝;③ 上传含 ../ 的 multipart filename → 落盘路径必须在 upload.base-dir 内(断言 resolved.startsWith(base));④ 超过大小上限 → 413/拒绝。通过标准:四个负向用例全绿。
① 问题描述:① 限流与登录锁定均直接采信客户端可控的 X-Forwarded-For / X-Real-IP,攻击者每次换一个假 IP 即获得全新限流桶,验证码防爆破与 admin 登录锁定形同虚设;② findRule 用 path.contains(key) 子串匹配且规则表为 ConcurrentHashMap,/api/auth/send-code 的"60 秒 1 次"规则可能被 /api/auth/ 的"1 秒 10 次"抢占,实际强度不确定;③ 验证码、微信 access_token、JWT 片段被明文写入日志;④ /api/admin-auth/account-roles 公开且按手机号返回角色列表,可批量枚举管理员账号;⑤ 登录密码兼容 MD5 哈希。
② 测试 bug 点:
cfc-backend/src/main/java/com/etotem/cfc/config/RateLimitInterceptor.java:174-186(getClientIp 采信 XFF)、:165-172(path.contains + 规则表顺序)、:52-60(规则定义)cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java:374-383(getClientIP 采信 XFF)、:337-355(账号枚举)、:410-425(MD5 兼容)cfc-backend/src/main/java/com/etotem/cfc/service/VerificationCodeService.java:67、:88、:112cfc-backend/src/main/java/com/etotem/cfc/service/WechatService.java:364、:384cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java:240-241③ 最优修改方案:
X-Forwarded-For(取链尾第一跳),否则用 request.getRemoteAddr();或改用 Spring ForwardedHeaderFilter + server.forward-headers-strategy=native。List<Rule> 按前缀长度倒序遍历,先匹配先生效),/api/auth/send-code 用 startsWith 精确前缀;同时把 /api/admin-auth/login 单独设为"每 IP+每手机号 10 分钟 5 次"。WechatService 打响应 JSON 前先剔除 access_token 字段。/api/admin-auth/account-roles 移除或加"同 IP 每日查询上限 + 失败不区分手机号是否存在"(统一返回空列表)。SecurityConfig.passwordEncoder() 已就绪),并安排全量重置。④ 修改目的:让防爆破与限流真正可被依赖;防止日志成为凭据二次泄露源;阻断管理员账号枚举。
⑤ 修改后能否做问题闭环:能。复测方式:新增 RateLimitTrustBoundaryTest:① 携带伪造 X-Forwarded-For 连续请求 → 仍被同一真实 IP 桶限流;② /api/auth/send-code 连发 2 次 → 第二次 429(60s 规则生效);③ 断言日志文件中不出现 code=123456 / access_token / JWT 前 30 字符。通过标准:三条全绿。
npm audit / mvn org.owasp:dependency-check-maven:check / pip-audit / trivy fs / osv-scanner 等需联网更新漏洞库的命令 → 本报告 E 组所有"陈旧/高危"判断仅基于版本号与已知历史,未与当前漏洞库比对,不能等同于"这些版本一定存在可利用 CVE"。semgrep / spotbugs / sonar 等(离线档未安装且未获授权);结论来自人工模式检索。WebConfig.java:44-68 配置,未发实际跨域请求验证 Access-Control-Allow-Private-Network 是否真被浏览器接受。wechat.test-mode 生产实际取值:仓库内 application.yml:98 与 config/application-prod.yml:37 均为 true,但生产容器的环境变量覆盖与实际运行 profile 未实测(SPRING_PROFILES_ACTIVE 在部署侧的实际取值需运维确认)。192.168.16.251 / bianwoyou.mysql.rds.aliyuncs.com 的实际可达性与公网暴露程度未做网络层探测。cfc-web / cfc-frontend 产物做 bundle 级审计(source map 是否泄露、是否内置敏感配置)。cfc-langgraph 的边界防护:cfc-langgraph/app/main.py 的 CORS、中间件与 API 鉴权仅做了粗查(未发现 CORSMiddleware/Depends 鉴权),未逐端点复核,也未验证其是否直接暴露公网(application.yml:141 base-url 生产指向 ai.etotem.com.cn)。DatabaseInitializer.runMigrations() 在启动期执行 DDL,其对生产库的影响属安全与稳定性交叉项,本次未实测。