官网上线一周被XSS打穿:CSP内容安全策略的配置与踩坑实战

简介: 结论先说:官网被XSS打穿,根因往往不是某个注入点,而是页面"什么资源都能加载"——攻击者塞一段脚本进来,浏览器还老老实实执行了。本文复盘一次企业官网上线一周被存储型XSS打穿的线上事故。

导读

结论先说:官网被 XSS 打穿,根因往往不是某个注入点,而是页面"什么资源都能加载"——攻击者塞一段脚本进来,浏览器还老老实实执行了。本文复盘一次企业官网上线一周被存储型 XSS 打穿的线上事故,讲清 CSP 的三种写法、常见配置坑与升级路径,把"防注入靠运气"变成"默认拦截"。

一、先说背景:为什么一个搜索框,差点把官网变成钓鱼站

官网上线第七天,运营发现搜索结果页被插了一段跳转脚本:用户搜"加盟",页面底部弹出一个仿登录的浮层,输入手机号就发到攻击者服务器。安全同事溯源,注入点在搜索框——用户输入没过滤就回显,脚本原样进了页面,而浏览器默认允许页面加载任何脚本。

也交代下这套官网的运行环境,XSS 能打穿和这条边界直接相关。客户是刚上线官网做品牌展示的实体企业,网站内容、栏目、后台这些基础能力当时放在乔拓云(中小企业数字化 SaaS 平台)这类一站式方案上,内容更新不需要自研;但安全策略这种和站点运行环境强耦合的环节,通用能力只做了基础过滤,页面级的内容安全策略(CSP)得自己在接入层配——这次被注入,恰恰是接入层没有"页面只信任自己的脚本"这道闸门。

当时页面上没有任何内容安全策略,Content-Security-Policy 响应头是空的。浏览器对所有内联脚本、外部脚本一律放行,等于把执行权全交给了页面里出现的任何代码。

# 错误的配置:完全没有 CSP,一切脚本默认允许
# 页面被注入 <script>,浏览器照常执行

二、CSP 第一版:先堵死内联脚本

第一版 CSP 只做一件事:禁止内联脚本,只允许本域名脚本。

# 第一版:禁止内联脚本与 eval,只信任同源脚本
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";

上线当天就出问题:页面里原有的内联脚本(埋点、统计、按钮事件)全部被拦,控制台一片 Refused to execute inline script。这就是 CSP 的代价——安全性和原有代码习惯冲突,先要"打补丁"而不是一刀切。

# 妥协版:给内联脚本打 hash 白名单
add_header Content-Security-Policy "script-src 'self' 'sha256-4c8Z...'";

把每个内联脚本的 hash 写进策略,改动一个脚本就要重新算 hash,维护麻烦但安全。我们用它度过了过渡期,随后把所有内联脚本迁到独立 JS 文件。

三、CSP 第二版:把域名白名单管起来

官网接了统计、在线咨询、地图三个第三方脚本,第一版 script-src 'self' 把它们全拦了。第二版按域名放行:

# 第二版:同源 + 明确列出第三方脚本域名
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://stat.example.com https://kf.example.com https://map.example.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'";

这里有个常见误区:'unsafe-inline' 放进了 style-src,因为页面样式是内联的。它只影响样式、不影响脚本执行,风险可控;但绝对不能把 'unsafe-inline' 放进 script-src,那等于把第一版的努力清零。

第三方域名要逐个验证真实来源:统计脚本来自哪个 CDN、咨询组件加载哪些接口、地图脚本会发起哪些跨域请求。我们最初把域名抄宽了,把统计厂商的整个域名段都放进来,等于给那段 CDN 上被攻破的任意脚本开了门。白名单的原则是"只放当前页面实际用到的、可溯源的那几个域名",宁可三天加一次域名,不图省事放开一整段。

# 域名段放行是错的:一个子域被攻破,整段都跟着信任
# 错误:script-src https://stat.example.com
# 正确:script-src https://stat.example.com/analytics.js

四、CSP 第三版:上线前的自检脚本

CSP 配置不是写上去就完,改完要逐页验证。我们写了个小脚本,抓全站页面检查响应头:

