40 KiB
S01 定向整改报告(F1 / F2 / F3)
- 整改日期:2026-09-11
- 分支:
fix/resume-upload-safety - 范围:仅修复独立验收确认的 F1、F2、F3,并完成对应自动化与干净构建
- 本报告不包含:数据库口令、.env 内容、JWT、验证码值、完整手机号、明文测试口令、非法 principal 敏感标记原文
- 整改期间状态:
S01独立验收未通过,定向整改中。
1. F1 的修复方式
docs/smoke-test.sh 的 sms_code() 彻底删除了对应用日志的依赖(grep MOCK SMS CODE),改为双重保护 + 容器内部只读 SQL 查询。
同时删除:SERVER_LOG 变量、resolve_log() 函数、全部日志 grep。未恢复任何验证码日志。
| 项 | 整改前 | 整改后 |
|---|---|---|
| 取码来源 | grep -a "MOCK SMS CODE.*phone=$PHONE" <日志> |
容器内 SELECT code FROM sms_verification_codes … |
| 前置授权 | 无 | SMS_MOCK_ENABLED=true 且 ALLOW_TEST_OTP_DB_READ=true |
| 取码返回值 | stdout(进入 command substitution) | REPLY 变量约定(不打印) |
| 请求体传参 | -d "<含验证码>"(进入进程参数) |
--data-binary @-(经 stdin) |
| 凭据处理 | 需 SERVER_LOG |
容器自身 MYSQL_ROOT_PASSWORD / MYSQL_DATABASE(不在本机命令行拼写口令) |
| 清理 | 无 | unset REPLY/SMS/LOGIN_BODY + trap cleanup_secrets EXIT |
脚本仍为 Bash,兼容当前 Git Bash;bash -n 语法校验通过。
2. smoke-test.sh 的新取码机制
sms_code()
├─ require_mock_sms_mode() # SMS_MOCK_ENABLED 必须为 true
├─ require_test_otp_db_read_permission() # ALLOW_TEST_OTP_DB_READ 必须为 true
├─ 校验 PHONE 为 11 位纯数字(防注入)
├─ 记录并关闭 xtrace(set +x),执行后精确恢复原状
├─ IFS= read -r code < <(otp_db_query ...) # 不使用 command substitution 取值
├─ 校验 code 为 6 位数字(当前 code-length / CODE_PATTERN)
├─ 失败 → unset code + 固定提示 + return 1(绝不携带空值继续登录)
└─ 成功 → REPLY="$code"; unset code; return 0
otp_db_query() 关键点:
- 容器名不硬编码:
compose_mysql_container()用awk解析项目实际backend-infra/compose.yaml中mysql服务的container_name;用BASH_SOURCE定位自身目录(被source时同样正确) - SQL 经 stdin(heredoc 风格
printf | docker exec -i),不出现在任何进程参数中 - 查询条件完整:
phone+purpose+used = 0+expired_at > NOW()(更正:本行原写作expires_at,该列在sms_verification_codes中不存在,是 F6 记录的错误字段名;已于第四轮一并更正,详见文末「第四轮定向整改(F6)」) ORDER BY created_at DESC, id DESC LIMIT 1,只取code列- 不使用
curl -v;失败诊断不打印请求体
3. 为什么不会把验证码输出到终端 / 日志 / 文件
- 取码函数从不
echo/printf验证码,只写REPLY;调用方SMS="$REPLY"后立即unset REPLY set +x包住取值与查询,且按调用前状态精确恢复,不无条件开启 xtrace- 登录请求体经
--data-binary @-从 stdin 送入 curl,验证码不进入 argv - 请求体只在
LOGIN_BODY局部变量中存在,用完unset;不落盘 trap cleanup_secrets EXIT兜底清理OTP/REPLY/LOGIN_BODY- 脚本控制台只输出
PHONE=$(mask_phone "$PHONE")(脱敏为199****9999),不打印完整手机号 - 应用侧本就不输出验证码(S01 已整改),Mock 发送器仅输出固定文本
4. ALLOW_TEST_OTP_DB_READ 的强制条件
is_true() { v=$(printf '%s' "${1:-}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]'); [ "$v" = "true" ]; }
require_mock_sms_mode() { is_true "${SMS_MOCK_ENABLED:-}"; }
require_test_otp_db_read_permission() { is_true "${ALLOW_TEST_OTP_DB_READ:-}"; }
- 两者同时为
true(大小写/空白不敏感)才允许查询 - 任一不满足:立即返回非 0,输出固定提示
test OTP retrieval is disabled,不执行任何 docker/SQL 调用 - 提示中不含手机号、验证码、数据库口令、SQL 结果或容器名
5. 未授权取码的实际结果
| 场景 | 实际结果 |
|---|---|
SMS_MOCK_ENABLED 缺失 |
拒绝;输出 test OTP retrieval is disabled;docker 未被调用 |
SMS_MOCK_ENABLED=false |
拒绝;同上 |
ALLOW_TEST_OTP_DB_READ 缺失 |
拒绝;同上 |
ALLOW_TEST_OTP_DB_READ=false |
拒绝;同上 |
| 两者为 true 但无可用验证码 | test OTP retrieval failed,不继续登录 |
| 两者为 true 但查询失败/结果非 6 位数字 | 同上,不继续登录 |
6. F2 修改的全部文档与行
| 文件 | 行(整改后) | 修改前 | 修改后要点 |
|---|---|---|---|
docs/acceptance-week1.md |
94 | # Mock 登录(从日志获取验证码) |
指向受控取码方式 |
docs/acceptance-week1.md |
148 | 注: 验证码从应用日志中获取 | 明确日志不输出;列出双开关;生产禁止查表 |
docs/acceptance-week1.md |
152 | 验证码打印到日志 | 明确不输出;标注明文存储遗留 |
docs/api-v0.1.md |
27 | Mock环境验证码打印到日志 | 明确不输出 + 受控取得 |
docs/api-v0.1.md |
174 | 验证码打印到日志 | 明确不输出;未接入供应商返回服务不可用 |
docs/frontend-integration.md |
37-49(§3 整节重写) | 「Mock 验证码日志位置」+ 旧示例行 MOCK SMS CODE: … code=****** + 「看终端输出/看日志文件/取最新一条」 |
改为「本地获取测试验证码(不通过日志)」:双开关 + 受控脚本 + 生产禁止查表 + 明文遗留风险 |
docs/frontend-integration.md |
149 | 验证码打印在服务日志 | 改为不再输出 + 受控取得 |
docs/offer-api.md |
262 | 验证码见应用日志 | 改为受控取得 + 生产禁止查表 |
docs/resume-api.md |
189 | 登录学生账号(Mock 验证码,见日志) | 改为受控取得 + 生产禁止查表 |
docs/smoke-test.sh |
全文头部与取码段 | 依赖日志取码 | 见第 1-5 节 |
文档统一口径(已落实):
- 删除所有「从控制台/日志查看验证码」的操作指导
- 明确应用日志不会输出验证码
- dev/test 本地自动化使用受控测试夹具/数据库内部只读查询
- 冒烟脚本仅在
mock=true且ALLOW_TEST_OTP_DB_READ=true时查表 - 真实生产环境禁止查询验证码表辅助登录
mock=false且无真实供应商时返回「服务不可用」- 明确当前尚未接入真实短信供应商
- 未声称验证码已加密/哈希存储
- 明确验证码当前仍为数据库明文遗留风险
- 示例中不含真实手机号、验证码、JWT 或数据库口令
在途文件保护:docs/frontend-integration.md 与 docs/resume-api.md 仅改动取码相关段落;git diff 复核确认简历上传加固内容(文件名净化、PDF/DOC/DOCX 结构校验、上传替换一致性、恶意软件扫描边界等)逐字保留,未覆盖、未还原、未重新格式化。
7. 全仓遗留搜索结果
grep -rn -i "MOCK SMS CODE|控制台.*验证码|验证码.*打印到日志|从日志获取|查看.*验证码"
docs README.md mydreamjob-server/src
- 「打印到日志 / 从日志获取」命中数:0
MOCK SMS CODE在mydreamjob-server/src与docs/中的命中数:0- 剩余匹配全部为禁止性表述或无关标识符(如「日志不得包含验证码」「不得写入日志」「SMS_CODE_INVALID」枚举名)
README.md:167本身即「不写入敏感信息」的正确约定,未修改
8. F3 使用的 Bean 装配方案
采用推荐的单 @Configuration + 单 @Bean 方案(不再依赖两套互斥 @ConditionalOnProperty):
@Configuration
public class SmsSenderConfiguration {
private static final String MOCK_ENABLED_VALUE = "true";
@Bean
public SmsCodeSender smsCodeSender(
@Value("${app.sms.mock.enabled:false}") String rawEnabled) {
if (rawEnabled != null && MOCK_ENABLED_VALUE.equalsIgnoreCase(rawEnabled.trim())) {
return new MockSmsCodeSender();
}
return new UnavailableSmsCodeSender();
}
}
MockSmsCodeSender/UnavailableSmsCodeSender已移除@Component与@ConditionalOnProperty,成为普通类- 满足:始终恰好一个 Bean;仅显式
true启用 Mock;非法值不启用 Mock;不根据 profile 偷偷启用;application-dev.yml仍显式true;application.yml默认false - 不把选择逻辑放回
SmsVerificationService;不新增第二套发送流程;不用 try/catch Bean 缺失兜底;不在每次发送时读配置;不输出配置原值到日志
9. 各配置取值对应的实现
app.sms.mock.enabled |
装配实现 |
|---|---|
true / TRUE / True / " true " |
MockSmsCodeSender |
false |
UnavailableSmsCodeSender |
| 缺失 | UnavailableSmsCodeSender |
空字符串(=) |
UnavailableSmsCodeSender |
仅空白(= ) |
UnavailableSmsCodeSender |
非法值(yes/1/on/enabled/null/mock/TRUE1) |
UnavailableSmsCodeSender(安全回退,绝不启用 Mock) |
10. 每种配置的 Bean 数量
由 SmsSenderConfigurationTest.everySupportedConfigurationCreatesExactlyOneSenderBean 断言(真实 ApplicationContextRunner,含 9 种取值 + 缺失属性):
每种取值 getBeanNamesForType(SmsCodeSender.class).length == 1。
11. 空值启动结果
| 层级 | 结果 |
|---|---|
快速装配测试(ApplicationContextRunner) |
上下文启动成功;恰好一个 Bean;类型 UnavailableSmsCodeSender;消费者构造注入成功;发送抛固定安全 SERVICE_UNAVAILABLE |
全应用上下文测试(SmsEmptyPropertyStartupIntegrationTest,真实 H2 + Flyway + 全 Bean) |
应用上下文启动成功;恰好一个 SmsCodeSender;类型 UnavailableSmsCodeSender;SmsVerificationService 可注入;发送返回固定安全服务不可用 ✅ |
整改前该取值导致 APPLICATION FAILED TO START … required a bean of type 'com.mydreamjob.auth.sms.SmsCodeSender' that could not be found(独立验收阶段已复现并记录在验收报告 F3)。
本轮按要求未操作 Docker / 未执行真实 MySQL 黑盒,故未做真实 JAR 启动;全应用上下文启动已由 H2+Flyway 覆盖,真实环境启动仍待独立复验。
12. 是否恢复验证码日志
未恢复。 grep -rn "MOCK SMS CODE" mydreamjob-server/src 命中 0;MockSmsCodeSender 仅输出固定文本 SMS verification code generated (mock sender);SmsVerificationService 仅两处 log.warn,参数为 maskPhone(phone);AuthService 仅记录 id。
13. 原 S01 权限与事务回归结果
| 原 S01 要求 | 结果 |
|---|---|
| 不恢复验证码日志 | ✅(第 12 节) |
| 不输出完整手机号 | ✅ SmsVerificationSecurityTest 10/10 |
| Mock 发送器不打印验证码 | ✅ |
| mock=false 不返回虚假成功 | ✅ SmsSenderConfigurationTest + 回滚集成测试 |
| mock=false 发送失败事务回滚 | ✅ SmsUnavailableSenderRollbackIntegrationTest 3/3(真实 Spring + H2 + Flyway:失败后该手机号无任何验证码行、总数不变、无虚假成功审计) |
| 匿名 POST /jobs = 401 | ✅ AuthenticationBoundaryIntegrationTest 15/15 |
| STUDENT/ADMIN 创建岗位 = 403/2002 | ✅ |
| COMPANY 合法创建保持成功 | ✅ |
| 非法 principal 返回 401 | ✅ CurrentUserAccessorTest 11/11 |
| 不返回 NumberFormatException / 内部 principal | ✅ |
| OpenAPI 173/211 | ✅ |
| 无新增公网取码接口 | ✅(OpenAPI 173/211 不变) |
| 无新 Flyway/表/索引/ErrorCode | ✅ |
14. Shell 测试数量与结果
docs/tests/smoke-test-sms-code-test.sh(新增):22 用例,PASS=22 / FAIL=0
覆盖:mock=true+ALLOW=true 可取码、mock=false 拒绝、配置缺失拒绝、ALLOW 缺失/为 false 拒绝、无可用验证码安全失败、查询失败不继续、结果非数字/长度非法拒绝、查询为空时不继续登录(无 login 请求)、stdout/stderr 不含验证码、不含完整手机号、生成文件不含验证码、不 grep 应用日志、不存在 MOCK SMS CODE、REPLY/SMS/LOGIN_BODY 清理与 EXIT trap 存在、未授权时不触碰数据库、拒绝提示不含手机号、授权模式下可完成登录流程(登录请求已发出且携带 6 位码、登录步骤判定 PASS)。
测试通过 PATH 注入假 docker/curl 实现,不要求 Docker 或真实 MySQL,仓库内不写入真实验证码,结束时扫描输出确认标记未泄露。
15. Maven 专项、关联与全量测试数
| 命令 | Tests run |
|---|---|
-Dtest="SmsSenderConfigurationTest,SmsVerificationSecurityTest,SmsVerificationServiceTest,CurrentUserAccessorTest,AuthenticationBoundaryIntegrationTest" |
56 |
-Dtest="*Auth*Test,*Security*Test,*Sms*Test,*Job*Test" |
222 |
mvn test(最终,含本轮新增 2 个集成测试) |
1671 |
mvn clean verify |
1671 |
S01 相关类明细:
| 测试类 | Tests |
|---|---|
SmsSenderConfigurationTest(F3,由 4 扩展至 12) |
12 |
SmsVerificationSecurityTest |
10 |
SmsVerificationServiceTest |
8 |
CurrentUserAccessorTest |
11 |
AuthenticationBoundaryIntegrationTest |
15 |
SmsUnavailableSenderRollbackIntegrationTest(本轮新增) |
3 |
SmsEmptyPropertyStartupIntegrationTest(本轮新增) |
2 |
16. Failures / Errors / Skipped
Failures=0、Errors=0、Skipped=0(全量与 clean verify 一致)
17. clean verify 结果
mvn clean verify → BUILD SUCCESS(4:34),产出 target/mydreamjob-server-0.0.1-SNAPSHOT.jar。
18. OpenAPI paths / operations
| 来源 | paths | operations |
|---|---|---|
静态 docs/verify_openapi.py |
173 | 211 |
| 校验输出 | All checks passed! |
— |
未新增/删除路径或 operation;无新增测试取码端点。
19. 精确修改文件
修改(已跟踪,本轮 F1/F2)
| 文件 | 变更 |
|---|---|
docs/smoke-test.sh |
+176/−…(取码机制整体替换、双保护、脱敏输出、库模式钩子) |
docs/acceptance-week1.md |
3 处取码表述 |
docs/api-v0.1.md |
2 处取码表述 |
docs/frontend-integration.md |
§3 整节重写 + 1 处(在途文件,最小改动) |
docs/offer-api.md |
1 处取码表述 |
docs/resume-api.md |
1 处取码表述(在途文件,最小改动) |
修改(S01 新建文件,本轮扩展,F3)
| 文件 | 变更 |
|---|---|
auth/sms/MockSmsCodeSender.java |
移除 @Component/@ConditionalOnProperty,改为普通类 |
auth/sms/UnavailableSmsCodeSender.java |
同上 |
auth/SmsSenderConfigurationTest.java |
由 4 用例扩展至 12(真实 ApplicationContextRunner + 消费者探针) |
新增
| 文件 | 用途 |
|---|---|
auth/sms/SmsSenderConfiguration.java |
F3 单 Bean 装配 |
auth/SmsUnavailableSenderRollbackIntegrationTest.java |
真实 Spring+H2:失败不残留验证码 |
auth/SmsEmptyPropertyStartupIntegrationTest.java |
空值全应用上下文启动 |
docs/tests/smoke-test-sms-code-test.sh |
F1 shell 安全测试(22 用例) |
20. Git 状态
- 分支:
fix/resume-upload-safety(未变) - 暂存区:空
git diff --check:无空白错误(仅 CRLF 提示)- 迁移改动:无(V1–V41 共 41 个文件,最大版本 41)
- 删除文件:0
- 历史未跟踪文件:未删除、未移动、未覆盖(含
docs/acceptance/s01-20260911/全部验收证据 43 个文件与验收报告,保持原样)
21. 是否修改 Flyway、表、ErrorCode 或公网接口
均未修改。
- Flyway:无新增/修改,V1–V41 原样
- 数据库表/索引:未新增或修改(验证码仍明文存储于
sms_verification_codes,属已记录遗留风险) ErrorCode:未被改动- 公网接口:OpenAPI 保持 173 paths / 211 operations;未新增取码接口
22. 明确未执行 git add -A / commit / push
未执行 git add -A、未 commit、未 push(亦未 reset/restore/checkout/clean)。
23. 明确未操作 Docker、未执行真实 MySQL 黑盒
未执行任何 Docker 命令、未执行真实 MySQL 黑盒。本轮全部验证均为:shell 测试(假 docker/curl 注入)+ Maven 单元/集成测试(H2 + Flyway + 真实 Spring 上下文)。
24. 真实 MySQL / Docker 复验仍待独立验收
F1 的数据库取码路径(docker exec + 容器内 SQL)尚未在真实 MySQL 环境端到端执行;F3 的运行期 JAR 启动(含空值 / 缺失 / 非法值)亦未在真实环境启动验证。二者必须由独立复验在真实 MySQL/Docker 环境完成,不得以本轮 H2 结果代替。
遗留与后续
- 验证码仍为数据库明文存储(本轮未改表结构,属已知遗留)
- 真实短信供应商未接入;生产
mock=false下发码一律安全失败,需产品确认上线策略 - 存量 43 处直接身份解析未收口(均不可匿名到达;
DeletionGuardInterceptor对 20 位数字溢出仍可能 500)——非本轮范围 - 安全层 401 响应不含 requestId(既有
AuthenticationEntryPoint固定载荷),与业务层错误响应格式不一致 - 上一轮验收的其余观察项(失败后残留
sms:rate限流键 60s;Spring Boot 默认generated security password入日志)本轮未处理,仍属非阻断遗留
第三轮定向整改(F4 / F5)
- 整改日期:2026-09-11(第二轮之后)
- 独立复验结论:S01 仍 FAIL —— F2/F3 已通过,但新发现 F4(xtrace 泄露)与 F5(数据库密码进入 mysql argv)两个阻断项
- 本轮范围:仅修 F4/F5,并相应扩展 shell 测试、修正文档测试口径
- 本轮修改文件:
docs/smoke-test.sh、docs/tests/smoke-test-sms-code-test.sh、本报告 - 本轮未触碰:S01 Java 生产代码(时间戳仍为 15:45,本轮为 17:38+ 仅改 docs)
T1. F4 修复前的泄露位置
以 bash -x 执行时,以下位置被 trace 到 stderr(原文来自复验报告,此处仅列位置):
| 位置 | 泄露内容 | 原因 |
|---|---|---|
PHONE="${STUDENT_PHONE}" / 生成分支 |
完整手机号 | 脚本开头未关闭 xtrace |
SMS="$REPLY" |
验证码 | sms_code() 返回前 set -x 恢复,赋值被 trace |
LOGIN_BODY="{...code...}" |
验证码 + 完整手机号 | 赋值被 trace |
printf '%s' "$LOGIN_BODY" | curl ... |
登录请求体(含验证码) | 命令与参数被 trace |
LOGIN="$(...)" |
登录响应全文(含 JWT) | 赋值被 trace |
TOKEN="$(extract_token "$LOGIN")" |
JWT | 赋值被 trace |
AUTH="Authorization: Bearer $TOKEN" |
JWT | 赋值被 trace |
后续 curl ... -H "$AUTH" |
JWT | 命令参数被 trace |
sms_code() 内部 set +x / set -x |
只保护了取码阶段 | 保护范围不足 |
T2. 全局 xtrace 处理方式
采用「开头立即关闭且全程不恢复」(任务书推荐方案):
#!/usr/bin/env bash
# ── xtrace 防护(S01 第三轮 F4)──
# 必须在读取任何环境变量、组装任何请求之前关闭 xtrace。
# 脚本是独立可执行程序,安全优先:全程不重新 set -x。
case "$-" in
*x*) set +x ;;
esac
set -u
- 位置:
set +x位于任何环境变量读取与任何请求组装之前(仅在 shebang 与注释之后) sms_code()内部的xtrace_was_on与恢复set -x逻辑已彻底删除(grep -c xtrace_was_on= 0)- 脚本中不存在可执行的
set -x(仅 1 处注释提及,测试 F4-9 已断言) - 保护范围覆盖全脚本:验证码取码、手机号、请求体、登录响应、TOKEN、AUTH 及后续所有携带 Authorization 的 curl 调用
- 未使用
curl -v/--trace/--trace-ascii(测试第 20 条断言) - 登录响应全文不再交给打印函数:新增
extract_status_fields()+report_fields(),登录步骤只把 http / business code / requestId 传出去(测试第 21 条断言)
T3. bash -x 真实执行结果
由测试脚本内部驱动(捕获被测脚本 stdout+stderr 后扫描标记,只输出 PASS/FAIL):
| 用例 | 结果 |
|---|---|
| F4-1 bash -x 下不泄露验证码 | PASS |
| F4-2 bash -x 下不泄露完整手机号 | PASS |
| F4-3 bash -x 下不泄露 JWT | PASS |
| F4-4 bash -x 下不泄露 Authorization 头 | PASS |
| F4-5 bash -x 下不泄露数据库密码 | PASS |
| F4-6 bash -x 下不泄露 LOGIN_BODY | PASS |
| F4-7 bash -x 下正常流程仍可完成登录 | PASS |
| F4-8 bash -x 下登录步骤判定 PASS | PASS |
| F4-9 脚本中不存在可执行的 set -x | PASS |
| F4-10 失败流程(无可用验证码)同样不泄露 | PASS |
| F4-11 source 模式下 REPLY 契约可用 | PASS |
| F4-12 source 模式下清理后无敏感变量残留 | PASS |
| F4-13 source 模式下 EXIT trap 已登记清理函数 | PASS |
| F4-14 cleanup_secrets 覆盖 PHONE/TOKEN/AUTH/COMPANY_TOKEN | PASS |
另外单独执行 bash -x docs/tests/smoke-test-sms-code-test.sh → exit=0、PASS=51/FAIL=0;
该模式下测试脚本自身的假值可能被 trace(任务书已明确属预期,不作为安全判定),
但测试结论输出(stdout)中标记值命中数为 0。
T4. stdout/stderr 敏感标记命中数
以 bash -x 执行被测 smoke-test.sh(正常流程与失败流程各一次),对捕获的 stdout+stderr 递归扫描:
| 标记类别 | 命中数 |
|---|---|
| 测试验证码 | 0 |
| 完整手机号 | 0 |
| 测试 JWT | 0 |
Authorization: Bearer |
0 |
| 数据库密码标记 | 0 |
LOGIN_BODY= |
0 |
(报告不记录标记值本身;标记值均为测试用固定假值,仅存在于测试脚本内。)
T5. TOKEN / AUTH 生命周期
| 变量 | 生命周期 |
|---|---|
REPLY |
sms_code() 成功时写入;调用方立即 SMS="$REPLY"; unset REPLY |
SMS |
组装 LOGIN_BODY 后立即 unset SMS |
LOGIN_BODY |
经 --data-binary @- 送入 curl 后立即 unset LOGIN_BODY |
LOGIN(响应全文) |
仅用于提取 http/code/requestId 与 token;extract_token 后立即 unset LOGIN |
LOGIN_FIELDS |
只含可安全打印的三字段,用完 unset |
TOKEN |
保留到流程结束(后续接口需要),但不打印、不写文件、不进验收产物 |
AUTH |
由 TOKEN 派生,仅作为 curl 头参数使用;EXIT trap 统一清理 |
COMPANY_TOKEN |
调用方传入,仅作 curl 头参数;EXIT trap 统一清理 |
PHONE / STUDENT_PHONE |
EXIT trap 统一清理;控制台只输出脱敏形式 |
cleanup_secrets() 现覆盖:PHONE SMS OTP REPLY LOGIN_BODY LOGIN TOKEN AUTH 与 STUDENT_PHONE COMPANY_TOKEN,由 trap cleanup_secrets EXIT 统一清理。
注意:登记块不再给 PHONE/STUDENT_PHONE/COMPANY_TOKEN 赋空值(避免覆盖调用方传入值)。
T6. F5 修复前后的 mysql 调用形式
修复前
docker exec -i "$container" sh -c \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -N -B "${MYSQL_DATABASE:-mydreamjob}"'
容器内 shell 会把 $MYSQL_ROOT_PASSWORD 展开为 -p<真实密码>,进入 mysql 进程 argv。
修复后
docker exec -i "$container" sh -c '
if [ -z "${MYSQL_USER:-}" ] || [ -z "${MYSQL_PASSWORD:-}" ]; then
echo "db credentials unavailable" >&2
exit 3
fi
MYSQL_PWD="$MYSQL_PASSWORD" exec mysql \
-u"$MYSQL_USER" \
-N -B \
"${MYSQL_DATABASE:-mydreamjob}"' 2>/dev/null \
| head -n 1
T7. mysql argv 是否含密码
测试用假 mysql 记录自身真实 argv(它正是被检查的进程),实测结果形态为:
-u<应用账号> -N -B <数据库名>
| 断言 | 结果 |
|---|---|
| F5-1 mysql argv 不含数据库密码值 | PASS |
| F5-2 宿主机 docker 命令行不含密码值 | PASS |
| F5-3 容器侧脚本文本不含密码值 | PASS |
| F5-4 容器侧脚本以变量名引用 MYSQL_PASSWORD | PASS |
F5-5 mysql argv 不含 -p<密码> |
PASS |
F5-6 mysql argv 不含 --password |
PASS |
| F5-13 密码不经 argv 传递 | PASS |
F5-14 密码经 MYSQL_PWD 环境变量传递 |
PASS |
T8. 使用的数据库账号类型
- 使用应用账号(容器环境变量
MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE) - 不使用 root;
MYSQL_ROOT_PASSWORD在脚本中命中数为 0,-uroot命中数为 0(F5-7 / F5-8 PASS) - 凭据缺失时
exit 3安全失败,不回退 root(F5-15 PASS) - 未读取、未打印、未落盘凭据;未修改 compose.yaml;未修改 MySQL 用户权限;查询仅
SELECT(F5-11/F5-12 PASS)
T9. Shell 测试实际数量
docs/tests/smoke-test-sms-code-test.sh:51 用例,PASS=51 / FAIL=0
构成:F1 取码保护 10 项 + F4 xtrace/源模式 14 项 + F5 argv 18 项 + 静态保证与文档门禁 9 项。
T10. Maven 专项 / 关联 / 全量数量
| 命令 | Tests run |
|---|---|
| 7 类安全专项(含空值与回滚集成测试) | 61 |
*Auth*Test,*Security*Test,*Sms*Test,*Job*Test |
227 |
mvn test |
1671 |
mvn clean verify |
1671 |
T11. Failures / Errors / Skipped
Failures=0、Errors=0、Skipped=0(专项、关联、全量、clean verify 全部一致)
T12. clean verify
mvn clean verify → BUILD SUCCESS(4:34),产出最新 JAR。
T13. OpenAPI 数量
173 paths / 211 operations(All checks passed!);本轮未改 Controller 映射、未新增取码接口。
T14. 精确修改文件(本轮)
| 文件 | 变更 |
|---|---|
docs/smoke-test.sh |
开头新增全局 xtrace 关闭(全程不恢复);删除 xtrace_was_on/恢复逻辑;otp_db_query 改为应用账号 + MYSQL_PWD(去 root、去 -p);cleanup_secrets 覆盖面扩展;新增 report_fields/extract_status_fields,登录响应不再交给打印函数 |
docs/tests/smoke-test-sms-code-test.sh |
扩展至 51 用例:真实 bash -x 执行与标记扫描、假 docker 执行容器侧脚本 + 假 mysql 记录 argv、source 模式泄漏防护、活跃文件文档门禁 |
docs/s01-remediation-f1f2f3-20260911.md |
新增本「第三轮定向整改」章节 |
未修改:任何 Java 生产代码、任何既有测试、docs/smoke-test.sh 以外的文档正文、.env、compose.yaml、Flyway。
T15. Git 状态
- 分支:
fix/resume-upload-safety(未变) - 暂存区:空
git diff --check:无空白错误(仅 CRLF 提示)- 迁移改动:无(V1–V41 共 41 个)
- 删除文件:0;历史未跟踪文件:145 个全部保留
- 本轮已跟踪文件改动仅
docs/smoke-test.sh(S01 Java 文件时间戳仍为 15:45,本轮未触碰)
T16. 是否修改 Flyway、表、ErrorCode 或接口
均未修改:无新增/修改迁移、无表/索引变更、ErrorCode 未改、OpenAPI 保持 173/211。
T17. 明确未暂存、未 commit、未 push
未执行 git add / git add -A、未 commit、未 push(亦未 reset/restore/checkout/clean)。
T18. 真实 MySQL / Docker 最终复验仍待独立执行
本轮未执行任何 Docker 命令,未连接真实 MySQL:
- F5 的
docker exec+MYSQL_PWD路径仅在假 docker / 假 mysql 下验证了 argv 与 stdin 形态; 真实容器中MYSQL_USER/MYSQL_PASSWORD是否存在、MYSQL_PWD是否被 mysql 客户端接受,必须由独立复验在真实 Docker/MySQL 环境确认。 - F4 的 xtrace 防护已由真实
bash -x本地执行验证(无需容器),但完整真实登录链路仍待真实环境复验。
T19. 文档测试口径修正(§7)
历史验收报告 docs/s01-independent-mysql-acceptance-20260911.md 与本报告合理保留修复前缺陷证据(含修复前的日志依赖描述与旧命令),因此不得以「整个 docs 目录命中数为 0」作为门禁结论。
修正后的口径(已落入 shell 测试第 22-24 条断言):
- A. 活跃操作文档与正式代码(必须无「从日志取码」有效指导):
docs/acceptance-week1.md、docs/api-v0.1.md、docs/frontend-integration.md、docs/offer-api.md、docs/resume-api.md、docs/smoke-test.sh、README.md、mydreamjob-server/src/main/java - B. 历史验收报告(允许保留修复前失败证据,须明示为历史):
docs/s01-independent-mysql-acceptance-20260911.md、docs/s01-remediation-f1f2f3-20260911.md
测试使用精确活跃文件列表扫描(不扫描全部 docs),并额外断言两份历史报告仍然存在。
T20. 本轮遗留(非阻断,待后续)
- JWT 仍作为
-H "Authorization: Bearer <token>"的 curl 参数存在于进程 argv(非 xtrace/stdout/stderr 问题;如需消除,应改用curl --config经 stdin 传头) - 验证码仍为数据库明文存储(未改表结构)
- 真实短信供应商未接入
- 存量 43 处直接身份解析未收口;安全层 401 响应不含 requestId
- 上轮观察项(失败后残留
sms:rate限流键 60s;Spring Boot 默认generated security password入日志)本轮未处理
S01第三轮定向整改完成,待独立代码复验及真实MySQL/Docker黑盒复验。
第四轮定向整改(F6)
- 整改日期:2026-09-14
- 独立复验结论:S01 仍 FAIL —— F4 与 F5 已通过(bash -x 不泄露 / Shell 51 项 / Maven 安全专项 61 项 / 密码不进 mysql argv 均通过),新阻断项 F6:冒烟脚本使用了
sms_verification_codes不存在的列expires_at - 本轮范围:仅修 F6,并补真实表结构契约测试
- 本轮修改文件:
docs/smoke-test.sh、docs/tests/smoke-test-sms-code-test.sh、docs/tests/smoke-test-schema-contract-test.sh(新增)、本报告 - 本轮未触碰:全部 Java 生产代码、实体、Repository、Flyway、数据库表、
ErrorCode、公网接口
G1. F6 错误位置
| 项 | 内容 |
|---|---|
| 文件 | docs/smoke-test.sh |
| 行 | L122 |
| 修复前 | " AND expires_at > NOW()" \ |
| 修复后 | " AND expired_at > NOW()" \ |
| 后果 | 真实 MySQL 报 ERROR 1054,冒烟脚本取不到验证码,F1 未闭环 |
G2. 真实 MySQL SHOW COLUMNS 结果摘要
独立复验在真实 MySQL 执行 SHOW COLUMNS FROM sms_verification_codes,实际字段为:
id phone code purpose attempts max_attempts expired_at used created_at
不存在 expires_at。 该结论与两个契约来源一致:
| 来源 | 位置 | 结果 |
|---|---|---|
实体 @Column |
auth/SmsVerificationCode.java:46 |
@Column(name = "expired_at", nullable = false) |
| 建表 Flyway | db/migration/V1__base_schema.sql:37 |
expired_at DATETIME NOT NULL |
G3. 修复前的真实报错
ERROR 1054 (42S22): Unknown column 'expires_at' in 'where clause'
G4. 修复后的 SQL
SELECT code FROM sms_verification_codes
WHERE phone = '<phone>' AND purpose = '<purpose>' AND used = 0
AND expired_at > NOW()
ORDER BY created_at DESC, id DESC LIMIT 1;
除列名外未改动任何查询口径,以下均保持不变:phone 精确匹配、purpose 精确匹配、used = 0、expired_at > NOW()、ORDER BY created_at DESC, id DESC、LIMIT 1、仅 SELECT、使用应用数据库账号(非 root)、密码经 MYSQL_PWD 不进 argv、SQL 经 stdin、验证码不输出。
G5. 新增的 Shell 断言
docs/tests/smoke-test-sms-code-test.sh 由 51 项扩展至 69 项,新增 Part 6 共 18 条:
| 编号 | 断言 |
|---|---|
| F6-1 | otp_db_query 使用 expired_at > NOW() |
| F6-2 | otp_db_query 不再出现错误字段 expires_at |
| F6-3 | 查询只使用 sms_verification_codes 的真实列 |
| F6-4 | 真实列清单由 Flyway 派生且含 expired_at(非空、非自证) |
| F6-5 | 假 mysql 捕获到完整 stdin SQL(非无条件返回验证码) |
| F6-6 | SQL 字段错误时假 mysql 返回失败(非 0) |
| F6-6b | 字段错误时报 ERROR 1054 (42S22) |
| F6-7 | 字段错误时 sms_code() 不继续登录 |
| F6-7b | 字段错误时未发出任何 login 请求 |
| F6-8 | 正确字段时取码成功并完成登录 |
| F6-9 | expired_at 恰好等于 NOW() 时不得取出 |
| F6-10 | expired_at 晚于 NOW() 时可以取出 |
| F6-11 | expired_at 早于 NOW() 时不得取出 |
| F6-12 | used=1 不得取出 |
| F6-13 | purpose 不匹配不得取出 |
| F6-14 | phone 不匹配不得取出 |
| F6-15 | 多条有效记录只取 created_at/id 最新一条 |
| F6-15b | 与输入顺序无关,按 created_at/id 排序取值 |
另新增独立契约门禁 docs/tests/smoke-test-schema-contract-test.sh(15 条),契约来源为实体 @Column + 建表 Flyway,并交叉校验二者列集合一致:
C1–C4 来源存在性与解析非空 → C5 两个独立来源列集合一致 → C6 expired_at 存在 → C7 expires_at 不存在(表限定)→ C8 实体映射存在 → C9 expires_at 确实是其他表的真实列 → C10–C15 脚本 SQL 的表名、列存在性、列不含 expires_at、所用列全部真实。
G6. fake docker / fake mysql 是否校验 SQL
本轮之前:不校验。 旧假 mysql 只是 cat 掉 stdin 后无条件 printf "${FAKE_CODE}",因此错误的 expires_at 也能"取到"验证码并通过测试——这正是 F6 能逃过 51 项测试的原因。
本轮之后:校验。 假 mysql 改为:
- 完整读取并保存 stdin SQL(不再丢弃);
- 若 SQL 命中
sms_verification_codes且包含expires_at→ 如实复现真实 MySQL 的ERROR 1054 (42S22)并exit 1; - 设置
FAKE_ROWS时,按行套用WHERE phone/purpose/used与expired_at > NOW()语义、按created_at DESC, id DESC取值;未设置时保留历史行为供 F1/F5 既有用例使用。
G7. 过期 / 有效 / used / purpose / phone 口径
| 场景 | 假表语义 | 期望 | 结果 |
|---|---|---|---|
expired_at 恰好 = NOW() |
offset = 0 | 不取出(查询为严格 >) |
PASS |
expired_at 晚于 NOW() |
offset = +300 | 取出 | PASS |
expired_at 早于 NOW() |
offset = −300 | 不取出 | PASS |
used = 1 |
— | 不取出 | PASS |
purpose 不匹配 |
行 purpose=register,查 login |
不取出 | PASS |
phone 不匹配 |
行 phone 为另一号码 | 不取出 | PASS |
| 多条有效 | 两条均有效,created_at 不同 |
只取最新一条 | PASS |
G8. F4 / F5 回归结果
全部保持通过(本轮未改动 xtrace 防护与 MYSQL_PWD 路径):
| 回归项 | 结果 |
|---|---|
bash -x 不泄露验证码 / 完整手机号 / JWT / Authorization / 数据库密码 / LOGIN_BODY |
PASS(F4-1 ~ F4-6) |
bash -x 下正常流程仍可完成登录 |
PASS(F4-7、F4-8) |
脚本中不存在可执行的 set -x |
PASS(F4-9) |
| 失败流程同样不泄露 | PASS(F4-10) |
| source 模式 REPLY 契约 / 无敏感变量残留 / EXIT trap | PASS(F4-11 ~ F4-14) |
mysql argv 不含密码值、不含 -p/--password |
PASS(F5-1 ~ F5-6) |
| 不使用 root / 使用应用账号与数据库名 | PASS(F5-7 ~ F5-10) |
| SQL 经 stdin 且仅 SELECT | PASS(F5-11、F5-12) |
密码经 MYSQL_PWD 传递、不经 argv |
PASS(F5-13、F5-14) |
| 凭据缺失时安全失败(不回退 root) | PASS(F5-15) |
| 失败路径 stderr 不含数据库密码 | PASS(F5-18) |
不恢复 MOCK SMS CODE 日志、活跃文档不指导从日志取码 |
PASS(14、22、23) |
G9. Shell 测试数量
| 文件 | 用例数 | 结果 |
|---|---|---|
docs/tests/smoke-test-sms-code-test.sh |
69 | PASS=69 / FAIL=0 |
docs/tests/smoke-test-schema-contract-test.sh |
15 | PASS=15 / FAIL=0 |
合计 84 条,全部通过(真实 Git Bash 执行)。
断言非空转的证据(测试自检):把 smoke-test.sh 还原为 buggy 版本(expires_at)放入镜像目录,用同一套测试执行:
smoke-test-sms-code-test.sh→PASS=56 FAIL=13,其中 F6-1 / F6-2 / F6-3 翻红,F6-3 直接点名不存在的列expires_at;F6-8/10/15/15b 因取不到码一并失败;smoke-test-schema-contract-test.sh→PASS=12 FAIL=3,C12 / C13 / C14 翻红,C14 点名expires_at。
即:本轮断言确实能识别不存在的 SQL 列,而非"改前改后都通过"。
G10. Maven 专项 / 关联 / 全量测试数
| 命令 | Tests run |
|---|---|
-Dtest="SmsSenderConfigurationTest,SmsVerificationSecurityTest,SmsVerificationServiceTest,CurrentUserAccessorTest,AuthenticationBoundaryIntegrationTest,SmsEmptyPropertyStartupIntegrationTest,SmsUnavailableSenderRollbackIntegrationTest" |
61 |
-Dtest="*Auth*Test,*Security*Test,*Sms*Test,*Job*Test" |
227 |
mvn test |
1671 |
mvn clean verify |
1671 |
本轮仅改 docs/ 下的脚本与报告,未增删任何 Java 测试,故总数与上一轮一致(1671)。
G11. Failures / Errors / Skipped
Failures=0、Errors=0、Skipped=0(专项、关联、全量、clean verify 四者一致)。
G12. clean verify
mvn clean verify → BUILD SUCCESS(Total time 02:11),产出 target/mydreamjob-server-0.0.1-SNAPSHOT.jar。
G13. OpenAPI 计数
python docs/verify_openapi.py → All checks passed!,173 paths / 211 operations(与上一轮一致,未新增/删除路径或 operation)。
G14. Git 状态
- 分支:
fix/resume-upload-safety(未变) - 暂存区:空
git diff --check:无空白错误(仅 CRLF 提示)- 迁移改动:无(
git diff --name-only -- mydreamjob-server/src/main/resources/db/migration无输出;目录内 41 个文件,最大版本V41__message_recall.sql) - 本轮文件时间戳:
docs/smoke-test.sh09:22、smoke-test-sms-code-test.sh09:24、smoke-test-schema-contract-test.sh09:27、本报告 09:32(均 2026-09-14) - S01 Java 生产代码时间戳仍为 2026-09-11 15:45,本轮未触碰
- 删除文件:0;历史未跟踪文件:未删除、未移动、未覆盖
G15. 明确未修改 Java、Flyway、表、ErrorCode 或接口
均未修改。
- Java 生产代码:0 处改动(时间戳可证)
- 实体与 Repository:未改
- Flyway:未新增/修改,V1–V41 原样
- 数据库表/列/索引:未新增或修改;未为迁就脚本把列改成
expires_at ErrorCode:未改- 公网接口:OpenAPI 保持 173/211,未新增取码接口
- 未恢复验证码日志(
grep -rn "MOCK SMS CODE" mydreamjob-server/src命中 0)
G16. 明确未暂存、commit、push
未执行 git add / git add -A、未 commit、未 push(亦未 reset/restore/checkout/clean)。未操作 Docker,未进入 S02。
G17. 真实 MySQL 完整冒烟链仍待最终独立复验
本轮未执行任何 Docker 命令、未连接真实 MySQL。全部验证均为:shell 测试(假 docker / 假 mysql 注入)+ Maven H2/Flyway 测试。
因此以下必须由独立复验在真实 MySQL/Docker 环境完成:
SHOW COLUMNS FROM sms_verification_codes的 Field 集合与 Flyway 列集合对账(含expired_at、不含expires_at);- 修复后的取码 SQL 在真实 MySQL 端到端执行成功(不再出现 ERROR 1054);
- F1 完整冒烟链(发码 → 取码 → 登录 → 后续接口)闭环。
不得以本轮假库结果代替真实环境验收。
G18. 一处必须记录的旁证(不在本轮修改范围)
docs/acceptance/s01-20260911/snapshot-sms.sql:4 同样使用 expires_at 查询 sms_verification_codes:
SELECT id, purpose, CHAR_LENGTH(code) AS code_len,
TIMESTAMPDIFF(SECOND, NOW(), expires_at) AS expire_delta_s, ...
FROM sms_verification_codes
这是同一字段错误的传播。该文件属上一轮独立验收证据,不在本轮允许修改的文件范围内,故保持原样未改,仅在此记录,供后续处理。
G19. 字段名易混淆的说明(供后续维护)
expires_at 并非拼写错误,它是本仓库中三张表的真实列:
| 表 | 迁移 |
|---|---|
personal_data_export_tasks.expires_at |
V30 |
ability_attempts.expires_at |
V35 |
talent_invitations.expires_at(含索引) |
V40 |
只有 sms_verification_codes 用 expired_at。因此本轮门禁按表限定,未写成"全库禁用 expires_at"——那样会误报并掩盖真实错误。契约门禁 C9 专门守卫这一区分。
S01第四轮定向整改完成,待最终独立代码复验及真实MySQL/Docker黑盒复验。