4 Commits

Author SHA1 Message Date
cf91f6ba34 fix(download): 哨兵旁路改走 Content-Disposition 的 size= 扩展参数(R103 收口)
背景(R103-3 生产实测已定因):
  X-Accel-Redirect 分支下真正吐字节的是 nginx,Node 侧无从得知 nginx 是否吐对
  了内容(内部 location 缺失时会吐前端 index.html)。原实现靠自定义响应头
  X-File-Size 把「服务端 stat 的真实字节数」透出给前端比对,但双通道对照实测
  (.workbuddy/probes/2026-10-12-r103/prod_xfs_passthrough.js)证伪了原注释所称
  「nginx 会保留上游自定义响应头」:
    · 直连后端 :8023 ⇒ 204 空体,响应带 x-file-size: 19009
    · 经 nginx :80  ⇒ 200 / 19009 字节(与磁盘逐字节一致),而 x-file-size **缺失**
    · 对照 /api/health 经 nginx ⇒ 自定义头 x-readtime **保留**
  ⇒ 丢弃只发生在 X-Accel 内部重定向这一条路径。
  影响:该分支下前端哨兵空转(下载完整性校验被静默跳过)。

修法:
  把同一份字节数写成 RFC 6266 §4.1 的 disposition 扩展参数 `size=`(实测
  Content-Disposition 能穿透 X-Accel),前端按 x-file-size → CD size= 顺序回落。
  两通道同源同值;任一缺失只降级不误伤。Node 流分支(absolutePath)不加旁路 ——
  其响应走 nginx 常规转发,自定义头本就保留。

验证(2026-10-12 生产部署后实测):
  · 本地判别力自证 verify_cd_size_channel.js:pass=12 fail=0
    (含 C1 变异测试:修复前旧 CD 形态解不出 ⇒ 判据确判别力)
  · 生产判决点 r106_prod_sentinel.js:pass=11 fail=0
    ARM A(直连) CD 含 size=19009;ARM B(经 nginx) CD 的 size=19009 穿透成功;
    B2 端到端 sha256 与磁盘逐字节一致;C1 对照组佐证丢弃仅限 X-Accel 通道。
2026-10-12 11:05:47 +08:00
3b4d422e36 fix(download): X-Accel-Redirect 头值须 URL 编码(修复中文名下载 500)
承接 c94d8fe:上一提交把中文名从 400「非法文件路径」放行后,暴露出
同一族 bug 的第二处漏网 —— sendFile.js:98 把含中文的 relativePath
直接拼进 X-Accel-Redirect 头值,Node res.setHeader 抛
ERR_INVALID_CHAR ⇒ 500(生产实测,common-error.log 为铁证)。

该分支仅在 NODE_ENV=production(X-Accel 开启)下命中,本地 dev 走
Node 流分支 ⇒ 永远测不出,典型「本地绿、生产红」陷阱。

修法:内部路径逐段 encodeURIComponent(保留 / 分隔符)得到纯 ASCII
的 internalUri;nginx 按解码后 URI 匹配 internal location,alias 落到
磁盘真实中文文件名。

验证:X-Accel 头编码判别力自证 PASS=7/FAIL=0(旧写法必抛
ERR_INVALID_CHAR;新写法头全 ASCII、逐段解码 == 原路径);
经 nginx 生产端到端 PASS=7/FAIL=0(中文名 200、bytes==磁盘 size、
下载内容 sha256 == 源文件 sha256、RFC5987 头正确);
直连 8023 对照 204/0 字节(反证字节确由 nginx 吐出)。
2026-10-12 09:34:32 +08:00
c94d8fe3ff fix(download): sendFile 路径校验支持中文文件名(修复 400 非法文件路径)
根因:app/utils/sendFile.js:36 的路径穿越防护用全 ASCII 白名单
/^[A-Za-z0-9_.\/-]+$/,把合法中文文件名(如 测试.xlsx)误判为非法
路径 ⇒ 400「非法文件路径」。该正则与同文件 :88 的 RFC 5987
(filename*=UTF-8''…)中文名支持自相矛盾 —— 中文名本是设计内支持。

影响面:sendFile 是统一文件出口,materials(备赛资料,磁盘保留原始
中文/全角名)/ exam(Word 模板)/ admin(作品·导出)三通道,其中
materials 重度受影响(选手侧直接可见);works 通道磁盘名 ASCII 化
(work_<ts>_<rand>.<ext>)不受影响。

修法:把安全判据从「字符集白名单」换成「危险形态黑名单(控制字符/
反斜杠/绝对路径/.. 段)+ path.resolve 归一化后前缀校验(必须落在
app/public 内)」双层防护,不误伤 Unicode,且防穿越更硬。

验证:端到端(真后端 7001 + 真 DB)PASS=9/FAIL=0(中文名 200、
字节==磁盘 size、X-File-Size 哨兵一致);sendFile 守卫判别力自证
PASS=9/FAIL=0(5 类危险形态全 400、3 类合法 Unicode 放行);
既有门禁 verify_upload_download.py 16/16 通过。
2026-10-12 09:28:44 +08:00
WorkBuddy
a303cf2d53 chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。
已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
2026-08-17 12:25:42 +08:00