From 430c6ef6d897a9e1e9d46e8305abcdb0945e43b8 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Tue, 27 Oct 2026 12:37:55 +0800 Subject: [PATCH] =?UTF-8?q?docs(memory):=20=E8=AE=B0=E5=BD=95=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E7=BB=93=E8=AE=BA=EF=BC=88CSP=20=E9=BB=98=E8=AE=A4=20?= =?UTF-8?q?enforce=E3=80=81=E5=88=86=E4=BA=AB=E5=8D=A1=E5=AD=97=E4=BD=93?= =?UTF-8?q?=E9=99=90=E5=88=B6=E3=80=81=E5=A4=8D=E6=9F=A5=E6=96=B0=E5=8F=91?= =?UTF-8?q?=E7=8E=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - topics/sec.md:两轮修复的结论、全局复查新发现(私聊 POST 越权、页面密码 next)、 CSP 白名单口径、字体只能用 glyf 轮廓这条硬限制,以及复现脚本的两种「假红」 - MEMORY.md:安全小节补跳转白名单、GET/POST 校验不对称、CSP/umami、注册防枚举 - 2026-10-27.md / topics/build.md:本地 WSL 部署与验收的具体跑法 --- .workbuddy/memory/2026-10-25.md | 77 ++++++++++++++++++ .workbuddy/memory/2026-10-26.md | 67 ++++++++++++++++ .workbuddy/memory/2026-10-27.md | 67 ++++++++++++++++ .workbuddy/memory/MEMORY.md | 20 ++++- .workbuddy/memory/topics/build.md | 28 +++++++ .workbuddy/memory/topics/sec.md | 127 ++++++++++++++++++++++++++++++ 6 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 .workbuddy/memory/2026-10-26.md create mode 100644 .workbuddy/memory/2026-10-27.md create mode 100644 .workbuddy/memory/topics/sec.md diff --git a/.workbuddy/memory/2026-10-25.md b/.workbuddy/memory/2026-10-25.md index 154ab81..75e2662 100644 --- a/.workbuddy/memory/2026-10-25.md +++ b/.workbuddy/memory/2026-10-25.md @@ -47,3 +47,80 @@ - 顺手修预览测试:`GrowthMoM` 原来写死 12.3,与 `monthly` 造的数据对不上, 而新句会同时印出两个月的条数 → 截图会显示「5620 比 4890 多了 12.3%」这种假证据。 改成从 `monthly[10]/monthly[9]` 算。 + +## 安全评估(AI 挖洞背景下的一次全量自检) + +报告:`outputs/security-audit-2026-10-25.md`,脚本在 `outputs/sec-audit-2026-10-25/`。 +结论:无 getshell / SQLi / 未授权读数据;但 2 个 P1 直击「匿名」承诺。 + +### P1-1 匿名提问可植入远程图片(去匿名化) +- `markdown.go` 的 `safeURL` 只校验协议,`` 能原样输出。 +- 唯一挡住它的是审核「广告_引流类」维度,而该维度**只在 strict 档生效** + (`censor/policy.go`:standard 只有 Abuse/Contraband,lax 为空)。 +- 实测:账号切 standard → `` 直接入库并渲染; + 切回 strict 才被拒。→ 修法必须在 `markdown.go` 层限制 img src 为站内/白名单,不能依赖档位。 +- 同时确认 8 种混淆(实体编码、`java\tscript:`、协议相对 `//`、反斜杠)都被审核识别。 + +### P1-2 访问密码可爆破 + 提问密码校验早于验证码 +- `UpdatePagePassword`/`UpdateAskPassword` 只有 `maxlen:30`,**无 minlen**,可设 1 位。 +- `page.go` 的 `New()` 第一件事就比对 AskPassword,验证码要到后面才校验 → + 实测「错误提问密码 + `deadbeef:999999`」返回「提问密码错误」而不是验证码错误 → + **猜密码完全不消耗验证码**。限额 30/min(ask)、10/min(page)。 + +### 其余P2/P3 +- 登录开放重定向:`to=/\evil.example` → `Location: /\evil.example`,浏览器按 WHATWG + 把 `\` 当 `/` → `https://evil.example/`。`path.Clean` 不处理反斜杠。 +- 注册接口枚举邮箱(返回「已注册」),与 forgot-password 的防枚举口径不一致。 +- 无 CSP;`X-XSS-Protection: 1` 过时;`/user` 无 `Cache-Control: no-store`。 +- 限流/自动封禁是进程内存态(多副本叠加、重启清零)。 +- 匿名投票身份 = `vote_fp` cookie,清 cookie 即换身份,仅 200/min/IP 兜底。 +- 登录 `to` 兜底是死代码:`path.Clean("/"+to)` 恒非空 → 登录后永远落 `/` 而非箱主页。 +- JSON 响应无 `Content-Type`(实测 text/plain);429 无 `Retry-After`。 + +### 无法从外网确认的一项 +`trust_proxy` 真实值。三种伪造头(XFF / X-Real-IP / EO-Client-IP)各 22 次都不被采信 +(好的方向:无法伪造绕限流),但这既可能是 `trust_proxy=false`(→ 按 CF 边缘 IP 聚合, +限流与封禁会误伤整片区域),也可能是 true 且在 CF 侧删了 EO-Client-IP。需登服务器看 app.ini。 + +### 已验证没事(别重复怀疑) +CSRF 绑 uid(无 token POST → 400);18 条 XSS 载荷全灭;跨箱题目/投票越权全 302; +页面密码对主页/整箱分享图/单题分享图/API 四个出口都生效;pixel/service/ShareCard 无 SSRF; +`exec cwebp` 不接用户输入;SMTP 默认校验证书;`RegenerateID` + `pwd_ver`;TLS1.0/1.1 拒绝; +无 CORS 头;测试站蜜罐已开(`/.git/HEAD`、`/.env` 返回假后台)。 + +### 测试卫生 +- 测试站账号 `tbunread1022181904`,脚本已把严格度恢复 strict、清空 page/ask 密码、 + 删除本轮 15 条测试提问(115–129)。下次复用前先确认这三项是干净的。 +- 隔离 venv 里装了 `requests`(`~/.workbuddy/binaries/python/envs/default`), + 比 urllib 顺手;`/api/captcha/generate` 限流 20/min,批量提交脚本必须带 429 重试。 + +## 安全评估(续):trust_proxy=true 引出的 P1-3 + +用户确认两套环境 `trust_proxy = true`。据此重做了 IP 采信那一块,发现比原来的推断严重: + +### P1-3 双 CDN 下权威头优先次序反了 +- `internal/shared/ip.go` 的 `ResolveClientIP` **无条件**先读 `CF-Connecting-IP`,再读 `EO-Client-IP`。 +- 前提「权威头只能由 CDN 写入」只对 Cloudflare 成立: + - CF:强制覆盖 `CF-Connecting-IP`,客户端伪造直接 **403**(生产/测试站实测一致)。 + - **EdgeOne:不剥离客户端传入的 `CF-Connecting-IP`**,`share.shiroko.one` 实测带该头返回 200 + (响应头 `Server: edgeone makers` / `EO-LOG-UUID`)。 +- 代码优先读 CF 那个头 → 走 EO 线路时客户端 IP 完全可控。后果:限流/IP 黑名单全失效, + 且 `autoban.Record` 可被用来**栽赃任意 IP**(填别人的 IP 连打 10 次敏感路径 → 对方被封)。 +- **今天打不通**:`box.shiroko.one` 境内递归(AliDNS DoH)只解析到 `cloudflare.182682.xyz` → 104.18.x/172.64.x, + 文档里写的境内 `eo.dnse4.com` 线路已不在;`box.tama.guru` 301 跳新域名也走 CF。差一条 DNS 记录。 +- 修法:权威头也套 `isTrustedProxy(remote)`(回环可用 cloudflared 本机场景),fail-closed; + EO 侧删 `CF-Connecting-IP`;日志同时记 `remote_addr` 与解析后 IP。 + +### 源站定位(外网已尽力,未找到) +- `box.shiroko.one` CNAME `cloudflare.182682.xyz`(第三方 CF SaaS 优选,zone 不在我们手里, + 其 Transform/WAF 规则无法自查)。 +- certspotter 枚举 `*.shiroko.one` 11 个、`*.337845818.best` 19 个子域,逐个解析全部落 + Cloudflare / 腾讯 EdgeOne / Vercel(216.198.79.x、64.29.17.x),**无一条直连记录**。 +- `status.shiroko.one`、`share.shiroko.one` → 43.174.246.43/247.43,用 box 的 SNI 访问回 418 Teapot + → 是 EO 边缘不是源站。 +- 结论:源站隐藏目前成功,但「找不到」≠「不可达」,仍需服务器侧 `security-selfcheck.sh local` 确认 + `listen = 127.0.0.1:5877`。 + +### 顺带修正 +上一版报告曾把「三种伪造头都不生效」推断为 `trust_proxy` 可能为 false —— 那是错的解读: +无论 true/false,限流键都落在我自己的真实 IP 上,外部不可分辨。已在报告第四节改正。 diff --git a/.workbuddy/memory/2026-10-26.md b/.workbuddy/memory/2026-10-26.md new file mode 100644 index 0000000..17c81d2 --- /dev/null +++ b/.workbuddy/memory/2026-10-26.md @@ -0,0 +1,67 @@ +# 2026-10-26 + +## 修复 P1-3:CDN 权威客户端头未校验来源 + +用户确认:生产/测试 `trust_proxy = true`,回源是 **cloudflared Tunnel 回本机**(对端 127.0.0.1); +境内 **EdgeOne 已取消**(实测期国内速度不理想),将来可能重新启用。 + +### 改动 +- `internal/shared/ip.go` + - 新增 `headerSourceTrusted(remote)`:对端是回环 或 命中 `trusted_proxies` 才允许采信权威头。 + - `ResolveClientIP` 把 `CF-Connecting-IP` / `EO-Client-IP` 读取整体收进该判断;公网对端忽略。 + - 公网对端带权威头 → `sync.Once` 一条 WARN(`remote_addr` + `header_ip`),覆盖「伪造」与 + 「CDN 直连回源忘登记网段」两种情形。 + - XFF / X-Real-IP 信任范围未变(仍只认 `trusted_proxies`,回环不隐含信任)。 +- `internal/shared/ip_test.go`:原 4 个用例按新契约调整;新增 + `RejectsForgedHeaderFromPublicPeer`(4 种伪造组合)`TrustedProxyHonorsCDNHeader` + `IPv6LoopbackHeader` `LoopbackDoesNotTrustXFF`;加 `setTrustedProxies` 助手(还原包级状态)。 +- `conf/app.sample.ini`(CRLF,用脚本按行块改,未破坏行尾)+ `internal/conf/static.go` 注释同步。 + +### 验证 +- 变异验证:把 `headerSourceTrusted(remote)` 临时改成 `true`(旧逻辑)→ 回归用例 3 个子项失败; + 还原后全绿。确认测试咬得住。 +- `go build -overlay dev/overlay.json ./...` 通过;排除 `internal/storage`(overlay 桩缺 + `fitsWithin`/`aspect` 等辅助函数,属既有问题)后全包测试通过。 + +### 环境坑(值得记) +- 本机 `go` 是 1.25.5,`go.mod` 要 1.27.3;`GOSUMDB=off` 会让工具链校验失败( + "verifying module: checksum database disabled by GOSUMDB=off"),`GOTOOLCHAIN=local` 又降级到 1.25.5。 + 解法:直接调缓存里的工具链 + `~/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.27.3.windows-amd64/bin/go.exe`,配 `GOTOOLCHAIN=local`。 +- Python 读 CRLF 文件要显式 `newline=''`,否则 universal newline 把 `\r\n` 归一成 `\n`, + `split('\r\n')` 只会得到 1 行(踩过一次,误判文件被写坏)。 + +### 待办 +P1-1(img src 白名单)、P1-2(密码 minlen + 验证码顺序)、P2-3(`to` 开放重定向)均未动。 + +## 修复 P1-1 / P1-2 + +### P1-1 外链图片(`internal/template/markdown.go`) +- 新增 `safeImageSrc()`:`` 只允许站内相对路径(无协议部分)或主机命中自有图片出口 + (`image_bucket_cdn_host` / `aliyun_bucket_cdn_host` / `.` / `external_url` 主机)。 +- 含反斜杠一律拒(`\` 在浏览器等价 `/`);`//` 先于「以 / 开头=站内」判断; + `url.Parse` + `Hostname()` 比对 → 后缀 / userinfo 把戏失效。 +- `img` 无合法 src 时**整段丢弃**(只删 src 会留 `` 变成破图)。 +- `a[href]` 仍走 `safeURL`(外链需点击,产品上要允许)。 +- 测试:`TestSafeImageSrc`(12 条拒绝)+ `TestMarkdownToHTML_ImageSource`(端到端); + 变异验证:`safeImageSrc` 直接 return true → 7 条拒绝用例失败。 + +### P1-2 密码(5 个文件) +- `minlen:6` + `form.PasswordMinLen`;handler 里 `rejectTooShortPassword()` 二次校验(rune 计数); + 前端 `minlength=6 maxlength=30` + 提示文案「6–30 位,留空则关闭」;i18n 新增 + `form.profile.passwordLen` / `question.errPasswordLocked`(4 语言)。 +- 校验顺序:抽 `askPasswordDenied()`,从 `New()` 首行挪到**两个分支验证码之后**(私聊 + 普通提问各一次)。 + 没把验证码提到最前——会让「箱已关闭」「需登录」变成验证码错误。 +- 失败锁定:新增 `shared.AttemptLimiter`(无后台 goroutine,惰性清理 + 键数上限); + `pagePasswordGuard(5, 10min, 10min)`,键 = 箱主ID + LimitKey(IP)。锁定期内不比对密码。 + +### 端到端验收脚本 +`outputs/sec-audit-2026-10-25/verify_p1_fixes.py`(`--only p1-1,p1-2a,p1-2b,p1-2c`)。 +**在未部署的旧版本上四项全 FAIL,已实测** → 证明检测有效;部署后应全 PASS。 + +踩到的两个判定坑(写脚本时记下来): +- 提问成功页是 **200**(不是 302),不能用状态码判断提交成功。 +- 受保护的箱是**在 `/_/` 内联渲染密码表单(200)**,不是跳转 `/password`; + 判断「有没有密码闸门」只能看页面里有没有 `action="/_//password"`。 +- 箱主页与 `/api/v1/user//questions` 都**不暴露题目 id**(无后台), + 要定位刚建的提问只能扫 `/_//`(脚本里 `question_ids_with_marker`)。 diff --git a/.workbuddy/memory/2026-10-27.md b/.workbuddy/memory/2026-10-27.md new file mode 100644 index 0000000..ff90adf --- /dev/null +++ b/.workbuddy/memory/2026-10-27.md @@ -0,0 +1,67 @@ +# 2026-10-27 + +## 在 WSL Debian 上把 TamaBox 跑起来,做 P1 修复的端到端复验 + +用户新搭了 WSL2 Debian,给了 `data/data.txt`(测试环境的 PG / Redis / OSS 凭据),要求「用它部署测试一下」。 + +**环境**(详细踩坑见 `topics/build.md` 的「本地部署」小节): +- `wsl.exe -d Debian -u root` 可免密进 root;apt 装了 `redis-server / python3 / python3-requests / + gcc / libwebp-dev`(有 libwebp 后 `s3.go` 正常编译,不用 s3stub overlay)。 +- Go:`/opt/go` 装 1.27.3,但 `~/.bashrc` 里 `GOROOT=/usr/local/go`(1.26.3)→ 必须 `env -u GOROOT`。 +- PG:新建 role `tamabox`(CREATEDB)+ 库 `tamabox`、`nekobox`(schema `nekobox`)。 +- 服务:`/etc/systemd/system/tamabox-test.service`,源码在 `~/tb/src`,二进制 `~/tb/tamabox`, + 日志 `~/tb/server.log`,端口 5877。配置 `~/tb/src/conf/app.ini`(`production=true`、 + `external_url=http://127.0.0.1:5877`、`captcha_type=math`、`enable_text_censor=false`、`trust_proxy=true`)。 + +**验收结果(全 PASS)**: +- P1-1 外链图片剥离 / 白名单图片保留 / 实体与反斜杠变体; +- P1-2a 4 位密码被拒、6 位生效;P1-2b 验证码先于密码;P1-2c 6 次失败后正确密码仍被锁; +- P1-3 A/B:回环对端 + `CF-Connecting-IP: 1.2.3.4` → 命中黑名单 302;Windows 侧(非回环)同样头 → 忽略并 WARN; +- 回归:18 条 XSS 载荷无可执行/无外联、CSRF 无 token 400、越权全 302; +- 单测:`internal/{template,shared,form,conf,db,route}` + `route/{question,vote}` 全绿。 + +**这次揪出的新问题(已修)**:P1-1 的白名单把配置串整串当主机名,而真实配置是 +`image_bucket_cdn_host = "r2.337845818.best/tamabox"`(host/path)→ 自家 R2 图片被一起剥掉。 +修法:`normalizeHostLike()` 归一化(补协议 → url.Parse → Hostname),endpoint 主机自身也入白名单 +(R2 path-style 链接是 `//…`);新增 `TestSafeImageSrc_RealConfiguredHosts`, +变异验证(退回旧逻辑 → 4 条放行用例变红)。**教训:白名单类单测必须照抄生产配置的写法,不能自己编一个好写的形态。** + +**P2-3 从推断升级为实测**:`/login?to=/\evil.example` 登录后 `Location: /\evil.example`, +按 WHATWG 解析 = `http://evil.example/`;`\\evil.example` 同样外跳(`//`、`///` 已被规整)。 +同一载荷在正文渲染里是安全的(`` 被剥成 ``)。修复未做,等用户指示。 + +**产物**: +- 报告新增「六、本地部署实测」章节(`outputs/security-audit-2026-10-25.md`); +- 脚本新增 `verify_p1_allow.py`(正向放行)、`local_seed.py`(本地注册审计账号), + `audit.py` 改为 `TB_BASE/TB_EMAIL/TB_PWD/TB_DOMAIN` 可覆盖 + 验证码 429 退避。 + +**待办**:改动仍未 commit / push;P2-3、P2-4、P2-5、P2-6、P2-7 未动。 + +--- + +## 第二轮:修完 P2/P3 + 全局复查 + +用户:「把你发现的问题修了,然后全局检查一遍有无其他问题」。 + +**已修(P2-3~P2-8、P3-9~P3-15)**:都写进 `topics/sec.md` 的「第二轮修复」。要点: +跳转白名单收敛到 `shared.SafeRedirectTarget`(`url.Parse` 会解 `%0d%0a`,必须逐 rune 查 `u.Path`); +注册三条失败分支共用 `registerSucceeded`;CSP 三档 + umami 的 `mami.shiroko.one` 必须入 `script-src`; +`no-store`;`securityHeaders` 前移;封禁落库 `ban_records`;常量时间比较;页码上限;429 统一出口。 + +**全局复查新发现 3 个**(详见 `topics/sec.md`): +1. 🔴 `chat_handler.SendMessage` 的 POST 缺 `conv.OwnerID != pageUser.ID` —— 被拉黑者换 domain 即可继续骚扰 + + 触发邮件 + 绕过屏蔽词/日限额。已修,3/3 PASS,并做变异验证(去掉校验后对应用例变红)。 +2. 🟡 页面密码 `next` 的反斜杠外跳(与 P2-3 同源)。 +3. 🟡 分享卡无字体时每请求烧数秒 CPU(公开端点)→ 失败结果缓存 1 分钟,实测 3.44s → 0.53s。 + +**验收**:`verify_p1_fixes.py` 4/4、`verify_round2.py` 19/19、`verify_page_next.py` 6/6、 +`verify_chat_owner.py` 3/3;单测全绿;全项目编译通过。测试数据已清(库里只剩 `tb-local@example.com`)。 + +**报告**:`outputs/security-audit-2026-10-25.md` 新增「七、第二轮修复与全局复查」。 + +**再待办**:仍未 commit / push(26 改 + 9 新);CSP 是否切 `enforce`;生产机补 glyf 轮廓字体。 + +**收尾复跑发现两个「假红」**(已写进 `topics/sec.md`):① 不给 `TB_BASE` 时脚本打的是远端 +box-dev-api,没部署修复所以 4/4 FAIL;② 页面密码入口限流 10 次/分钟 + p1-2c 的 10 分钟锁定 +会让 `verify_page_next.py` 后两项 429。顺手把该脚本判据从「Location 不含 evil」改成精确比对 +`/_/`、每条载荷开新会话、429 单独报错。四套最终:4/4、19/19、6/6、3/3。库已清回只剩 `tb-local`。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index e930a09..ce4f3e9 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -3,7 +3,7 @@ 明细已拆分到 `topics/*.md`(按需 Read 全文): `coop` 协作 / `build` 构建测试 / `embed` 行尾·模板·embed / `review` 审核处置举报 / `gorm` GORM / `style` 前端样式 / `theme` 主题 / `builder` 主题生成器 / `share` 分享卡 / -`honeypot` 蜜罐多家族伪装 / `stats` 全站数据页 / `misc` 其它约定。 +`honeypot` 蜜罐多家族伪装 / `stats` 全站数据页 / `sec` 安全评估·客户端 IP 信任 / `misc` 其它约定。 ## 协作 @@ -18,7 +18,10 @@ `export GOPROXY=https://goproxy.cn,direct GOSUMDB=off GOFLAGS=-mod=mod`。 - DB 测试:`DB_TYPE=postgres DB_USER=postgres DB_PASSWORD=15207786347hXPm DB_HOST=127.0.0.1 DB_PORT=5432`(DSN 不带库名)。 - store 方法内用 `NewXStore(db.DB)`,别引用包级单例。 -- 本沙箱起不了 `TamaBox web`(session 固定 redis,6379 未开)→ HTTP 端到端只能部署后做。 +- ~~本沙箱起不了 `TamaBox web`~~ **2026-10-27 起可在 WSL2 Debian 上真跑**(PG+Redis+systemd,端口 5877): + 详见 topics/build.md「本地部署」小节。要点:`env -u GOROOT`(bashrc 指着 1.26.3)、 + `[recaptcha] captcha_type = math`、Windows 侧 127.0.0.1:5877 被占要用 WSL eth0 IP。 + HTTP 端到端与 P1-3 的双路径 A/B 都能在这里做(回环=可信、跨主机=不可信)。 - python urllib 不稳定(用 curl);python 别用 `/tmp`;PIL 在 `~/.workbuddy/binaries/python/envs/default`。 ## 行尾 / 模板 / embed(topics/embed.md) @@ -59,6 +62,19 @@ - 主题生成器 `dev/theme-builder/`:非破坏性图片处理、`layerSize`/`unitGeom` 必须共用同一函数、 `hideDeco` 会把 @media 10 槽全写 none(窄屏素材消失的头号来源)。 +## 安全(topics/sec.md) + +- 权威头 `CF-Connecting-IP` / `EO-Client-IP` 只在 TCP 对端可信(回环 或 `trusted_proxies` 命中) + 才采信,公网对端一律忽略;唯一读取点 `internal/shared/ip.go`,别在别处另读 header。 +- 生产回源 = cloudflared Tunnel 回本机(对端 127.0.0.1)→ 零配置;`trust_proxy` 两套环境都 true。 +- **EdgeOne 已取消**(境内速度差)。重新启用前必须把 EO 回源网段写进 `trusted_proxies`, + 否则 EO 线退化成按边缘 IP 聚合(限流互挤、自动封禁误伤整片)。`app.sample.ini` 已注明。 +- 跳转参数只走 `shared.SafeRedirectTarget`;`url.Parse` 会解 `%0d%0a`,必须逐 rune 查 `u.Path`。 +- **同一 handler 里 GET 有所有权校验 ≠ POST 有**(私聊 `SendMessage` 曾漏,可跨箱骚扰)。逐分支核对。 +- CSP 三档默认 `report`;`script-src` 必须含 `mami.shiroko.one`(umami 是动态注入,grep 抓不到)。 +- 注册防枚举:邮箱重复/停用与正常注册共用同一个成功出口;代价是忘密只能走找回。 +- 报告 `outputs/security-audit-2026-10-25.md` + 复现脚本 `outputs/sec-audit-2026-10-25/`。 + ## 其它约定(topics/misc.md) - 校验用 `valid:"required;maxlen:N"` 标签。 diff --git a/.workbuddy/memory/topics/build.md b/.workbuddy/memory/topics/build.md index 10c5c09..0eb223b 100644 --- a/.workbuddy/memory/topics/build.md +++ b/.workbuddy/memory/topics/build.md @@ -32,3 +32,31 @@ (`go version` 显示的还是 shell 包装器的版本,容易误判成「版本没变」)。 解法:直接用缓存里的 toolchain 并禁用自动切换—— `PATH="$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.27.3.windows-amd64/bin:$PATH" GOTOOLCHAIN=local go test ./...` + +## 本地部署(WSL2 Debian,2026-10-27 打通) + +用户本机有 WSL2 `Debian`(可 `wsl.exe -d Debian -u root` 免密进 root)。在这上面跑通了完整服务, +从此 HTTP 端到端不必「部署到测试站才能做」。要点: + +- **GOROOT 是个坑**:`~/.bashrc` 里 `export GOROOT=/usr/local/go`(1.26.3)。用 `/opt/go/bin/go` + (1.27.3,从 dl.google.com 下的 tar 解到 /opt)时必须 `env -u GOROOT`,否则 + `compile: version "go1.26.3" does not match go tool version "go1.27.3"`。 + 另:GOCACHE 被旧版本编译过也会报同样的错 → 换独立 `GOCACHE=~/.cache/go-build-tb`。 +- **依赖**:`apt-get install redis-server python3 python3-requests gcc libwebp-dev pkg-config` + (有 libwebp 后 `internal/storage/s3.go` 正常编译,不再需要 s3stub overlay)。 + session 与 cache 都硬依赖 redis,6379 不通就起不来;`systemctl start redis-server` 即可(WSL 有 systemd)。 +- **库**:DSN 不带库名 → 连的是与用户名同名的库。测试还要 `CREATEDB` 权限(`NewTestDB` 自建临时库)。 + `CREATE ROLE tamabox LOGIN PASSWORD '...' CREATEDB; CREATE DATABASE tamabox OWNER tamabox; + CREATE DATABASE nekobox OWNER tamabox; CREATE SCHEMA nekobox AUTHORIZATION tamabox;` +- **配置坑**:`data/data.txt` 里的 `[app] title`、`[server] xsrf_expire`、`[captcha] provider`、 + `[rate_limit] *` 全是当前版本不认识的键(启动 WARN 后忽略)。验证码要关外网校验得写 + `[recaptcha] captcha_type = math`。`external_url` 用 http → session cookie 不带 Secure,本地能登录。 +- **进程托管**:`/etc/systemd/system/tamabox-test.service`(User=tamakyi、WorkingDirectory=源码目录、 + ExecStart=<二进制> web、日志 append 到文件)。`systemctl restart` 必须 root,普通用户报 Access denied。 +- **访问路径**:Windows 侧 `http://127.0.0.1:5877` 被本机别的进程占用(返回 502),要用 + `http://:5877`。这个差异正好白送一套 P1-3 的 A/B: + WSL 内 `curl 127.0.0.1` = 回环对端(模拟 cloudflared 回源),Windows 侧访问 = 非回环对端(模拟公网直连)。 +- **抓 IP 伪造**:临时配 `ip_blacklist = 1.2.3.4` + `ip_blacklist_redirect_url`,带头 curl 看有没有 302, + 比读日志直观;配合日志里的 `remote_addr` / `header_ip` 双重取证。 +- **限流会绊脚本**:`/api/captcha/generate` 按 IP 限流,批量跑用例会 429, + `audit.get_captcha` 已加退避重试。SMTP 密码不对只会打日志(`526 Authentication failure`),不影响用例。 diff --git a/.workbuddy/memory/topics/sec.md b/.workbuddy/memory/topics/sec.md new file mode 100644 index 0000000..0b15c2f --- /dev/null +++ b/.workbuddy/memory/topics/sec.md @@ -0,0 +1,127 @@ +# 安全评估与客户端 IP 信任 + +报告:`outputs/security-audit-2026-10-25.md`;复现脚本 `outputs/sec-audit-2026-10-25/`(可重跑)。 + +## 客户端 IP 采信(P1-3,2026-10-26 已修) + +- 唯一读取点:`internal/shared/ip.go` 的 `ResolveClientIP`,23 处调用共用。 + **不要在别处另读 `CF-Connecting-IP` / `EO-Client-IP`**,否则绕过校验。 +- 规则:权威头只在 `headerSourceTrusted(remote)` 为真时采信 —— + 对端是回环地址,或命中 `trusted_proxies`。公网对端带来的权威头一律忽略,回退 remote。 +- XFF / X-Real-IP 仍只认 `trusted_proxies`;回环**不**隐含信任这两个头。 +- 公网对端却带权威头 → 进程内一条 WARN(`remote_addr` + `header_ip`)。 + 出现这条日志 = 要么有人在伪造,要么 CDN 直连回源但没登记网段。 +- 不能靠「哪个头存在」推断请求走了哪家 CDN:客户端自带任意头都能反向利用。 + +## 部署形态 + +- 生产/测试都是 `trust_proxy = true`,回源 = **cloudflared Tunnel 回本机**(对端 127.0.0.1), + 因此修复后零配置变更。 +- **EdgeOne 已取消**(境内访问速度不理想),当前只有 Cloudflare 一条线。 + 重新启用前必须把 EO 回源网段写进 `[security] trusted_proxies`,否则 EO 线退化成按边缘 IP 聚合 + (限流互挤、自动封禁误伤整片)。注释已写进 `conf/app.sample.ini`。 +- `box.shiroko.one` CNAME 到第三方 `cloudflare.182682.xyz`(CF SaaS 优选),该 zone 的 + Transform/WAF 不归我们管 —— 别把安全前提建立在那边的规则上。 +- 源站隐藏:外网尽力排查(A 记录 / certspotter 51 个子域 / MX / 候选 IP)未找到直连入口; + 仍须服务器侧 `dev/security-selfcheck.sh local` 确认 `listen = 127.0.0.1:5877`。 + +## 实测确认「防护到位」(别重复怀疑) + +CSRF 绑 uid(无 token POST → 400);18 条 XSS 载荷全灭(含实体编码、`java\tscript:`、 +协议相对 `//`、反斜杠);跨箱题目/投票越权全 302;页面密码对主页/整箱分享图/单题分享图/API +四个出口都生效;pixel/service/ShareCard 无 SSRF;`exec cwebp` 不接用户输入;SMTP 默认校验证书; +`RegenerateID` + `pwd_ver`;TLS1.0/1.1 拒绝;无 CORS 头;测试站蜜罐已开。 + +## 已修(2026-10-26) + +- **P1-1** 外链 ``:`markdown.go` 的 `safeImageSrc` 只放行站内相对路径与自有图片 CDN 主机 + (配置来源 `image_bucket_cdn_host` / `aliyun_bucket_cdn_host` / `.` / `external_url`); + `a[href]` 仍允许外站。无合法 src 的 img 整段丢弃。 +- **P1-2** 密码:page/ask 密码 `minlen:6`(`form.PasswordMinLen`)+ handler 二次校验; + 提问密码比对挪到验证码之后(`askPasswordDenied`,私聊与普通提问各调一次); + 页面密码失败锁定 `shared.AttemptLimiter(5, 10min, 10min)`,键 = 箱主ID + LimitKey(IP)。 +- **P1-3** 权威头:见上「客户端 IP 采信」。 +- 部署后验收:`python outputs/sec-audit-2026-10-25/verify_p1_fixes.py`,四项应全 PASS + (旧版本上实测四项全 FAIL)。跑完会自动还原严格度/密码/测试提问。 + +## 第二轮修复(2026-10-27,P2/P3 全清) + +- **跳转白名单收敛为唯一实现 `shared.SafeRedirectTarget`**(`internal/shared/redirect.go`), + 登录 `to` 与 `/_//password` 的 `next` 共用。规则:拒绝空/含 `\`、控制字符、空格; + 必须单斜杠开头、非 `//`;`url.Parse` 后不得有 Scheme/Host/User/Opaque;遍历 `u.Path` 的 rune + 拒 `\`、`<0x20`、`0x7f`(**关键**:`url.Parse` 会把 `%0d%0a` 解码成 CRLF,只看原始串抓不住); + 最后 `path.Clean` 再验一次前缀。测试在 `redirect_test.go`(25 条定向 + 全量不变式)。 +- **注册防枚举**:邮箱重复/已停用与全新注册走**同一个** `registerSucceeded(ctx)` 出口 + (302 → `/login`、无 alert),日志里才留痕。域名占用仍明文提示(不然用户没法选)。 + 代价:忘密用户只能走「找回密码」。 +- **CSP**:`internal/route/csp.go` + `[security] csp = off|report|enforce`(默认 report)、 + `csp_report_uri`。`script-src` 白名单必须含 **`https://mami.shiroko.one`**—— + `templates/base/header.html` 用 `document.createElement("script")` 动态注 umami, + 静态 grep `