对应审计 Issue #101 / #103:将本项目的安全架构与边界文档化,便于贡献者理解设计取舍。
| 版本 | 支持状态 |
|---|---|
| main 分支最新提交 | ✅ 支持 |
| 历史版本 / 旧 release | ❌ 不支持 |
请勿通过公开 Issue 报告安全漏洞。请使用 GitHub 私有安全通告(Security → Report a vulnerability), 或联系仓库维护者。收到报告后我们会尽快评估并修复。
BioQuest 是**纯前端静态应用 + Supabase(Auth / 数据库 / RLS)**架构:
- 前端静态托管(如 GitHub Pages),无自建业务后端、无服务端会话;
- 认证与数据全部直连 Supabase,写权限由服务端 RLS(Row Level Security)强制;
- 浏览器端数据使用 localStorage / sessionStorage / IndexedDB,不使用 Cookie。
本平台不设置、不读取任何 Cookie(全代码库无 document.cookie 调用):
- 认证凭据为 Supabase JWT,通过
Authorization请求头由 Supabase JS SDK 携带, 存于 localStorage(sb-*前缀),不会由浏览器自动附带; - 所有状态变更请求必须显式附带 token,跨站页面无法伪造用户请求 → 天然免疫经典 CSRF;
- 该结论的前提:凭据不进 Cookie。若未来引入 Cookie(如服务端渲染), 必须同步引入 CSRF 防护(如 SameSite=Lax + CSRF token)并更新本文档。
- 登录/注册:
js/supabase-client.js走 Supabase Auth(signInWithPassword/signUp), 密码经 HTTPS 传输、由 Supabase 服务端哈希存储,前端不落盘; - 会话持久化:Supabase SDK
persistSession: true,token 存 localStorage;autoRefreshToken: true自动续期;restoreSession()恢复会话(带 5 秒超时保护); - 登出清理:
logoutUser()/forceLogout()分层清除sb-*会话、游客会话、 管理员状态等全部敏感键,保留学习数据; - 管理员会话加固:admin 前端 token 存 sessionStorage,5 分钟 TTL 自动过期;
登录后二次校验
profiles.user_group === 'admin',非 admin 立即登出; 真正的写权限始终由服务端 RLS 强制,伪造前端状态无法提权。
采用 BYOK(用户自带 Key)模式,js/ai-key-store.js:
- Key 默认仅存页面内存(闭包单例,非枚举属性),刷新即失;
- 仅当用户显式勾选「会话内记住」才写 sessionStorage(关闭标签页即清除);
- 绝不写 localStorage;检测到旧版 localStorage 残留明文 Key 会自动搬入内存并擦除。
index.html 通过 meta CSP 收紧(js/ai-client.js 同样受 connect-src 约束):
script-src 'self'+ CDN 白名单,无unsafe-inline/unsafe-eval(内联脚本已外部化,onclick改为事件委托);object-src 'none'、base-uri 'self'、form-action 'self';- 外连域最小化:Supabase、jsDelivr/unpkg、题图(Wikimedia)、PhET、6 家 AI 服务商。
- 题库分片带 SHA-256 校验(
js/loader.js),CDN 回退数据防篡改; - 本地备份采用 WebCrypto AES-GCM 加密(
js/storage.js),密钥由密码 PBKDF 派生; - URL 参数经
sanitizeUrlParam清洗(控制字符 / HTML 定界符过滤 + 长度上限)。
- localStorage 持久会话的 XSS 风险面:token 存 localStorage 意味着一旦发生 XSS 即可窃取会话。 缓解手段:严格 CSP、全站输出转义、无第三方脚本注入面。这是「无 Cookie + CSRF 免疫」 与「token 不受 XSS 影响」之间的显式取舍;
- 游客模式密码哈希非加密安全:游客可选密码使用加盐 FNV-1a(非加密哈希), 仅防明文泄漏,不防御离线暴力破解(代码注释已声明)。正式账号无此问题;
- 纯前端权限门禁仅为 UX:admin 前端校验只是 UI 门禁,安全边界完全依赖 Supabase RLS。