chore(security): 用户中心 / chat / 密码 / compose 多处姿态收紧#26
Merged
longsizhuo merged 2 commits intomainfrom May 8, 2026
Merged
Conversation
按 SECURITY.md 登记五条不变量并加 SecurityInvariantsTests 回归网:
- INV-001 UserCenterService.updateAuthorization 加 RESTRICTED_ROLES 黑名单,
阻断走 PUT /users/{id}/authorization 整集替换 roles 绕过 AdminUserController
superadmin 保护边界的路径
- INV-002 ChatHistoryController 加 chat 归属校验:已绑定 owner 的 chat 不再
允许他人写入;同时新增 AccessDeniedBusinessException 让业务归属类 403 与
Sa-Token 缺权限/缺角色场景的 message 解耦
- INV-003 PasswordService 切到 BCryptPasswordEncoder(cost=10),保留 dual-mode
让历史 SHA-256 用户能登录,AuthService 在登录成功后做 lazy upgrade 就地
迁移;schema.sql / init.sql / test-schema.sql 三处 seed 同步重写为 bcrypt
- INV-004 user_follows 表与 idx_user_follows_followee 索引加进 schema.sql /
init.sql / test-schema.sql,FollowService 引用的 relation 之前在新部署上
全部 500
- INV-005 docker-compose.yml 把 :-change_me 弱密码默认值改成 :? 强制 .env 提供,
避免漏配 env 时仍以弱密码起服务
顺手修:discord-bridge seed 自 PR #18 起就在 test-schema.sql 里,但
JdbcUserAccountRepositoryTests / UserCenterControllerIntegrationTests 的
hasSize(3) 没跟着更新,main 上一直红——这次一并改成 4。
测试:./mvnw test 全套 212 / 212;新增 SecurityInvariantsTests 12 条覆盖
五条不变量的"攻击成功就 fail"探测器。
4 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
该 PR 以“安全不变量(INV-001~005)”为主线,对用户中心授权、Chat 写入归属、口令哈希、DB schema 漂移、以及 docker-compose 默认弱配置进行了收紧,并引入回归测试与 SECURITY.md 作为长期防回退基线。
Changes:
- 将密码哈希切换为 bcrypt(含 legacy SHA-256 兼容 + 登录成功后 lazy upgrade),并同步所有 seed/初始化脚本与测试覆盖。
- 对 ChatHistory 写入增加 owner 归属校验路径,并新增“业务归属型 403”异常类型与全局处理器。
- 补齐
user_follows表与索引 DDL,并通过SecurityInvariantsTests+SECURITY.md固化不变量与 docker-compose 配置约束。
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/resources/test-schema.sql | 测试库 seed 改为 bcrypt,并补齐 user_follows 表以对齐生产 schema。 |
| src/test/java/com/involutionhell/backend/usercenter/service/PasswordServiceTests.java | 更新 PasswordService 单测以覆盖 bcrypt 输出、salt 随机性与 legacy SHA-256 兼容。 |
| src/test/java/com/involutionhell/backend/usercenter/service/AuthServiceTests.java | 适配 AuthService 依赖注入变更(新增仓库依赖的 mock)。 |
| src/test/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepositoryTests.java | 修复 seed 用户数量断言(discord-bridge)。 |
| src/test/java/com/involutionhell/backend/usercenter/controller/UserCenterControllerIntegrationTests.java | 修复 /users 列表 seed 数量断言(discord-bridge)。 |
| src/test/java/com/involutionhell/backend/security/SecurityInvariantsTests.java | 新增安全不变量回归测试(INV-001~005)。 |
| src/main/resources/schema.sql | 生产 schema seed 改为 bcrypt,并新增 user_follows 表与索引。 |
| src/main/java/com/involutionhell/backend/usercenter/service/UserCenterService.java | 在授权更新接口中拦截受限角色(superadmin)。 |
| src/main/java/com/involutionhell/backend/usercenter/service/PasswordService.java | PasswordService 改为 bcrypt(dual-mode matches + legacy 判定)。 |
| src/main/java/com/involutionhell/backend/usercenter/service/AuthService.java | 登录成功后对 legacy SHA-256 口令进行 lazy upgrade 到 bcrypt。 |
| src/main/java/com/involutionhell/backend/usercenter/repository/UserAccountRepository.java | 新增 updatePasswordHash 用于 lazy upgrade 写回。 |
| src/main/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepository.java | 实现 updatePasswordHash。 |
| src/main/java/com/involutionhell/backend/common/error/GlobalExceptionHandler.java | 新增业务归属型 403 异常处理。 |
| src/main/java/com/involutionhell/backend/common/error/AccessDeniedBusinessException.java | 新增业务归属访问拒绝异常类型。 |
| src/main/java/com/involutionhell/backend/chat/repository/JdbcChatHistoryRepository.java | 新增 lookupOwner 查询 chat 归属信息。 |
| src/main/java/com/involutionhell/backend/chat/repository/ChatOwner.java | 新增 ChatOwner 载体区分“不存在/匿名/已绑定 owner”。 |
| src/main/java/com/involutionhell/backend/chat/repository/ChatHistoryRepository.java | 增加 lookupOwner 接口用于写入前归属校验。 |
| src/main/java/com/involutionhell/backend/chat/controller/ChatHistoryController.java | 写入前增加 owner 校验并在越权时返回业务归属型 403。 |
| SECURITY.md | 新增不变量文档(INV-001~005)并与测试建立映射。 |
| pom.xml | 引入 spring-security-crypto 以使用 BCryptPasswordEncoder。 |
| docker/init-db/init.sql | 初始化脚本 seed 改为 bcrypt,并补齐 user_follows DDL。 |
| docker-compose.yml | compose 收紧:去除弱密码 fallback,要求显式提供 env,并强调 PG 端口 loopback 绑定。 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - `docker/init-db/init.sql`(同步 DDL) | ||
| - `src/test/resources/test-schema.sql`(测试同步) | ||
| - `usercenter/follows/FollowService`(SQL 调用方) | ||
| - **测试**:`SecurityInvariantsTests#user_follows表存在且follow_unfollow能闭环` |
Comment on lines
+46
to
+50
| -- 关注关系(user_follows)—— 与 schema.sql 保持一致;H2 PostgreSQL MODE 接受 | ||
| CREATE TABLE IF NOT EXISTS user_follows ( | ||
| follower_id BIGINT NOT NULL, | ||
| followee_id BIGINT NOT NULL, | ||
| created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
Comment on lines
+304
to
+308
| // 简化版扫描:postgres ports 段后面紧跟的 mapping 行必须 127.0.0.1: 开头 | ||
| Pattern unsafePattern = Pattern.compile( | ||
| "(?m)^\\s*-\\s*\"(0\\.0\\.0\\.0:|\\d+:)5432:5432\""); | ||
| Assertions.assertThat(unsafePattern.matcher(content).find()) | ||
| .as("docker-compose.yml 检测到 postgres 端口暴露公网形式(如 \"5432:5432\" 或 \"0.0.0.0:5432:5432\")") |
Comment on lines
+49
to
+53
| // INV-002:归属校验。已绑定 owner 的 chat 必须由 owner 本人写。 | ||
| // 这一步必须在 saveTurn 之前——saveTurn 内部用 ON CONFLICT upsert, | ||
| // 一旦执行就会插入 Message 行,事后回滚得靠 @Transactional,宁可前置拦截。 | ||
| Optional<ChatOwner> existing = chatHistoryRepository.lookupOwner(req.chatId()); | ||
| if (existing.isPresent() && !existing.get().isAnonymous()) { |
| log.info("已就地升级用户 {} 的密码哈希(legacy → bcrypt)", userAccount.username()); | ||
| } catch (Exception e) { | ||
| // lazy upgrade 失败不阻断登录——记日志即可,下次登录会再次尝试 | ||
| log.warn("用户 {} 密码哈希升级失败:{}", userAccount.username(), e.getMessage()); |
Comment on lines
+55
to
+59
| // INV-003 lazy upgrade:把老 SHA-256 hash 升级为 bcrypt(用同一明文重新 hash) | ||
| if (passwordService.isLegacyHash(userAccount.passwordHash())) { | ||
| try { | ||
| userAccountRepository.updatePasswordHash( | ||
| userAccount.id(), |
PR #26 Copilot review 7 条评论里 6 条本 commit 处理,第 7 条(chat saveTurn 的 TOCTOU window)单独跟踪 follow-up,本 PR scope 不修。 修复点: 1. SECURITY.md INV-004 引用的测试方法名跟实际代码对不上——文档本来写 `user_follows表存在且follow_unfollow能闭环`,实际是 `user_follows表存 在且字段可读写`,按 Copilot 反馈对齐文档。 2. test-schema.sql 之前只建了 user_follows 表但没建 idx_user_follows_followee 索引,跟生产 schema.sql 漂移;补上 CREATE INDEX 让"索引被误删"也能被 CI 抓到(即便表本身仍存在)。 3. INV-005a compose 端口扫描原来只盯 "5432:5432" / "0.0.0.0:5432:5432" 两种带引号写法,漏过无引号 5432:5432、IPv6 [::]:5432、host port 改成 15432 但容器仍 5432 等其它形式。改成扫描所有 host:5432 mapping,凡是 host part 不以 127.0.0.1: 起头的全算违规。 4. AuthService lazy upgrade 失败的 log.warn 之前只传 e.getMessage(),丢 stack trace;改成把异常对象作为最后一个参数,保留完整堆栈用于排查 死锁/连接池/权限根因,"不阻断登录"行为不变。 5. UserCenterService.updateAuthorization 之前只检查 RESTRICTED_ROLES, 没拒 null/blank 元素——roles 里塞 null 会让 repository 的 String.join 抛 NPE 走 500 兜底。增加 null/blank 校验返回 400,permissions 同样补齐。 6. 新增 3 条 AuthServiceTests 覆盖 INV-003 lazy upgrade 路径: - legacy hash 匹配后触发 updatePasswordHash 写回 - updatePasswordHash 抛异常时不阻断登录 - bcrypt hash 路径不触发 lazy upgrade 测试:./mvnw test 全套 215 / 215(之前 212 + 新增 3 条)。 未在本 PR 修: - ChatHistoryController#save 的 lookupOwner + saveTurn 两步 TOCTOU window: 利用窗口需要 victim/attacker 毫秒级并发写同一 UUID chatId,最坏后果 一条 message 错挂到 victim chat。完整修法是把逻辑下沉到 service 层 @transactional + 改 saveTurn SQL 加 owner WHERE,工作量超出本 hotfix scope,单独 follow-up 跟踪。
Member
Author
Copilot CR 反馈处理小结7 条评论里 6 条已在 commit fb5033b 处理( TOCTOU 一条本 PR 不修:
确认是真问题,但评估后单独 follow-up 跟踪而不是本 PR 修。理由:
会单独建 issue 跟踪根治方案。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
2026-05-07 三方代码 review(backend / frontend / ChatBot)发现串成一条的 P0 攻击链与若干独立 P0。本 PR 处理后端侧 5 项,前端侧 1 项见 frontend 仓库同名 PR。
完整不变量清单见新增的
SECURITY.md。所有不变量都在SecurityInvariantsTests里有"攻击成功 → CI 红"的回归测试。改动按不变量分组
INV-001 · superadmin 角色不能通过 API 授予
UserCenterService#updateAuthorization加RESTRICTED_ROLES黑名单。AdminUserController#setAdminRole之前已经手工拒绝 superadmin,但PUT /users/{id}/authorization整集替换 roles 完全绕过了那条边界 —— admin 自带user:center:manage权限,可以一行 curl 给自己挂上superadmin。INV-002 · 不允许往他人的 chat 历史写消息
ChatHistoryController#save在saveTurn之前先lookupOwner:已绑定 ownerId 的 chat 必须由 owner 本人写。匿名 chat 仍允许匿名继续写,保留"匿名 → 登录迁移"语义。新增
AccessDeniedBusinessException+GlobalExceptionHandlerhandler,让"业务归属"类 403 不和 Sa-Token "缺权限/缺角色"场景共用 message 模板。INV-003 · 密码必须 bcrypt
PasswordService切到BCryptPasswordEncoder(cost=10)。matches双路径:bcrypt 原生 + legacy SHA-256 dual-mode 兼容AuthService#login成功后判断isLegacyHash,触发 lazy upgrade 就地迁移(失败不阻断登录)schema.sql/docker/init-db/init.sql/test-schema.sql三处 seed 同步重写为 bcrypt 形式(明文与之前一致:Admin@123456/Alice@123456/Audit@123456)新增
spring-security-crypto依赖(单独 artifact,不引入完整 Spring Security 的 Filter chain)。INV-004 · user_follows 表必须随 schema 一并建立
FollowService用的user_follows(follower_id, followee_id, created_at)之前在schema.sql与init.sql都没建。新部署 / 新 Neon 库一访问/api/user-center/follows/...必 500。本 PR 加表 +idx_user_follows_followee索引。INV-005 · compose 不暴露公网 PG 端口、不留弱密码默认值
postgres 端口部分历史已经收回 loopback,本 PR 把
${POSTGRES_PASSWORD:-change_me}/${PGPASSWORD:-change_me}/${PGADMIN_PASSWORD:-change_me}三处弱密码 fallback 改成:?强制.env显式提供,并加测试防回退。顺手修
PR #18 加
discord-bridgeseed 后JdbcUserAccountRepositoryTests/UserCenterControllerIntegrationTests的hasSize(3)没跟着改,main 上一直红。本 PR 一并改成 4。测试
新增的
SecurityInvariantsTests12 条全是"测试代码本身就是攻击脚本,断言被拒"的形式,未来谁删了校验、改坏了 seed、回退 compose 端口,CI 立即红。Test plan
./mvnw test全套 212 通过SecurityInvariantsTests单独跑通过(12/12)set -a && . ./.env && set +a && ./mvnw spring-boot:run起后端,按 PR 描述用 curl 复现攻击路径,验证现在被拒SECURITY.md五条不变量的描述是否准确,能否作为长期防回归参考