从原型到上线的安全检查清单 从原型到上线的安全检查清单原型能运行上线还差一组边界Node.js、Next.js、Nuxt 等方案让页面渲染与 API 放在相近的工程里但服务端代码仍要按后端入口对待。浏览器传来的 URL、对象 ID、富文本和请求头都是外部输入仓库位置接近不会让这些输入自动可信。SSR/API Routes 增加了服务端请求入口。若服务端直接请求用户提供的 URL可能引入 SSRF 风险域名解析、重定向和内网地址都应进入出站策略。WAF 可以作为附加防护SSR 渲染层和 Node.js 运行时仍需完成参数校验、对象授权与出站请求控制。上线检查的目的是确认这些判断发生在实际副作用之前并且拒绝路径也能被观察和复验。需要逐项检查的入口1. SSR 渲染层的反射型与存储型 XSSReact、Vue 等框架在普通文本位置会进行转义但dangerouslySetInnerHTML、v-html和手工模板属于逃生口。若产品确实允许用户提交 HTML应使用维护中的 HTML sanitizer并限制允许的标签和属性。清洗后的内容不要再交给会修改字符串的未知库若只需显示文本继续使用框架默认文本渲染更简单。2. SSRF (Server-Side Request Forgery服务端请求伪造)链接预览、OpenGraph 抓取和图片代理会让服务器访问用户提供的地址。只检查 URL 字符串不够主机名解析后的地址、IPv4 与 IPv6、重定向后的每一跳和实际连接目标都要受限制。能够列出允许域名的场景优先采用白名单并关闭客户端自动跟随未经重新校验的重定向。3. 对象级授权失效 (BOLA / IDOR)API 收到documentId等对象标识时要验证当前身份是否有权对该对象执行当前动作。只比较“对象所有者是否等于当前用户”只能覆盖一部分模型共享文档、组织角色和委托访问需要按业务授权规则判断。对象标识不可猜也不能替代授权。4. 环境变量与 API 密钥透传漏洞框架通常约定某些前缀的变量可进入客户端包。命名扫描只能发现明显问题更可靠的办法是维护允许公开的变量清单并检查最终构建产物。服务端密钥通过部署平台受控注入不写进仓库、示例和错误日志怀疑泄露时先轮换不能只改变量名。防护放在实际执行位置出站 HTTP 应收口到统一客户端负责协议、解析地址、重定向、超时、响应大小和网络出口策略。API 授权根据当前会话、目标资源与动作执行不能信任请求体传来的用户 ID。公开环境变量采用允许清单并在候选制品中复查。三类控制分别解决不同问题不要用一个“安全中间件”标签掩盖职责。一个 TypeScript 检查示例及其局限下面的FullstackSafetyGateway演示富文本清洗、字面 IP 检查和简单所有者比较。它不能作为完整 SSRF 防线主机名没有解析IPv6 与更多保留地址未覆盖也没有重新校验重定向和实际连接地址。assertResourceOwnership同样只适合单一所有者模型。保留这段代码是为了说明检查位置接入应用前必须按网络和授权模型补全。import { Request, Response, NextFunction } from express; import { z } from zod; import createDOMPurify from dompurify; import { JSDOM } from jsdom; import * as net from net; import { URL } from url; // 在 Node.js 环境中初始化 DOMPurify const window new JSDOM().window; const DOMPurify createDOMPurify(window as any); /** * 全栈安全防爆网关 */ export class FullstackSafetyGateway { /** * 1. XSS 富文本清洗中间件 (专门防护 SSR 动态 HTML 渲染) */ public static sanitizeHtmlInput(dirtyHtml: string): string { return DOMPurify.sanitize(dirtyHtml, { ALLOWED_TAGS: [b, i, em, strong, a, p, br], ALLOWED_ATTR: [href, target, title], }); } /** * 2. SSRF 防护拦截器: 检查 URL 是否尝试访问内网 IP 或敏感域名 */ public static validateSafeUrl(inputUrl: string): { isSafe: boolean; reason?: string } { try { const parsed new URL(inputUrl); // 仅允许 http 和 https 协议禁止 file://, ftp://, gopher:// if (![http:, https:].includes(parsed.protocol)) { return { isSafe: false, reason: 仅支持 HTTP/HTTPS 协议 }; } const hostname parsed.hostname; // 校验是否为 IP 地址 if (net.isIP(hostname)) { if (this.isPrivateIP(hostname)) { return { isSafe: false, reason: 拦截 SSRF 攻击: 禁止请求私有/内网 IP 地址 }; } } // 禁止localhost与常见内网域名 if ([localhost, metadata.google.internal].includes(hostname.toLowerCase())) { return { isSafe: false, reason: 拦截 SSRF 攻击: 禁止请求保留域名 }; } return { isSafe: true }; } catch (e) { return { isSafe: false, reason: 非法 URL 格式 }; } } /** * 判断是否为私有/内网 IP */ private static isPrivateIP(ip: string): boolean { // 匹配 127.0.0.1, 10.x.x.x, 172.16.x.x - 172.31.x.x, 192.168.x.x, 169.254.x.x (Metadata) const privatePattern /^(127\.0\.0\.1|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3})$/; return privatePattern.test(ip); } /** * 3. BOLA 对象级越权防护断言 */ public static assertResourceOwnership( currentUserId: string, resourceOwnerId: string ): void { if (currentUserId ! resourceOwnerId) { throw new Error([Security Violation 403] 越权访问: 您无权对该资源进行读取或修改); } } } // 使用示例与测试 if (require.main module) { // 测试 SSRF 防护 const dangerousUrls [ http://127.0.0.1:8080/admin, http://169.254.169.254/latest/meta-data/, https://api.github.com/users, ]; console.log(️ 正在进行 SSRF 安全过滤检测:); dangerousUrls.forEach(url { const check FullstackSafetyGateway.validateSafeUrl(url); if (!check.isSafe) { console.error(❌ [拦截成功] URL: ${url} - 原因: ${check.reason}); } else { console.log(✅ [安全通过] URL: ${url}); } }); // 测试 XSS 清洗 const dirtyScript scriptalert(XSS Vulnerability)/scriptpHello World a hrefjavascript:stealCookies()Click Me/a/p; const cleanHtml FullstackSafetyGateway.sanitizeHtmlInput(dirtyScript); console.log(\n️ XSS 清洗前后对比:); console.log(清洗前:, dirtyScript); console.log(清洗后:, cleanHtml); }上线前逐项核对校验分类核心检查项 (Checklist)确定性防范措施密钥泄漏防护.env、客户端变量与构建产物中没有秘密公开变量允许清单、制品扫描和轮换预案SSRF 风险控制所有服务端出站请求经过统一策略校验协议、解析地址、重定向和实际网络出口XSS 防护搜索dangerouslySetInnerHTML、v-html与手工模板文本使用安全输出位置确需 HTML 时清洗并及时更新依赖对象级授权每个接收对象 ID 的接口按动作校验使用服务端身份与业务策略查询可访问对象并测试拒绝路径CORS 与 Header来源、凭据和响应头符合实际部署按调用方设置 CORSCSP 作为附加层并在报告模式下验证日志与错误响应和日志不包含令牌、内部地址或完整输入使用请求标识、错误阶段与脱敏摘要定位用反例完成验收安全验收不只跑正常请求。准备无权对象 ID、过期会话、富文本危险属性、主机名解析到受限地址和多次重定向等受控样本确认请求在副作用前停止。测试在隔离环境进行不访问真实内网资源也不把生产凭据写进样例。发现问题时保留配置版本、请求标识和修复后的回归结果。依赖、网络或授权模型变化后重跑相关场景。原型到上线的差别不是多装一个安全包而是每个外部入口都有明确的允许条件、拒绝行为和维护负责人。