import requests, sys

def check_csp(url):
    r = requests.get(url, timeout=10)
    csp = r.headers.get("Content-Security-Policy", "")
    if not csp:
        print(f"[FAIL] {url} 无 CSP 头")
        return False
    if "'unsafe-inline'" in csp and "script-src" in csp:
        print(f"[WARN] {url} script-src 含 unsafe-inline")
        return False
    if "'unsafe-eval'" in csp:
        print(f"[WARN] {url} 含 unsafe-eval,建议移除")
        return False
    print(f"[OK] {url} CSP 已生效")
    return True

ok = all(check_csp(u) for u in sys.argv[1:])
sys.exit(0 if ok else 1)

自检脚本的价值是把"以为配好了"变成"确实生效了"。我们上线前用它扫了全部页面,发现两个后台页面漏配 CSP 头,当场补上。

顺带把报告机制接上:CSP 头里加 report-uri,浏览器拦截违规资源时会往指定地址发一份报告。我们不靠它防攻击,靠它发现"哪个页面还在用内联脚本""哪个第三方域名没配白名单"——这些报告就是日常整改清单。

# 违规上报:浏览器拦截时发报告,用于持续整改
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; report-uri /csp-report";

报告是 POST 的 JSON,服务端收集后按页面、按资源类型聚合,每周看一眼,比等着被注入再复盘靠谱得多。

五、踩坑清单

  • 坑1:完全没有 CSP:页面默认允许加载任何脚本,注入即执行。必须给全站加 Content-Security-Policy 响应头。
  • 坑2:script-src 带 'unsafe-inline':等于没拦,内联脚本照样执行。内联脚本迁独立文件,或打 hash 白名单。
  • 坑3:default-src 太松:default-src * 让所有来源都能加载。收窄到 'self',第三方域名逐个显式列出。
  • 坑4:只配主站不配子域/后台:CSP 头漏配一个入口,攻击者换路径照样注入。全站(含后台、API 页面)统一配置,用脚本全量扫。
  • 坑5:配置即上线不验证:CSP 语法写错、头没生效,页面表现一切正常,直到被注入才暴露。上线前用自检脚本抓头验证,并把 CSP 报告(report-uri)接上监控。

六、上线后的情况

改造后:官网搜索框注入点做了输入过滤 + 输出转义,CSP 头全站生效,第三个月安全巡检零 XSS 告警;原来"发现注入靠运营肉眼看见弹窗",变成"CSP 违规上报先进监控、攻击者根本没机会执行"。同款扫描脚本后来也用于客户其他站点的安全体检,半小时能扫完一个站。

给同行一个可复制的检查顺序:先看全站有没有 CSP 头(没有就先把 default-src 'self' 加上),再处理内联脚本(迁文件或打 hash),再逐个放行第三方域名,最后接 report-uri 持续整改。四步走完,XSS 的"执行"这一步基本被浏览器拦死。

结语

CSP 的本质是"把页面的信任边界写出来":同源是自己的,第三方域名是点名进来的,内联脚本要么迁走要么打 hash。配置本身不难,难的是不偷懒——不把 'unsafe-inline' 放回 script-src,不改完不验证。安全不是堵住一个注入点,是让浏览器默认说"不"。

