轻松聘AI开发
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

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. 为什么不会把验证码输出到终端 / 日志 / 文件

  1. 取码函数从不 echo/printf 验证码,只写 REPLY;调用方 SMS="$REPLY" 后立即 unset REPLY
  2. set +x 包住取值与查询,且按调用前状态精确恢复,不无条件开启 xtrace
  3. 登录请求体经 --data-binary @- 从 stdin 送入 curl,验证码不进入 argv
  4. 请求体只在 LOGIN_BODY 局部变量中存在,用完 unset;不落盘
  5. trap cleanup_secrets EXIT 兜底清理 OTP/REPLY/LOGIN_BODY
  6. 脚本控制台只输出 PHONE=$(mask_phone "$PHONE")(脱敏为 199****9999),不打印完整手机号
  7. 应用侧本就不输出验证码(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 节

文档统一口径(已落实):

  1. 删除所有「从控制台/日志查看验证码」的操作指导
  2. 明确应用日志不会输出验证码
  3. dev/test 本地自动化使用受控测试夹具/数据库内部只读查询
  4. 冒烟脚本仅在 mock=true 且 ALLOW_TEST_OTP_DB_READ=true 时查表
  5. 真实生产环境禁止查询验证码表辅助登录
  6. mock=false 且无真实供应商时返回「服务不可用」
  7. 明确当前尚未接入真实短信供应商
  8. 未声称验证码已加密/哈希存储
  9. 明确验证码当前仍为数据库明文遗留风险
  10. 示例中不含真实手机号、验证码、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 结果代替。


遗留与后续

  1. 验证码仍为数据库明文存储(本轮未改表结构,属已知遗留)
  2. 真实短信供应商未接入;生产 mock=false 下发码一律安全失败,需产品确认上线策略
  3. 存量 43 处直接身份解析未收口(均不可匿名到达;DeletionGuardInterceptor 对 20 位数字溢出仍可能 500)——非本轮范围
  4. 安全层 401 响应不含 requestId(既有 AuthenticationEntryPoint 固定载荷),与业务层错误响应格式不一致
  5. 上一轮验收的其余观察项(失败后残留 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. 本轮遗留(非阻断,待后续)

  1. JWT 仍作为 -H "Authorization: Bearer <token>" 的 curl 参数存在于进程 argv(非 xtrace/stdout/stderr 问题;如需消除,应改用 curl --config 经 stdin 传头)
  2. 验证码仍为数据库明文存储(未改表结构)
  3. 真实短信供应商未接入
  4. 存量 43 处直接身份解析未收口;安全层 401 响应不含 requestId
  5. 上轮观察项(失败后残留 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 改为:

  1. 完整读取并保存 stdin SQL(不再丢弃);
  2. 若 SQL 命中 sms_verification_codes 且包含 expires_at → 如实复现真实 MySQL 的 ERROR 1054 (42S22) 并 exit 1;
  3. 设置 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.sh 09:22、smoke-test-sms-code-test.sh 09:24、smoke-test-schema-contract-test.sh 09: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 环境完成:

  1. SHOW COLUMNS FROM sms_verification_codes 的 Field 集合与 Flyway 列集合对账(含 expired_at、不含 expires_at);
  2. 修复后的取码 SQL 在真实 MySQL 端到端执行成功(不再出现 ERROR 1054);
  3. 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黑盒复验。