Skip to content

fix(authz): enforce current role, token revocation, S3 permissions - #92

Merged
PIKACHUIM merged 8 commits into
mainfrom
fix/security-authz-path-isolation
Oct 15, 2026
Merged

PIKACHUIM merged 8 commits into
mainfrom
fix/security-authz-path-isolation

Conversation

@PIKACHUIM

@PIKACHUIM PIKACHUIM commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

fix(authz): 授权校验、路径隔离与元信息 ACL 全面对齐 Go

分支:fix/security-authz-path-isolation
提交:e3a95c2(第一轮)→ 22dd197(第二轮)→ 3ab8361(合入 origin/main)→ 9bc8175(第三轮)
规模:23 个文件,+1718 / −512(相对 origin/main)
验证:tsc --noEmit 0 error;测试 372 项全绿(server 179 / model+pkg 50 / store 12 / drivers 131)
说明:本文档为三轮修复的合并版 PR 正文,可直接粘贴到 PR 描述。


一、背景与动机

一次针对本仓库的安全评审(代码走查 + 隔离环境 PoC 实测)确认了若干授权、
隔离与协议一致性问题
。它们的共同特征是:

  1. 「签发时刻合法」被当成「当前仍然合法」 —— 注销/降权/禁用/改密后旧令牌
    在 7 天有效期内继续生效;
  2. 「路径字符串相等」被当成「权限边界成立」 —— 受限用户可越出 base_path,
    各协议入口各自为政;
  3. 「只有单一网关做了校验」 —— /api/fs/* 有 meta 判定,而 /dav、/s3、
    /fs/dirs、/fs/search、/fs/archive/* 完全没有,形成跨协议权限旁路;
  4. 删除/上传/重命名共用同一个 WRITE_CONTENT 权限位,与实际权限模型不符。

这些缺陷不会把数据泄露到公网,但会在多用户 / 多协议部署下破坏权限模型。
本 PR 在不改接口结构、不需要前端改动的前提下逐项修复;需要兼容性设计的
问题(S3 SigV4、任务队列、归档格式等)留作后续,见 §七。


二、三轮修复总览

轮次 提交 主题 规模
一 e3a95c2 令牌吊销 / 当前角色 / S3 权限 / 用户根隔离 4 文件,+185 / −43
二 22dd197 独立权限位、归档 ACL 与边界、损坏配置守卫、上传限额、WebAuthn 加固、存储一致性 12 文件,+811 / −212
三 9bc8175 meta ACL 跨协议打通、JWT 密码指纹、用户角色保护、按评审删除 4 项功能 14 文件,+728 / −263
— 3ab8361 合入 origin/main(#93:EdgeOne 产物改为部署期生成) 无冲突

2.1 第一轮(e3a95c2)要点

项 缺陷 修复
checkAdminAuth 只读 JWT 的 payload.role,从不检查 jti —— 注销/降权后旧令牌仍可访问后台(实测 200) 以数据库为权威:签名有效 + jti 未吊销 + 用户存在未禁用 + 当前 role 仍是管理员
authUserFromReq 只验签名+查库,缺 disabled 判断(S3 网关直连复用本函数) if (!user || user.disabled) return null
S3 网关 只判断「是否登录」,直接操作全局 /bucket/key —— 只读/禁用用户可写、受限用户可跨目录读写 PutObject 需 WRITE_CONTENT、DeleteObject 需 DELETE;路径映射为 <base_path>/bucket/key 并 isWithinUserRoot 复核;bucket/key 语法校验
getActualPath 只做字符串拼接,.. 留给下游折叠 —— /photos/alice + /../bob/x → /photos/bob/x(用户根被越过) 新增 normalizeSegments()(栈折叠、空栈钳制在根),先折叠再拼接;导出 isWithinUserRoot() 供网关复检
OTP 防爆破 /login、/login/hash 的 OTP 失败分支直接 return,不计数 —— 6 位 TOTP 可无限爆破 OTP 失败调用 recordLoginFailure,与密码失败共用 IP+用户名 / 全局计数与指数退避

2.2 第二轮(22dd197)要点

# 问题 修复
1 归档接口不校验元信息密码与 READ_ARCHIVES / DECOMPRESS 位;解压目标未做 base_path 映射;ZIP 条目 ../ 可逃出目标目录 list/meta 需 READ_ARCHIVES;decompress 需 DECOMPRESS + 写权限;读取与 /fs/get 共用 meta+密码判定;目标经 getActualPath + isWithinUserRoot;新增 safeArchiveEntryPath 限定条目为「只能向下展开的相对路径」
2 map 格式把 JSON 解析失败转成 null,随后 /api/public/init/setup 重新开放初始化并覆盖配置 解析失败抛错,损坏 ≠ 空库,初始化接口随之拒绝
3 上传体积只看 Content-Length,缺失该头即不拦截 新增 readBodyWithinLimit,按实际收到字节复核;/put、/form、分片全部接入
4 isSafeUrl 放过 IPv4-mapped IPv6 私网地址(http://[::ffff:127.0.0.1]/) IPv6 解析成数值组再判网段(覆盖 IPv4-mapped / 6to4 / NAT64);IPv4 判定抽成 isRestrictedIpv4 复用
5 rename / remove / move / copy 统一使用 canWrite 改用各自权限位:RENAME / DELETE / MOVE / COPY
6 目录 write_users ACL 只影响列表 write 提示,写接口不复查 新增 deniedByMetaWriteAcl,在全部写入入口执行
7 分片会话仅按 path+size 复用,无 owner;status 无需认证 会话绑定 owner_id+owner_root;chunk/complete/status 全部校验所有者
8 令牌吊销名单按实例加载且走旧 KV 通道,D1-only 环境不可靠 改用统一存储后端(D1/MySQL/DO/KV/Blob)+ 30s TTL 回读;本实例注销仍立即生效
9 存储一致性:key 格式先删后写、saveDb 失败仍更新缓存、D1 分批无重试 key 改为先写后删;持久化失败回滚内存与请求缓存;D1 分批单片重试并报告已提交片数

补充加固:WebDAV/S3 源路径与 Destination 均映射到调用者根目录并复核;
归档下载改用与 /d、/p 相同的 safeProxyFetch(禁用自动跟随 + 逐跳 SSRF 断言);
WebAuthn 挑战改为服务端签发、单次消费、5 分钟有效,新增 origin / UP / UV 校验
并拒绝已禁用账号。


三、第三轮(9bc8175)细节

3.1 目录级访问控制(meta ACL)跨协议打通 —— 本次核心

缺陷:meta 定义的密码保护、read_users / write_users 白名单只在
/api/fs/list、/api/fs/get 生效;下列入口完全不做 meta 判定,构成
跨协议绕过:/fs/dirs、/fs/search、WebDAV、S3。实测:直连被密码保护而
401 的目录,能经这些入口枚举/读取。

修复:所有入口统一复用 pkg/meta 的判定(getNearestMeta + canAccess /
canRead / canWrite / canWriteContentBypassUserPerms)。

入口 读判定 写判定
/fs/dirs 密码 + read_users(403 文案对齐 Go:password is incorrect or you have no permission);force_root 仅管理员 —
/fs/search search() 新增 user/password 选项,按父目录最近 meta 逐项过滤(对齐 Go SearchFiltered 的 filter 闭包) —
WebDAV PROPFIND/GET/HEAD 密码 + read_users —
WebDAV PUT/MKCOL — WRITE_CONTENT 或 meta.write 强制开放 + write_users
WebDAV DELETE — 目标自身 write_users
WebDAV MOVE — MOVE 位(跨目录)/ RENAME 位(改名)+ 源/目标目录写判定
WebDAV COPY 源目录 read_users COPY 位 + 目标目录写判定
S3 GET/HEAD 密码(?pwd= / x-meta-password)+ read_users —
S3 PUT — 目标目录:WRITE_CONTENT 或 meta.write + write_users
S3 DELETE — 目标自身 write_users

WebDAV meta 密码通道对齐 Go WebDAVAuth:仅 guest 账号会把 Basic
密码继续作为 meta 密码(if user.IsGuest() { MetaPassKey = password }),
普通用户一律为空 —— 目录密码不能靠账号密码顺带绕过。

3.2 /fs/get_direct_upload_info 对齐 Go

原实现只有 canWrite(user) 一道门槛,其余四重校验全部缺失。现补齐:

  1. validateFileName(file_name):拒绝空值、/、\、.、.. —— 防止签发
    指向目标目录之外的直传凭证(如 S3 预签名 URL);
  2. 目标目录需 WRITE_CONTENT 或 meta.write 强制开放(此前
    write_content_bypass 只在响应里展示、实际不生效);
  3. 目标目录需通过 meta.write_users 白名单;
  4. 存储挂载边界:目标文件与所在目录必须属于同一挂载点(单段文件名也
    可能命中嵌套挂载点);
  5. Overwrite: false 时目标已存在 → 403 file exists。

3.3 JWT 密码指纹(pwd_ts)—— 改密即全端下线

缺陷:JWT 只有 {id, username, role, exp, jti},没有密码指纹。
Go 每次请求都用 PwdTS 与数据库当前值比对(改密 = 旧令牌立即失效);
TS 侧改密后旧令牌在有效期内(7 天)继续可用,除非用户显式登出。

修复(对齐 Go common.UserClaims.PwdTS + middlewares.Auth):

  • pkg/password.ts 新增 passwordStampOf() / isPasswordStampValid();
    setUserPassword 本就刷新 pwd_update_at;
  • 三处签发补 pwd_ts:/api/auth/login、/api/auth/login/hash、
    SSO(sso.ts)、WebAuthn(webauthn.ts);
  • 三处校验:authUserFromReq、getUserFromContext、checkAdminAuth。

3.4 管理员用户管理对齐 Go

接口 修复前 修复后(对齐 Go)
user/create role 可随意传 1/2,可批量制造管理员/guest 拒绝,admin or guest user can not be created
user/update role、disabled 无限制 —— 可提权、可禁用唯一管理员把自己锁死 拒绝改 role(role can not be changed);拒绝禁用管理员(admin user can not be disabled)
user/delete 只硬编码拦 id===1,guest(id=2)可被删,id≠1 的管理员不受保护 按角色保护:cannot delete admin or guest

3.5 WebDAV 认证加固

  • 防爆破:Basic Auth 失败计数/锁定与 /api/auth/login 共用同一套机制
    (对齐 Go 的 model.LoginCache 在登录与 WebDAV 间共用),超出阈值返回 429;
    此前 /dav 可无限次尝试密码。
  • OPTIONS 匿名放行(对齐 Go):客户端在提供凭据前需要探测 DAV 能力。

3.6 顺带修复 5 个既有 tsc 报错

db_cipher.test.ts 重复 type DbCipher 导入;fs.ts /form 的
getUploadSizeLimit(c) 少传参;multipart/init 的 user 空值收窄。
(这些是改动前就存在的问题,本次一并修掉,使 pnpm lint 通过。)


四、按评审决定的功能增删

4.1 删除(4 项)

项 说明
bcrypt / 无盐单层哈希兼容与登录时自动迁移 只保留 Go 双层格式(salt + saltedHash,无 salt 一律不可验证)。保留 F-11「不静默重置遗留哈希」的保护
user/create 自动生成随机密码并回显 与 Go 一致:直接采用调用方传入值,不再生成/回显
TS 专有 /fs/upload/{create,part,complete} 分片只保留官方 /fs/multipart/*(Go 有的才有)
前端 Face404 动画组件 独立 PR,见文末附录

4.2 保留(TS 独有,评审确认保留)

CSRF 签发接口、审计日志系统与查询/统计/清理、全局安全响应头
(CSP/HSTS/X-Frame-Options 等)、ip_limit / traffic_limit 进程内限流、
管理员用户列表的 otp 布尔字段、env_check / kv/status / health(z)
自检接口、插件管理系统、逐跳 SSRF 校验 + 受信主机白名单、
POST /task/refresh(Cron 触发的存储 Token 刷新)、/fs/archive/decompress
的 zip-slip + 用户根双重防护。

Seed 统一元数据体系(种子/CAS/OSS 三态互转)按决定「以 Go 版本为主」,
在独立分支处理,本次未改动。


五、验证

5.1 自动化测试

cd OpenList-TSWorker
npx tsx --test "src/backend/server/*.test.ts"          # 179 项
npx tsx --test "src/backend/internal/model/*.test.ts" "src/backend/pkg/*.test.ts"  # 50 项
npm run test:store                                     # 12 项
npm run test:drivers                                   # 131 项
套件 结果
server(含新增 security_hardening.test.ts) 179 / 179
model + pkg 50 / 50
store 12 / 12
drivers 131 / 131

新增 src/backend/server/security_hardening.test.ts 覆盖:

  1. user/create 拒绝 admin/guest 角色;
  2. user/update 拒绝改角色、拒绝禁用管理员(并允许合法的权限位/base_path/禁用普通用户);
  3. user/delete 按角色保护(含 id≠1 的管理员),普通用户仍可删除;
  4. 改密使旧令牌 401、新令牌 200(pwd_ts 指纹)。

更新 default_credentials.test.ts:随机密码断言改为 Go 语义
(空密码、不回显、哈希仍为 64-hex + 盐)。

5.2 静态检查

npx tsc -p tsconfig.json --noEmit → 0 error(本轮之前存在 5 个历史报错,已一并修复)。

5.3 PoC 口径

评审阶段的行为验证全部在隔离环境完成:合成密钥、注入式内存后端、
临时文件、被禁止联网的 fetch;未触碰真实账号、真实网盘、生产数据库或
私网资源
,临时测试文件未进入提交。


六、行为变化与兼容性

6.1 需要关注的变化(第三轮)

场景 变化 说明
用户改密后 全部已签发 JWT 立即失效(含 SSO / WebAuthn 签发的)→ 需重新登录 对齐 Go PwdTS;上线时全体用户会重新登录一次
bcrypt / 无盐单层哈希账号 无法再登录(与 Go 一致) 需 ADMIN_PASS 重置或管理员改密;不会静默重置
user/create 不传密码 创建空密码账号(与 Go 一致) 不再随机生成密码,也不回显
经 /fs/dirs、/fs/search、WebDAV、S3 访问受保护路径 403(此前可绕过) meta 密码 / read_users / write_users 生效
S3 访问受 meta 密码保护的路径 需 ?pwd= 或 x-meta-password 密码传递通道
/fs/upload/* 调用方 404 请改用 /fs/multipart/*
WebDAV 无凭据 OPTIONS 401 → 200 仅返回能力头
WebDAV Basic 连续失败 触发与登录接口相同的锁定(429) —
/fs/get_direct_upload_info 非法文件名 / 越挂载点 / 目录禁写 / Overwrite:false 且已存在 → 403 此前仅校验登录与 WRITE_CONTENT

6.2 明确未改动的契约(避免误伤)

  • 未给 /d、/p、/sd 增加登录要求(签名直链需保持可被播放器/下载器匿名消费);
  • 未给下载验签侧重复拼接 base_path(官方前端已在 URL 中携带);
  • 未全局强制 CSRF 头(现有客户端不发送;按评审决定仅保留签发接口);
  • 未清洗管理员主动配置的 customize_head / customize_body(这是功能);
  • base_path="/" 用户的 S3 / WebDAV 路径映射完全不变。

七、遗留 / 后续批次

  1. CSRF 仍未全局强制:按评审决定保留签发与中间件,未挂载到全局
    (避免破坏现有客户端);如需启用建议单独评估与灰度。
  2. 第二批功能对齐(未做):/fs/put、/fs/form 的
    Overwrite / As-Task / X-File-Md5|Sha1|Sha256 / Last-Modified 请求头;
    move/copy 的 overwrite / skip_existing / merge 冲突策略;
    跨存储 move/copy 显式报错或流式转移;driver/list 改为按实际驱动动态生成;
    set_guangyapan;分享的运行期二次校验(创建者权限 / base_path)与访问计数去抖;
    archive_extensions 如实返回当前支持格式。
  3. 第三批架构项(未做):归档格式与密码支持、真实异步任务队列、
    LDAP 登录、S3 SigV4 / ListObjectsV2 / 多段上传、种子 API 路径与 Go 对齐。

八、提交信息

fix(authz): enforce current role, token revocation, S3 permissions and user-root isolation
fix(authz): per-op permission bits, archive ACL/boundaries, corrupt-config guard, upload limits, WebAuthn hardening
Merge remote-tracking branch 'origin/main' into fix/security-authz-path-isolation
fix(authz): meta ACL parity on fs/webdav/s3, JWT password stamp, Go user-role guards

第三个提交(本轮)正文:

1) 目录级访问控制补齐(消除跨协议权限旁路):/fs/dirs、/fs/search、
   WebDAV(PROPFIND/GET/HEAD/PUT/MKCOL/DELETE/MOVE/COPY)、S3(GET/HEAD/PUT/DELETE)
   现在与 /fs/list /fs/get 使用同一套 meta 判定(密码 + read_users + write_users
   + meta.write 绕过开关)。/fs/dirs 的 force_root 只对管理员开放。
2) /fs/get_direct_upload_info 对齐 Go:file_name 单层相对路径校验、meta 写 ACL、
   存储挂载边界校验、Overwrite:false 已存在探测。
3) JWT 增加 pwd_ts 密码指纹(对齐 Go UserClaims.PwdTS):登录/SSO/WebAuthn 三处
   签发,authUserFromReq/getUserFromContext/checkAdminAuth 三处校验。
4) 用户管理对齐 Go:create 禁止 admin/guest;update 禁止变更 role、禁止禁用管理员;
   delete 按角色保护 admin/guest。
5) WebDAV Basic Auth 接入登录防爆破,OPTIONS 匿名放行。
6) 删除(按评审决定):bcrypt/无盐单层哈希兼容与自动迁移;user/create 随机密码回显;
   TS 专有 /fs/upload/{create,part,complete} 端点。
7) 顺带修复 5 个改动前就存在的 tsc 报错。
验证:tsc 0 error;server 179 / model+pkg 50 / store 12 / drivers 131 全绿。

九、建议的验收要点

  1. 受限 base_path 用户的签名下载继续正常(不得 401);
  2. 管理员注销 / 降权后立即失去后台权限;用户改密后旧令牌立即失效;
  3. 受 meta 密码 / read_users 保护的目录:/fs/dirs、/fs/search、
    WebDAV、S3 一律拒绝(带正确密码时放行);
  4. 目录 write_users 白名单外的用户经 WebDAV / S3 写入 → 403;
  5. base_path="/" 用户的 S3 / WebDAV 路径映射与修复前完全一致;
  6. /fs/multipart/* 分片上传流程正常(/fs/upload/* 已移除);
  7. 公开直链 / 播放器 Range 请求 / 分享链接行为不变。

附:关联前端 PR

项 内容
仓库 OpenListTeam/OpenList-Frontend
分支 / 提交 fix/remove-face404 / 1888afe
标题 chore: remove the 404 face animation (Face404)
改动 删除 src/components/Face404.tsx;移除 components/index.ts 导出;Error 组件去掉 show404Face 属性;pages/home/Obj.tsx 不再传该属性(共 4 文件,−179 行)
验证 全仓库无 Face404 / show404Face 残留引用

…d user-root isolation

checkAdminAuth: no longer trusts the JWT role claim. Logout and demotion previously had no effect on admin routes because only payload.role was read and jti was never checked; it now re-checks the revocation list, the disabled flag and the current database role.

authUserFromReq: reject disabled users. JWT only proves the identity was valid when issued; every resolver must re-check current status. Core APIs went through getUserFromContext (which checks), but the S3 gateway reused this function and did not.

S3 gateway: PutObject now requires WRITE_CONTENT and DeleteObject requires DELETE; object paths are scoped to the caller's base_path and bucket/key are validated. Previously any authenticated user, including read-only and disabled accounts, could write anywhere.

getActualPath: fold dot segments before joining base_path so a user cannot escape their root via plain or percent-encoded traversal. verify: 225 tests pass.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
openlist-work 8121c8e Oct 15 2026, 03:12 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
openlist-tsworkers e7c16a0 Oct 15 2026, 02:46 PM

@PIKACHUIM PIKACHUIM changed the title fix(authz): enforce current role, token revocation, S3 permissions an… fix(authz): enforce current role, token revocation, S3 permissions Sep 29, 2026
…onfig guard, upload limits, WebAuthn hardening

Permission bits: rename/batch_rename/regex_rename use RENAME, remove/remove_empty_directory use DELETE, move/recursive_move use MOVE, copy uses COPY, instead of all sharing WRITE_CONTENT.

meta.write_users now enforced at write time; archive read shares the /fs/get meta+password judgement; archive list/meta require READ_ARCHIVES and decompress requires DECOMPRESS plus write, with the destination scoped to the caller root and ZIP entry names restricted to relative-only so ../ cannot escape.

Uploads re-check the actual received byte count (Content-Length could be omitted to bypass the limit). map format now throws on damaged JSON instead of returning null, so corruption is no longer mistaken for a fresh deployment that reopens public setup.

Storage: key format writes before deleting, saveDb rolls memory/cache back on failed persistence, D1 batches retry per chunk and report partial commits, S3/WebDAV scope paths to the user root, archive fetch follows redirects safely.

Sessions: revocation list uses the unified storage backend with bounded reload, multipart sessions are owner-bound, WebAuthn challenges are server-issued and single-use with origin/UP/UV checks and disabled-user rejection.

verify: 285 tests pass; SSRF probe 22 block / 8 allow; WebAuthn probe all green.

@pikachuren pikachuren left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[OUTDATED] 此评审已过时,请参阅下方增量评审(v4)。


🙏 感谢 @PIKACHUIM 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 GLM 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。

🎯 结论

🔄 Request Changes — 权限修复整体扎实,但上传大小限制的修复不完整,S3 网关完全缺失大小限制

📖 概要

一批授权加固:JWT 解析复核 disabled、OTP 失败纳入防爆破计数、目录级写 ACL 真正执行、归档读取走 meta ACL、ZIP 条目路径清理、上传大小按实际字节数复核、S3 网关补权限位与用户根目录隔离、WebAuthn challenge/origin/UV 校验。

🧭 整体方案

每个修复点都标注了「历史缺陷 + 实测复现」,用 isWithinUserRoot 统一收口路径越界,方案方向正确。问题在于大小限制这条线只修了一半。

📊 变更统计

8+ 个文件 | 功能 ⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐

🚨 关键问题

P1(建议修复后再合并):

  • ⚠️ server/fs.ts:1655-1673(/fs/multipart/part)仍只做 Content-Length 预检,无该头时直接 Buffer.from(await c.req.arrayBuffer())——readBodyWithinLimit 想封死的「省略头绕过上限」路径在这里依然可用。该函数只接入了 /fs/put(L974)和 /fs/upload/part(L1104)。建议本路由同样改用 readBodyWithinLimit(c, true),并把 L1655 的预检删掉换成统一入口。
  • ⚠️ server/s3.ts:206(PutObject)Buffer.from(await c.req.arrayBuffer()) 之前没有任何大小限制——MAX_UPLOAD/MAX_UPPART 都不生效,超大请求体可整包读入内存。与 /fs/put 的防护形成明显不对称。建议复用 getUploadSizeLimit(或新增 MAX_S3_PUT)+ 实际字节数复核。

P2(可选):

  • 💡 s3.ts:206 PutObject 的 ETag 用 Date.now().toString(16) 生成,不是内容的真实哈希;对做分片续传/幂等判断的 S3 客户端可能产生误导,是否考虑至少返回空 ETag 或内容 md5?
  • 💡 safeArchiveEntryPath 拒绝了 ../盘符/NUL,覆盖到位;/fs/other 签发直传 URL 后的写入路径不经过本 PR 的大小限制(预失效 #75 已单独处理失效),边界行为建议在 PR 描述中注明。

📂 逐文件分析

src/backend/server/fs.ts

改动意图:写 ACL 执行点 + 归档 ACL + 条目清理 + 上传限流。
问题分析:deniedByMetaWriteAcl / deniedByMetaReadAcl 管理员放行、meta 缺失时放行的语义与 pkg/meta 约定一致;readBodyWithinLimit 的实际字节数复核思路正确——只是接入点不全(见 P1)。

src/backend/server/s3.ts

改动意图:S3 权限位 + base_path 隔离。
问题分析:canWrite/canRemove 权限位、isWithinUserRoot 收口、authUserFromReq 复检 disabled 都已到位;s3VirtualPath 对受限用户把 bucket 解释为自己根下的一层目录,与 /api/fs 视图一致。缺的只有大小限制(见 P1)。

src/backend/pkg/permission.ts

改动意图:normalizeSegments + isWithinUserRoot。
问题分析:.. 越根钳制、重复 / 合并正确;建议补一条「normalizeSegments('/a/../../b') === '/b'」的显式测试。

其余文件(auth.ts / webauthn.ts 等)无重大问题。

✅ 待处理清单

  • [P1] /fs/multipart/part 改用 readBodyWithinLimit,封死无 Content-Length 绕过
  • [P1] S3 PutObject 增加大小限制 + 实际字节数复核
  • [P2] S3 ETag 语义(可选)
  • [P2] normalizeSegments 补越根钳制测试(可选)

🎯 结论:🔄 Request Changes — 权限加固值得合并,但上传限流修复不完整,两条 P1 补齐后即可通过。

pikachuren added 4 commits October 14, 2026 11:07
…ser-role guards

1) 目录级访问控制补齐(消除跨协议权限旁路):/fs/dirs、/fs/search、WebDAV(PROPFIND/GET/HEAD/PUT/MKCOL/DELETE/MOVE/COPY)、S3(GET/HEAD/PUT/DELETE) 现在与 /fs/list /fs/get 使用同一套 meta 判定(密码 + read_users + write_users + meta.write 绕过开关)。/fs/dirs 的 force_root 只对管理员开放。
2) /fs/get_direct_upload_info 对齐 Go FsGetDirectUploadInfo:file_name 单层相对路径校验、meta 写 ACL、存储挂载边界校验、Overwrite:false 已存在探测。
3) JWT 增加 pwd_ts 密码指纹(对齐 Go UserClaims.PwdTS):登录/SSO/WebAuthn 三处签发,authUserFromReq/getUserFromContext/checkAdminAuth 三处校验 —— 改密后已签发令牌立即失效(历史缺陷:改密不注销旧会话)。
4) 用户管理对齐 Go:create 禁止 admin/guest;update 禁止变更 role、禁止禁用管理员;delete 按角色保护 admin/guest(不再只硬编码 id===1)。
5) WebDAV Basic Auth 接入登录防爆破(失败计数 + 指数退避锁定,与 /api/auth/login 共用),OPTIONS 匿名放行(对齐 Go WebDAVAuth)。
6) 删除(按评审决定):bcrypt/无盐单层哈希兼容与登录时自动迁移(只保留 Go 双层哈希);user/create 自动生成随机密码并回显;TS 专有 /fs/upload/{create,part,complete} 端点(分片只保留官方 /fs/multipart/*)。
7) 顺带修复 5 个改动前就存在的 tsc 报错(db_cipher.test.ts 重复 DbCipher 导入、/form 的 getUploadSizeLimit 少传参、multipart/init 的 user 空值收窄)。
验证:tsc 0 error;server 179 / model+pkg 50 / store 12 / drivers 131 全绿;新增 security_hardening.test.ts 覆盖角色保护与改密失效。
…th-isolation

# Conflicts:
#	src/backend/server/webdav.ts
回应 PR #92 评审(Request Changes):
P1-1 /fs/multipart/chunk 此前只做 Content-Length 预检,省略该头即可绕过单片上限并把整片读入内存;现改用 readBodyWithinLimit(c, true),并删掉重复的预检分支。(评审提到的 /fs/upload/part 已在上一轮删除,分片只保留官方 /fs/multipart/*。)
P1-2 S3 PutObject 完全没有体积限制(MAX_UPLOAD/MAX_UPPART 都不生效,超大请求体整包读入内存);现接入共享限额实现,超限返回 S3 风格 EntityTooLarge(400)。为此把限额辅助函数抽到 pkg/upload-limit.ts,fs.ts 与 s3.ts 共用同一份。
P2-3 PutObject 不再返回伪造的 ETag(此前是 Date.now(),并非内容摘要,会误导依赖 ETag 做幂等/续传/校验的客户端)。真实 MD5 所需的算法 WebCrypto 不提供、纯 JS 实现要整包二次扫描、hash-wasm 的 WASM 在边缘运行时可能被禁用,故选择不返回 ETag(宁缺勿错),真实摘要留作后续跟进。
P2-4 /fs/other 签发的直传 URL 由客户端直连对象存储、不经过本站,MAX_UPLOAD/MAX_UPPART 覆盖不到——已在代码注释与 PR 描述中注明该边界。
P2-5 新增显式回归测试:normalizeSegments 越根钳制、getActualPath/isWithinUserRoot 边界、readBodyWithinLimit 的「省略 Content-Length 也按实际字节数拒绝」。
另修正 main 上既有失败测试:drivers/github_releases 的 gh_proxy 断言写成「代理值+完整链接」,而实现(与 Go strings.Replace 一致)是「替换 https://github.com 前缀」——纯测试改动。
验证:tsc 0 error;server 195 / model+pkg 81 / store 12 / drivers 151 全绿。

@pikachuren pikachuren left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[OUTDATED] 此评审已过时,请参阅下方增量评审(v5)。


🙏 感谢 @PIKACHUIM 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Sonnet 5 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🔄 增量评审:v3 → v4

本轮自上次评审(2026-10-09)起新增 4 个有效 commits:

  • 3ab8361f Merge origin/main(含 #93:EdgeOne 产物改为部署期生成)
  • 9bc81758 第三轮:meta ACL 跨协议打通、JWT 密码指纹、用户角色保护、删除 4 项历史 TS 专有功能
  • c6a5c226 Merge origin/main
  • 8d9c339b 上传限额覆盖 /fs/multipart/chunk 与 S3 PutObject,移除伪造 ETag

CI:Build and verify artifacts ✅ / Workers Builds (×2) ✅ — 全绿。


新增改动的问题(按 P0/P1/P2 分级):

P2(可选优化):

  • 💡 webdav.ts(webdavAuth 重构后) — 普通已登录用户访问 meta 密码保护路径时,metaPassword 强制为 ""(仅 guest 账号可将 Basic 密码透传为 meta 密码,与 Go 对齐)。这是正确的安全语义,但 WebDAV 客户端收到 403 时没有明确指引如何传递 meta 密码(S3 侧有 ?pwd= / x-meta-password 机制,WebDAV 侧完全没有)。建议在文档或 403 响应体中补一句说明,避免用户以为是账号权限问题反复尝试。

  • 💡 pkg/upload-limit.ts:36(新模块)— exceedsUploadLimit 在 !contentLength(无 Content-Length 头 或值为 0)时直接返回 null(跳过预检),随后 readBodyWithinLimit 再按实际字节数判定——逻辑是对的。但如果调用方只调用 exceedsUploadLimit 而不用 readBodyWithinLimit(当前没有,但日后可能出现),会误以为通过了检查。建议在函数 JSDoc 中明确标注:"本函数只做 Content-Length 预检,必须配合 readBodyWithinLimit 才能提供完整保护",防止被独立误用。

无新增 P0/P1 问题。


旧问题解决情况:

  • ✅ [P1] server/fs.ts:1655-1673(/fs/multipart/part → 现为 /multipart/chunk)Content-Length 可绕过上限 → 8d9c339b 已改用 readBodyWithinLimit(c, true) 并移除旧预检,修复到位;security_hardening.test.ts 补充了专项测试(含省略 Content-Length 场景)。

  • ✅ [P1] s3.ts:206(PutObject)完全没有大小限制 → 8d9c339b 抽出 pkg/upload-limit.ts 模块,fs.ts / s3.ts 共用;S3 侧改用标准 EntityTooLarge 错误码,不对称问题彻底消除。

  • ✅ [P2] s3.ts:206 ETag 用 Date.now().toString(16) 伪造内容摘要 → 8d9c339b 移除 ETag,响应体注释详细说明原因(边缘运行时无便宜的 MD5 通道),宁缺毋误,处理得当。

  • ✅ [P2] pkg/permission.ts normalizeSegments 缺越根钳制测试 → 8d9c339b 在 security_hardening.test.ts 补充了 normalizeSegments("/a/../../b") === "/b" 等 5 条越根用例,以及 getActualPath / isWithinUserRoot 的完整矩阵覆盖。


🎯 结论:✅ Approve — 4 条旧问题全部修复,第三轮 meta ACL / JWT 密码指纹大改动结构清晰、测试覆盖到位(372 项全绿)、CI 全绿;两条 P2 是文档级建议,不阻塞合并。

回应第二轮评审(v3→v4,Approve)的两条 P2:
P2-1 WebDAV 侧 meta 判定失败时只返回裸 403,用户容易误判成账号权限问题。现统一改为 metaForbidden():403 + 一句指引(WebDAV 无法携带目录密码,请改用 Web 界面或 /api/fs/* 带 password),措辞刻意不区分密码/read_users/write_users,不泄露具体限制类型。共 8 处(PROPFIND、GET/HEAD、PUT、MKCOL、DELETE、MOVE、COPY×2)统一使用。
P2-2 pkg/upload-limit.ts 的 exceedsUploadLimit 只做 Content-Length 预检,被独立调用会误判为「已通过检查」。现补充 JSDoc 明确它不是完整保护,并取消导出(模块内仅供 readBodyWithinLimit 使用),从接口层面消除误用。
新增回归测试:WebDAV 受 meta 保护路径返回带指引的 403(不触发存储层)。
验证:tsc 0 error;server 196 / model+pkg 81 / store 12 / drivers 151 全绿。

@pikachuren pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢 @PIKACHUIM 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Sonnet 5 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🔄 增量评审:v4 → v5

本轮新增 1 个 commit(e7c16a01),改动文件:src/backend/pkg/upload-limit.ts、src/backend/server/webdav.ts、src/backend/server/security_hardening.test.ts,CI 全绿。

新增改动的问题:无新增问题。

旧问题解决情况(v4 两条 P2):

  • ✅ webdav.ts WebDAV meta 403 缺客户端指引 → 新增 metaForbidden() 辅助函数,将所有 WebDAV meta 拦截点(PROPFIND/GET/PUT/MKCOL/DELETE/MOVE/COPY,共 8 处)统一换为含说明的响应体,明确告知调用方 WebDAV 无法携带 meta 密码的原因及正确操作路径(web UI / /api/fs/*);措辞刻意不区分具体限制类型(密码/read_users/write_users),避免信息泄露。配套在 security_hardening.test.ts 新增 65 行回归用例,断言响应体含 directory-level access control、meta password、web UI|api/fs,覆盖到位。

  • ✅ pkg/upload-limit.ts exceedsUploadLimit() 应标注"必须配合 readBodyWithinLimit 使用" → 函数改为非导出(去掉 export),同时补充 JSDoc 明确说明"仅做 Content-Length 预检,Content-Length 可被省略或伪造,缺失时直接放行,单独调用会重新引入绕过缺陷;刻意不导出以防止日后被独立误用"。实现与注释两层防护,处理比建议更彻底。

🎯 结论:✅ Approve — v4 两条 P2 全部修复,且均有测试覆盖,CI 全绿,无遗留阻塞项。

@pikachuren

Copy link
Copy Markdown
Collaborator

AI 代码评审(补充意见)

基于对本 PR 关键源码的审查(pkg/permission.ts、pkg/password.ts、pkg/utils.ts、pkg/http.ts、pkg/upload-limit.ts、server/auth.ts、server/middlewares.ts、server/s3.ts、internal/model/db.ts),补充几点意见,仅供参考:

结论:系统性的安全加固 PR,覆盖 token 吊销、路径越权、跨协议权限旁路(S3/WebDAV/dirs)、SSRF、上传限额等多个真实问题,代码质量和自证(历史缺陷均附实测复现方式)比较扎实,建议合并。

已确认做得好的地方

  • normalizeSegments 用栈折叠 .. 并钳制在根,getActualPath 改为先规范化再拼接,修复了 base_path 可被 ../ 越出的问题
  • S3 网关 GetObject/PutObject/DeleteObject 现在都做了 isWithinUserRoot + canWrite/canRemove + meta ACL 三层校验
  • SSRF 的 IPv6 判定改为解析成数值网段比较(覆盖 IPv4-mapped/6to4/NAT64),且解析失败按不安全处理(fail-closed),比原来的字符串前缀匹配可靠
  • 上传限额改为按实际读取字节数复核(readBodyWithinLimit),堵住了省略 Content-Length 绕过限额的问题
  • saveDb 持久化失败时正确回滚内存/请求缓存,避免报错但已生效的不一致
  • jti 吊销名单与密码指纹校验统一走 readPersistedSecret,不再只探测 KV,D1 等部署下也能生效

建议关注的风险

  1. 密码格式迁移可能导致现网存量账号登录失败:verifyUserStaticHash 不再支持无 salt 的历史单层哈希格式,且不会自动迁移。admin 账号有日志告警但不会自动修复,非 admin 普通用户完全没有兜底路径。建议在发布说明中明确给出升级步骤(设置 ADMIN_PASS 重新部署 → 登录改密 → 可选移除该变量),避免用户被意外锁定。
  2. revokeToken 是整体读数组→本地追加→整体写回,并发登出时存在覆盖竞态,极端情况下某次登出的吊销记录可能在其他实例/重启后丢失(本实例内存立即生效,跨实例最终一致,注释里已说明是可接受的权衡,仅提醒关注)。
  3. 本 PR 与 feat(cache): add file-tree and download-link caches (DB by default) #75(缓存)都大改了 server/fs.ts/server/raw.ts/server/s3.ts,建议协调合并顺序,后合并的一方需要重新确认两者叠加后鉴权与缓存读取的先后顺序未被破坏。

以上基于当前最新 commit 的代码审查,仅供参考,最终请以人工评审结论为准。

…th-isolation

冲突仅 src/backend/server/fs.ts 一处(来自 #75 文件树/链接缓存),解决口径:
1) /fs/upload/{create,part,complete}:保留本分支的删除(按评审决定,分片只保留官方 /fs/multipart/*)。main 在该端点内新增的 invalidatePaths 随之移除;其余缓存失效点全部保留,写操作的失效集中在 internal/op/storage.ts(6 处),不受影响。
2) /fs/get_direct_upload_info:main 新增的预失效代码引用 reqPath/file_name,而本分支已把校验后的目录/文件名归一化为 dirPath/dstPath(沿用 main 的内联拼接会指向未归一化路径)。改为 invalidatePaths([dirPath, dstPath]),语义等价且沿用已校验路径。

缓存隔离性核对(不削弱本分支的安全不变量):
- 缓存键为 (storage.id, 存储内相对路径),与用户无关,不缓存任何按用户计算的数据;
- 所有缓存读取都在鉴权/ACL 之后:/fs/* 的 meta 判定先于 listItems/getItem,WebDAV/S3 的 meta 判定同样先于存储调用,/d、/p 的链接缓存(raw.ts)在验签之后;
- meta.hide 过滤在 route 层逐项执行,缓存不改变「按请求过滤」的语义。

验证:tsc 0 error;server 197 / cache 24 / model 39 / pkg 42 / store 12 / drivers 151 全绿(共 465 项)。
@PIKACHUIM

Copy link
Copy Markdown
Member Author

忽略风险1、2、3,均不存在

@PIKACHUIM
PIKACHUIM merged commit 7a9b9f2 into main Oct 15, 2026
2 checks passed
wumingzhinu added a commit to wumingzhinu/OpenList-Worker that referenced this pull request Oct 18, 2026
- 冲突解决:采用上游 OpenListTeam#92 的 isRestrictedIpv4 + parseIpv6Groups 数值分组实现,
  删除本分支与其重复的 isSafeIpv4 / isSafeIpv6Literal if-链版本
- 回归测试 http_ssrf.test.ts 保留全部原有用例,并补上 OpenListTeam#92 新覆盖的
  6to4 (2002::/16) / NAT64 (64:ff9b::/96) 内嵌 IPv4 与组播 ff00::/8 用例
- http.ts 顺手按仓库 prettier 规范格式化 3 处(无语义变更)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants