项目:html5-examples | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本项目是一个"展示半可信代码"的平台:它的核心业务——把 750 个含 HTML/CSS/JS 的示例渲染到用户浏览器——本身就是一次大规模的"执行第三方代码"。因此它的安全设计必须回答三个层次的问题:示例代码如何与站点隔离(浏览器沙箱层)、用户与用户、用户与数据如何隔离(认证授权层)、后端自身暴露面如何收敛(服务加固层)。源码给出的答案可以概括为一句话:浏览器沙箱 + 半可信内容假设挡第一层,默认拒绝的全局守卫漏斗挡第二层,启动安检与参数白名单挡第三层;同时源码对每一层的残余风险都有坦诚的注释记录,本文逐条复述并给出评估。
三个层次之外还有一个横切事实需要先说明:本项目当前没有任何内容级 CSP(index.html 无 CSP meta、后端未下发 CSP 头、srcdoc 文档亦无),这既是当前的已知缺口,也是理解"半可信"假设分量的前提——一旦该假设被打破,纵深防御的层数会比表面看起来少一层。这一缺口与"未来开放用户提交"的前置条件在 §6 展开讨论。
沙箱的全部逻辑集中在 src/utils/sandbox.ts(逐函数走读见《07-模块详解》§1.1)。决策链是:needsSameOrigin(example) 用正则在示例三段源码里探测同源资源引用(/shared-assets/、/static-sites/、../assets/、./assets/、/images/ 等模式)→ 命中则 getSandboxAttributes(true) 追加 allow-same-origin → DeviceFrame.vue 把属性数组 join 后写入 <iframe :sandbox>。默认组合始终是 allow-scripts allow-forms allow-modals allow-popups。
sandbox.ts 的 getSandboxAttributes 上方有一段罕见坦诚的安全注释,原文要点:allow-scripts + allow-same-origin 组合理论上存在沙箱逃逸风险——iframe 内脚本可访问父窗口 cookie/localStorage,甚至移除自身 sandbox 属性。技术上这条注释完全准确:HTML 规范明确警告,同屏授予这两个 flag 等于沙箱失效——同源脚本拿到 parent.document 后可以为所欲为,包括读取宿主 localStorage 里的登录会话(html5-examples-auth-session 键含 access/refresh token)、以宿主身份发起同源请求、以及自我移除 sandbox 属性后重开页面。
needsSameOrigin 让 allow-same-origin 按需授予——不引用同源资源的绝大多数示例(纯 CSS 动画、Canvas 逻辑等)运行在真正隔离的 opaque origin 里,攻击面被压缩到引用共享素材的子集。buildSrcDoc 对文档 <title> 做五字符实体转义、对 JS 做 </script 提前闭合转义,堵住"示例内容意外破坏文档结构"这一类低级注入;srcdoc 文档本身与宿主同源但内容受控拼装。源码注释同时给出了后续加固选项:1) 在 srcdoc 中添加 CSP meta 标签限制脚本能力;2) 将示例资源迁移到独立子域(如 sandbox.example.com),使 allow-same-origin 授予的是"资源子域"而非主站 origin;3) 改用 postMessage 通信替代同源访问。三者中独立子域收益最高(把半可信代码彻底赶出主站 origin,localStorage/cookie 自然隔离),代价是部署拓扑变更与 <base href> 策略重设计;CSP meta 成本最低(一行模板改动),能限制 script-src/connect-src,但挡不住"同源读 DOM"本身;postMessage 路线改动最大。当前评估:在"内容仅维护者可写"的前提下风险可接受,但任何"开放用户提交示例"的需求都必须视为假设被打破,先落地加固再开放(详见 §6)。
另一个值得记录的细节:DeviceFrame.vue 在组件卸载与刷新时把 iframeRef.value.srcdoc 置空——这既是体验处理(停掉示例音频),也算一种资源层卫生(终止失控循环/定时器的执行环境)。iframe 无 allow-top-navigation flag,示例无法主动导航宿主页面,这一点是 sandbox 属性组合天然提供的。
后端(server/src/modules/auth/)签发的凭证是一对 token:access token(JWT,payload { sub, username, role },默认 TTL 由 JWT_ACCESS_EXPIRES_IN 决定,未配置时 auth.service.ts 兜底 604800 秒即 7 天;.env.example 建议 7200 秒)+ refresh token(非 JWT,64 字节 crypto.randomBytes hex,落 refresh_tokens 表,TTL 由 JWT_REFRESH_EXPIRES_IN 控制,建议 7 天)。登录/注册/刷新三个入口同时:HTTP 响应体返回 token 对 + 设置 auth-token httpOnly cookie(secure 仅生产、sameSite=lax、path=/,maxAge 同 access TTL)。前端 authStore.ts 把 token 对持久化到 localStorage(html5-examples-auth-session),隐私模式静默降级内存会话。
双通道设计(Bearer 优先、cookie 兜底)在请求侧的落地是:client.ts 的 doRequest 在 authTokenProvider() 返回非空时附加 Authorization: Bearer;后端 jwt.strategy.ts 的提取器同样是 Bearer 头优先、cookie auth-token 兜底。cookie 的存在使"纯 Bearer 前端"与"仅 cookie 的历史会话"都能工作。
前端 authStore.ts(255 行)实现了完整的提前刷新状态机:REFRESH_AHEAD_SECONDS = 300——applyAuthResult 与 ensureHydrated 两处都会调 scheduleTokenRefresh(tokenExpiresAt),以模块级 refreshTimer(刻意不放 Pinia state,避免非响应式数据进响应式系统)在过期前 5 分钟触发 refreshAuth();initialize() 时若发现"距过期 ≤ 300s"则直接同步刷新。刷新成功走 applyAuthResult(写状态 + persistSession + 重挂定时器);失败清空会话并 clearFavorites()——换号隔离的三个触发点(applyAuthResult 检测 userId 变化、refreshAuth 失败、logout)都汇聚到这一个函数。
auth.service.ts 的 refreshAccessToken 是安全设计里最讲究的一段:refresh token 一次性使用——校验通过后立即把旧 token 打上 revokedAt 并签发新对;若发现被使用的 token 已经是吊销态,直接判定为可能泄露,吊销该用户全部 refresh token 强制重新登录。配套还有 changePassword 成功后全量吊销、logoutUser 全量吊销、cleanupExpiredRefreshTokens 过期清理。这套"旋转 + 重用检测"是 refresh token 方案的标准最佳实践,本项目的实现完整。
captchaService.verifyCaptcha,challenge 落 captcha_challenges 表(有 expiresAt)。RateLimitGuard(5, 60_000)——进程内 Map 按 IP 计数,超限返回 429 + retryAfter。局限:单实例内存态(重启清零、多实例不共享),仅覆盖认证入口而非全站(见 §7 隐患清单)。后端授权是默认拒绝、显式放行的三级漏斗:
| 层级 | 机制 | 源码位置 |
|---|---|---|
| L0 公开 | @Public() 装饰器 + 全局 JwtAuthGuard 读 metadata 短路放行 | decorators/public.decorator.ts、guards/jwt-auth.guard.ts |
| L1 已登录 | 全局守卫解析 JWT 填充 req.user;严格端点叠加 RequireAuthGuard(user 缺失抛 401) | guards/require-auth.guard.ts、knowledge 全域 |
| L2 管理员 | 写操作叠加 AdminGuard(未登录 401、非 admin 403) | guards/admin.guard.ts,topics/categories/examples/static-sites/ui-elements/canvas 的写端点 |
全局守卫的宽容模式是理解整个漏斗的关键:JwtAuthGuard.handleRequest 特判——请求完全不带凭证(无 Authorization 头且 cookie 中无 auth-token)时放行且 req.user 为 undefined,由业务方法自行决定;带了凭证但验证失败才抛 401。这使"公共浏览 + 可选认证"无需给每个公开接口打 @Public()(当然项目在目录类端点上还是显式打了),而 knowledge 域则用 RequireAuthGuard 把宽容扭回严格。角色模型本身极简:users.role 只有 user 与 admin 两值,JWT payload 携带 role,AdminGuard 字符串比对——没有权限表、没有资源级 ACL,符合"内容站"的定位。
前端侧的联动是:路由守卫对 meta.requiresAuth 页面 await initialize() 后判断 isLoggedIn;任意 API 401 触发全局 authFailureHandler(清会话 + 清收藏缓存 + window.location.href = /login?redirect=…),403 不触发登出(会话有效,仅权限不足)——401/403 的语义区分贯穿两侧(机制详见《05-核心流程》流程三)。
ValidationPipe{ whitelist: true, forbidNonWhitelisted: true, transform: true }——请求体里 DTO 未声明的字段直接 400 并返回字段级 message 数组,同时 class-validator 完成类型与长度校验。这是对"Mass Assignment(越权批量赋值)"的直接防御:攻击者往 PATCH /api/topics 里塞 role 之类的字段会被拒绝。pagination.util.ts 的 clampPage/clampLimit 把非法值静默回退默认并钳上限(100/放宽 1000)——注入式大 limit 拖库被结构性地排除。:param 绑定 + Repository API),源码中未发现字符串拼接 SQL;唯一例外的 Like(%${search}%) 走的是 Repository 参数化 Like,非注入面。run 模块的 SQL 查询另有只读前缀白名单兜底(§5)。src/utils/sanitize.ts 提供 DOMPurify 消毒(显式标签白名单,覆盖常见展示标签),供富文本/描述类渲染使用;沙箱侧由 escapeHtml(title)与 </script 转义兜底文档结构完整性。encodeURIComponent;后端按 id 精确查库,无文件路径拼接(run 模块的临时目录由 mkdtemp 生成,文件名固定 main.py 等)。| 暴露面 | 内容 | 评估 |
|---|---|---|
/api/auth/* | 认证全家桶(公开写端点有限流 + 验证码) | 主攻击面,已加固 |
/api/{topics,categories,examples,static-sites,ui-elements,canvas-examples,visual-demos,chart-examples} | 全部 GET [公开],写 [Admin] | 只读暴露内容数据,examples 等冻结模块仍在响应(信息暴露面大于必要,见《13-技术债务》第 2 条) |
/api/knowledge/*(7 子域,约 70 端点) | 强制登录(RequireAuthGuard) | 前端无消费方但仍在线——登录用户可触及一整套文档/片段/画板 API |
/api/knowledge/oss/* | 签名/上传,需登录 | OSS 凭证不出后端,签发的是临时直传签名 |
/api/run/* | 任意 python/java 代码执行 + 跨库只读 SQL | 最高危暴露面,靠 RUN_ADMIN_TOKEN 头部令牌守卫;未配置即整通道禁用(fail-closed) |
/admin | 管理后台静态页(ServeStatic) | 无鉴权头的静态资源,依赖运维层 IP 限制;真实操作走带鉴权的 /api |
| CORS | CORS_ORIGIN 白名单 + credentials | 默认仅 localhost:5555/5560 |
run 模块值得单独展开:它执行任意代码(python3/java 子进程)与数据库只读查询,本质是一个"受控后门"。三层防御依次是——AdminTokenGuard 的共享秘密头(x-admin-token 常量时间比较未做,但为高熵随机串 + 非公开端点,风险可控)、进程级资源限制(mkdtemp 隔离工作目录、timeout 钳制 0.5–10s、代码与输出各 100K 上限、输出截断标记)、SQL 只读前缀正则(注释强调"服务端最终安全边界,必须与前端校验一致且更严格")。未做容器级隔离(无 namespace/cgroup),子进程可访问宿主文件系统与网络——在 RUN_ADMIN_TOKEN 泄露场景下这是完整的 RCE,评估时应把它当作"管理员能力"而非"用户功能"。
当前三层都缺 CSP:index.html 无 meta、NestJS 未设置 Content-Security-Policy 响应头、buildSrcDoc 产物无 CSP meta。在"内容仅维护者可写"的威胁模型下这是可接受的简化;但它意味着防线的最后一张网不存在——任何一处存储型 XSS(例如未来某个展示用户输入的页面)都可以直接加载外域脚本。
因此若未来开放用户提交示例,以下前置条件应视为硬性门槛,缺一不开:
sandbox.ts 注释给出的三选一,推荐独立子域):用户提交内容绝不能与主站同源运行;script-src 'unsafe-inline'(示例代码就是内联脚本,需精细设计)、connect-src 白名单与 frame-ancestors;宿主页面加全站 CSP;is_public 这类发布开关模式(knowledge 片段迁移已示范)应复用到示例提交;| # | 隐患 | 位置与证据 | 影响 | 缓解/建议 |
|---|---|---|---|---|
| 1 | POST /api/auth/me 标了 @Public(),cookie 回退通道疑似失效 | auth.controller.ts 148-155 行:@Public() 使全局守卫短路,req.user 永远为 undefined,端点恒返回 { user: null } | authStore.initialize() 的无 token 分支 checkMe() 永远拿不到用户——仅凭 cookie 的历史会话无法恢复登录(Bearer 通道不受影响) | 去掉该端点的 @Public() 或改为在端点内手动跑一次 jwt 解析;⚠️ 待确认 是否存在依赖 cookie 通道的存量用户 |
| 2 | refresh/login 令牌存 localStorage | authStore.persistSession | 沙箱逃逸(§1)或任何 XSS 可读走 token | 独立子域方案落地后自动缓解;短期可接受(内容半可信) |
| 3 | RateLimitGuard 为进程内存态且覆盖面窄 | guards/rate-limit.guard.ts;仅 auth 三端点 | 重启清零、多实例不共享;其他写端点无限流 | 换 Redis 实现;扩展到全部公开写端点 |
| 4 | run 模块无容器隔离 | run.service.ts 直接 spawn | token 泄露即 RCE | 部署层隔离(容器/低权用户/独立实例);确认 RUN_ADMIN_TOKEN 必配 |
| 5 | 无 CSP(全层) | 见 §6 | 纵深防御少一层 | 与开放提交需求绑定落地 |
| 6 | /admin 静态页无应用层鉴权 | app.module.ts ServeStaticModule | 依赖网络层限制;后台逻辑全在 API 侧 | 运维 IP 白名单 + 文档化 |
| 7 | 冻结模块在线(examples/chart-examples 等) | 见《13-技术债务》第 2 条 | 攻击面大于功能面 | 后端瘦身任务统一清理 |
| 8 | 默认管理员密码兜底 admin123 | auth.service.ts createDefaultAdmin | 首次部署忘配 ADMIN_DEFAULT_PASSWORD 时弱口令上线(有告警无阻断) | 与 JWT_SECRET 同样的"生产拒绝启动"策略 |
清单之外再强调一次结构性优点,因为它决定了隐患 1-8 的严重度上限:写路径几乎全部收敛在 AdminGuard 之后——公开可写的端点只有认证三件套与收藏(收藏写的是自己的数据,身份由 Bearer 决定,无越权面)。换言之,匿名攻击者在这个系统里能"改"的东西只有自己的会话与收藏,能"读"的只有本就公开的内容。这把绝大多数常见 Web 漏洞的利用前提直接拿掉了,也是评估该项目安全水位时最应该看到的一层。
结合 server/src/main.ts 的启动安检与《06-部署运行》§7,安全项的最小核对集:生产 JWT_SECRET 为 ≥32 字符随机值(安检通过即证明);ADMIN_DEFAULT_PASSWORD 已设置且非默认;DB_SYNC 关闭(防止实体漂移被静默写库);CORS_ORIGIN 为真实域名列表且不含通配;RUN_ADMIN_TOKEN 要么必配强随机值、要么明确该通道不用(未配置即禁用是安全的);/admin 已加网络层限制;auth-token cookie 在生产为 Secure(自动,取决于 NODE_ENV);若启用手机号登录,确认短信通道与验证码频控已评估(当前 phone-auth 仅靠图形验证码 + 限流兜底)。
本篇完。下一篇:09-测试与质量保障.md