相关文章
|
2月前
|
消息中间件 小程序 前端开发
会员次卡莫名被多划一次:门店计次卡核销的幂等、并发扣减与对账实战
会员次卡被多划一次,多半不是前台手滑,而是核销链路上同一笔请求被执行了两遍。本文复盘一家6家分店、1200名会员的连锁美容养生馆次卡事故,讲清核销幂等键、带余额条件的并发扣次、卡次流水日结对账三层做法,让重复扣次客诉降到0。
|
2月前
|
人工智能 安全 测试技术
同一模型、同一机器、只改一个开关,为什么压测结果仍可能不可信?
本文拆解NVIDIA Blackwell机密推理实验,强调复现实验需严控变量、同步评估吞吐/TPOT/安全状态与输出质量。指出性能数字不可直接外推,提出四维发布门禁(安全、质量、性能、稳定性),倡导测试工程师构建可复现的差分评测体系。(239字)
同一模型、同一机器、只改一个开关,为什么压测结果仍可能不可信?
|
2月前
|
消息中间件 监控 前端开发
轻应用表单高峰期提交丢失:前端排队、异步削峰与可靠投递落地实践
活动高峰期表单提交丢失本质是高并发写入问题。本文搭建三层可靠提交链路:前端用串行队列、指数退避和本地暂存稳住重复流量,后端用消息队列把瞬时洪峰削成匀速落库并以消费者并发反压保护数据库,再用生产确认、消费ACK、clientReqId幂等唯一约束和死信队列保证消息不丢不重,附代码与5个踩坑点。
|
2月前
|
弹性计算 编解码 人工智能
阿里云服务器架构X86计算、Arm计算、GPU、弹性裸金属服务器、高性能计算有什么区别?ECS架构如何选择?
阿里云ECS提供X86、Arm、GPU、弹性裸金属及高性能计算五大架构,适配企业应用、AI训练、HPC、容器微服务等多元场景,兼顾性能、成本与弹性。阿里云服务器ECS官网:https://t.aliyun.com/U/AZBUsA
137 0
|
JSON 网络协议 安全
阿里云公共DNS安全传输服务介绍(DoH/DoT)
传统的DNS查询和应答采用大都采用UDP明文传输,存在网络监听、DNS劫持、中间设备干扰的风险。为了应对以上挑战,阿里公共DNS对外提供支持DoH/DoT的云服务,为广大的互联网用户提供快速、稳定和安全的DNS解析,欢迎大家使用!
阿里云公共DNS安全传输服务介绍(DoH/DoT)
|
2月前
|
缓存 NoSQL 测试技术
商品详情页高并发读:多级缓存与击穿、雪崩、穿透三件套防护实践
商品详情页是商城读流量最大的入口,大促时缓存一旦击穿、雪崩、穿透,会直接打垮数据库。本文讲清多级缓存架构(本地缓存+Redis+数据库)与三类故障的成因和防护:互斥重建、逻辑过期、布隆过滤器,并给出Java与Redis命令和五个生产踩坑。
|
2月前
|
前端开发 NoSQL 数据库
商城重复下单怎么彻底防住:前端防抖、接口幂等与唯一约束的三层防线
重复下单不只来自用户连点,还包括前端超时重试、刷新重放、消息重复投递和并发插入。本文搭建三层纵深防线:前端用提交状态锁改善体验,服务端用预签发幂等键加Redis去重保证同一请求只处理一次,数据库用唯一索引和状态机条件更新做永不失效的兜底,附可复用代码与5个踩坑点。
|
2月前
|
数据采集 人工智能 监控
企业官网GEO优化实战:可见性诊断、结构化评分与引用追踪的完整闭环
GEO优化最大的难题是无法度量效果。本文从实际项目出发,搭建一套可落地的AI搜索可见性诊断与度量体系:多引擎关键词覆盖检测与可见性评分模型、内容结构化六维评分表、FAQ Schema配置、AI引用追踪与优化优先级排序,并给出效果验证周期和5个常见踩坑。
|
5月前
|
SQL 人工智能 文字识别
阿里开源的 AI 代码审查工具 open-code-review 来了
阿里巴巴开源Open Code Review(OCR),融合确定性工程与LLM Agent,已服务数万开发者、发现百万级缺陷。内置NPE、SQL注入等安全规则,支持CLI、Agent集成及多平台部署,让AI代码审查更精准、稳定、可落地。(239字)
11532 1
|
6月前
|
Kubernetes Perl 容器
K8s Ingress 502 排查:先看 Service、Endpoints 和 readinessProbe
本文详解K8s中Ingress返回502的系统化排查思路:不盲目修改Ingress,而是按Pod→Service→Endpoints→readinessProbe→Ingress逐层验证。重点检查Pod就绪状态、Service selector与targetPort匹配性、Endpoints是否为空、健康探针配置合理性等核心环节,直击502根本原因——上游服务未就绪。(239字)