外链地址形如 https://assets.talkincode.net/<hash>/<filename>,其中
hash是 22 位 base58 随机串(crypto.getRandomValues+ 拒绝采样,约 128 bit 熵), 由服务端生成,命中概率可以忽略;filename只影响下载名,不参与定位,可任意;路径穿越、控制字符、引号在写响应头前被清理;admin、api、health、favicon.ico等保留路径永远不可能成为 hash。
自定义 hash(?hash= / --hash)要求 16–64 位 [A-Za-z0-9_-],其余一律 400:
弱 hash 会把“不可猜”降级成“可猜”,所以下限卡在 16 位(约 96 bit)。
删除、换 hash、改过期都会让旧链接立刻失效(受缓存传播上限约束,见下)。
- 密钥形如
ak_<48 位 hex>,服务端只存 SHA-256,明文仅在创建响应里出现一次; - 校验时用
crypto.subtle.timingSafeEqual比较摘要,避免时序侧信道; - 可创建多把,按用途命名(
laptop、ci、agent-x);吊销立即生效,并记录last_used_at/last_used_ip/use_count方便排查; - 第一把密钥可以离线生成(
scripts/bootstrap-key.mjs),不依赖 dashboard 是否已配好 SSO。
- Access 应用只覆盖
assets.talkincode.net/admin(含/admin/api/*),策略为allow+ 邮箱白名单jamiesun.net@gmail.com,会话 24 小时; - 登录推荐 Email One-time PIN(
setup-access.sh会创建 OTP IdP)。新 Zero Trust 组织默认是 Cloudflare 账号登录,不会自动带上 OTP; - 退出:dashboard「退出登录」→
/cdn-cgi/access/logout(清 Access cookie); - Worker 不信任边缘结果,每个管理请求都会:
- 取
Cf-Access-Jwt-Assertion,用团队域发布(并缓存 10 分钟)的 JWKS 按kid验签(RS256); - 校验
aud必须等于本应用的 AUD,iss必须是https://<ACCESS_TEAM_DOMAIN>; - 校验
exp(缺失即拒绝)与nbf(30 秒容差); - 校验身份:邮箱必须在
ACCESS_ALLOWED_EMAILS里,或(可选)Access Service Token 的common_name(ACCESS_ALLOW_SERVICE_TOKENS=true)。
- 取
- 应用的 destination 是
assets.talkincode.net/admin(path 挂在域名上),所以公开下载路径 完全不在 Access 覆盖范围内,而/admin下的一切都在;再加一条deny-everyone-else策略,"没命中任何 allow" 明确为拒绝,不依赖默认行为; - service token 走
non_identity策略(它不携带用户身份),邮箱走allow; ACCESS_AUD/ACCESS_TEAM_DOMAIN为空时管理接口直接 503:宁可不可用,不可出错放行。- 因此把
/admin暴露到别的域名(例如 workers.dev)也不会泄露数据,只会得到 401。
两条独立防线,只有失败请求会被记账:
-
边缘限流(
wrangler.toml的[[ratelimits]])READ_LIMITER:每来源网络 1200 次/分钟MISS_LIMITER:猜错 40 次/分钟UPLOAD_LIMITER:每密钥 120 次/分钟
-
状态化封禁(
AbuseGuardDurable Object,每个来源网络一份)- 来源按 IPv4
/24、IPv6/64聚合,换 IP 无法绕过; - 统计窗口(默认 600 秒)内累计猜错 15 次 → 封禁;
- 封禁时长按
ABUSE_BAN_SCHEDULE逐次加倍:5 分钟 → 1 小时 → 1 天 → 7 天; - 每次触发的计数会归零,但 strike 累加;
ABUSE_STRIKE_DECAY_HOURS(默认 24 小时) 内没有新的违规才会清零,也就是说“惯犯”的封禁越来越长,偶尔踩线则第二天就恢复; - 被封禁的来源在公开读路径、失败路径与上传接口上直接 403 +
Retry-After(已存在的文件也不再返回,避免用 200/403 区分命中); - dashboard「安全防护」页可查看与解封(会同时清掉 D1 镜像与 DO 状态); 每行区分「生效中 / 已过期」,过期记录保留 7 天供回溯。
- 来源按 IPv4
猜错的对象包括:格式非法的 hash、不存在的 hash、错误的上传密钥。未被封禁时,正常下载不计入猜错,
但保留路径(/admin、/health、/favicon.ico、/robots.txt 等)的 404 也不记账——
只有真正的“猜”才会累积违规。一旦封禁,同一来源网络连已知外链也会 403。
部署后不久就有扫描器因为探测 /.env.prod、/.env.dev 被自动封禁,
后续一次违规直接按 7 天档处理。
由于 hash 有 128 bit 熵,穷举本身不可行;这套机制的目的是挡住资源滥用(探测、 刷量、密钥爆破)并留下可观察的痕迹,而不是“防止猜中”。
| 头 | 处理 |
|---|---|
Content-Disposition |
文件名过滤控制字符/引号/路径分隔符,按 RFC 5987 输出 filename*=UTF-8''…。HTML / XHTML / SVG / XML / JavaScript 强制 attachment,?inline=1 无效 |
Content-Type |
上传声明优先,其次按扩展名推断;未知类型为 application/octet-stream |
X-Content-Type-Options |
资产响应一律 nosniff |
Content-Security-Policy |
资产响应一律 sandbox(导航到该 URL 不执行脚本,也不继承 dashboard 的源) |
Cache-Control |
public, max-age=min(剩余有效期, CACHE_TTL_SECONDS),不给已删除内容二次缓存的机会 |
Access-Control-Allow-Origin |
默认 *(公开内容,可嵌入);需要收紧就改 CORS_ORIGIN |
X-Assets-State: metadata-only |
元数据在但字节已被清理时的 404 提示 |
上传密钥只接受 Authorization 或 X-Assets-Key,不接受 ?key=(URL 会进访问日志)。
管理接口的非 GET 请求若带 Sec-Fetch-Site: cross-site 或与本机不一致的 Origin,直接 403;
不带这两个头的 Service Token / CLI 不受影响。Access cookie 仍应保持 HttpOnly + SameSite=Lax。
服务不设置 Set-Cookie。用户上传的 HTML 不会在本源执行,dashboard 是静态文件且带 noindex。
内容按 URL 不可变,所以边缘缓存是安全的,但删除必须赢:
- 默认
CACHE_TTL_SECONDS=60:删除/过期后,其他机房最多再服务 60 秒旧副本; - 缓存键只有 hash。文件名和任意 query 不再各自占一条缓存,下载计数只在回源时增加;
- 变更时先在处理请求的机房用
caches.default.delete清除规范化键(并顺手清掉旧的文件名变体); - 配置
CF_PURGE_TOKEN后会调用 Cloudflare 精确 URL 清除接口,全网立即生效。
- 字节在 R2(key 与 hash 解耦,换 hash 不需要搬数据);
- 元数据、密钥摘要、审计记录在 D1;
audit_log记录谁在什么时候删了/改了哪个 hash; - 过期资产每小时由 cron 释放字节并写
purged_at;手动删除的资产保留TRASH_RETENTION_DAYS(默认 7 天)以便误删恢复。
- 单文件上限受 Workers 请求体限制(当前默认 100 MiB);没有
Content-Length的上传直接 411,不再在 isolate 里缓冲; Content-Length与实际 body 不符时以流错误结束,不会落库;- 用户内容与 dashboard 仍在同一主机名。
nosniff+sandbox+ 主动内容强制附件挡住了同源脚本,彻底隔离要等文件换到独立源。 - Access Service Token 一旦签发就长期有效(默认 1 年),请放 CI secret 里并定期轮换;
- 反滥用状态保存在 DO 内,
ABUSE_*改动需重新部署生效; - 若把
/admin以外的新管理路径加到 Worker,记得同步 Access 应用的 path 与handleAdminRequest的调用点。