Web安全从零开始 XSS IV

  1. 1. Defend
    1. 1.1. Seven Principles
      1. 1.1.1. 原则1:不要在页面中插入任何不可信数据,除非这些数已经据根据下面几个原则进行了编码
      2. 1.1.2. 原则2:在将不可信数据插入到HTML标签之间时,对这些数据进行HTML Entity编码
      3. 1.1.3. 原则4:在将不可信数据插入到SCRIPT里时,对这些数据进行SCRIPT编码
      4. 1.1.4. 原则5:在将不可信数据插入到Style属性里时,对这些数据进行CSS编码
      5. 1.1.5. 原则6:在将不可信数据插入到HTML URL里时,对这些数据进行URL编码
      6. 1.1.6. 原则7:使用富文本时,使用XSS规则引擎进行编码过滤
      7. 1.1.7. Summary
    2. 1.2. Escape
      1. 1.2.1. htmlspecialchars - php
      2. 1.2.2. htmlentities - php
      3. 1.2.3. Front end
    3. 1.3. HTTP Header
      1. 1.3.1. X-XSS-Protection
      2. 1.3.2. Content Security Policy
      3. 1.3.3. X-Frame-Options
      4. 1.3.4. HttpOnly
      5. 1.3.5. X-Content-Type-Options
  2. 2. Reference

这是自己写的 Web 安全从零开始系列之 XSS 篇。第四篇讲 XSS 防御。

[TOC]

Defend

无论是服务端型还是客户端型xss,攻击达成都需要两个条件

  • 代码被注入
  • 代码被执行

其实只要做好无论任何情况下保证代码不被执行就能完全杜绝 xss 攻击.

总之, 任何时候都不要把不受信任的数据直接插入到 dom 中的任何位置, 一定要做转义。

对于某些位置,不受信任的数据做转义就可以保证安全

  • 一般的标签属性值(非事件属性)
  • div body 的内部html

对于某些位置,即使做了转义依然不安全

  • script标签中
  • 注释中
  • 表签的属性名名
  • 标签名
  • css标签中

使用 JSON.parse 而不是eval, request 的content-type要指定是Content-Type: application/json;

如果链接的URL中部分是动态生成的,一定要做转义。

Seven Principles

原则1:不要在页面中插入任何不可信数据,除非这些数已经据根据下面几个原则进行了编码

第一条原则其实是“Secure By Default”原则:不要往HTML页面中插入任何不可信数据,除非这些数据已经根据下面几条原则进行了编码。

之所以有这样一条原则存在,是因为 HTML 里有太多的地方容易形成XSS漏洞,而且形成漏洞的原因又有差别,比如有些漏洞发生在HTML标签里,有些发生在HTML标签的属性里,还有的发生在页面的<Script>里,甚至有些还出现在CSS里,再加上不同的浏览器对页面的解析或多或少有些不同,使得有些漏洞只在特定浏览器里才会产生。如果想要通过XSS过滤器(XSS Filter)对不可信数据进行转义或替换,那么XSS过滤器的过滤规则将会变得异常复杂,难以维护而且会有被绕过的风险。

所以实在想不出有什么理由要直接往HTML页面里插入不可信数据,就算是有XSS过滤器帮你做过滤,产生XSS漏洞的风险还是很高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>…不要在这里直接插入不可信数据…</script>直接插入到SCRIPT标签里 
<!– …不要在这里直接插入不可信数据… –>
插入到HTML注释里

<div 不要在这里直接插入不可信数据=”…”></div>
插入到HTML标签的属性名里

<div name=”…不要在这里直接插入不可信数据…”></div>
插入到HTML标签的属性值里

<不要在这里直接插入不可信数据 href=”…”></a>
作为HTML标签的名字

<style>…不要在这里直接插入不可信数据…</style>
直接插入到CSS里

最重要的是,千万不要引入任何不可信的第三方 JavaScript 到页面里,一旦引入了,这些脚本就能够操纵你的HTML页面,窃取敏感信息或者发起钓鱼攻击等等。

原则2:在将不可信数据插入到HTML标签之间时,对这些数据进行HTML Entity编码

在这里相当强调是往HTML标签之间插入不可信数据,以区别于往HTML标签属性部分插入不可信数据,因为这两者需要进行不同类型的编码。当你确实需要往HTML标签之间插入不可信数据的时候,首先要做的就是对不可信数据进行HTML Entity编码。比如,我们经常需要往DIV,P,TD这些标签里放入一些用户提交的数据,这些数据是不可信的,需要对它们进行HTML Entity编码。很多Web框架都提供了HTML Entity编码的函数,我们只需要调用这些函数就好,而有些Web框架似乎更“智能”,比如Rails,它能在默认情况下对所有插入到HTML页面的数据进行HTML Entity编码,尽管不能完全防御XSS,但着实减轻了开发人员的负担。

1
2
<body>…插入不可信数据前,对其进行HTML Entity编码…</body><div>…插入不可信数据前,对其进行HTML Entity编码…</div><p>…插入不可信数据前,对其进行HTML Entity编码…</p>
以此类推,往其他HTML标签之间插入不可信数据前,对其进行HTML Entity编码

[编码规则]
那么HTML Entity编码具体应该做哪些事情呢?它需要对下面这6个特殊字符进行编码:

1
2
3
4
5
6
&     –>     &amp;
< –> &lt;
> –> &gt;
” –> &quot;
‘ –> &#x27;
/ –> &#x2f;

有两点需要特别说明的是:

  • 不推荐将单引号( ‘ )编码为 ' 因为它并不是标准的HTML标签
  • 需要对斜杠号( / )编码,因为在进行XSS攻击时,斜杠号对于关闭当前HTML标签非常有用

推荐使用OWASP提供的ESAPI函数库,它提供了一系列非常严格的用于进行各种安全编码的函数。在当前这个例子里,你可以使用:

1
String encodedContent = ESAPI.encoder().encodeForHTML(request.getParameter(“input”));

这条原则是指,当你要往HTML属性(例如width、name、value属性)的值部分(data value)插入不可信数据的时候,应该对数据进行HTML属性编码。不过需要注意的是,当要往HTML标签的事件处理属性(例如onmouseover)里插入数据的时候,本条原则不适用,应该用下面介绍的原则4对其进行JavaScript编码。

1
2
3
4
5
<div attr=…插入不可信数据前,进行HTML属性编码…></div>属性值部分没有使用引号,不推荐 
<div attr=’…插入不可信数据前,进行HTML属性编码…’></div>
属性值部分使用了单引号
<div attr=”…插入不可信数据前,进行HTML属性编码…”></div>
属性值部分使用了双引号

[编码规则]

除了空格符可以闭合当前属性外,这些符号也可以:

% * + , – / ; < = > ^ | `(反单引号,IE会认为它是单引号)

可以使用ESAPI提供的函数进行HTML属性编码:

1
String encodedContent = ESAPI.encoder().encodeForHTMLAttribute(request.getParameter(“input”));

原则4:在将不可信数据插入到SCRIPT里时,对这些数据进行SCRIPT编码

这条原则主要针对动态生成的JavaScript代码,这包括脚本部分以及HTML标签的事件处理属性(Event Handler,如onmouseover, onload等)。在往JavaScript代码里插入数据的时候,只有一种情况是安全的,那就是对不可信数据进行JavaScript编码,并且只把这些数据放到使用引号包围起来的值部分(data value)之中,例如:

1
2
3
<script>
var message = “<%= encodeJavaScript(@INPUT) %>”;
</script>

除此之外,往JavaScript代码里其他任何地方插入不可信数据都是相当危险的,攻击者可以很容易地插入攻击代码。

1
2
3
4
5
6
7
8
9
<script>alert(‘…插入不可信数据前,进行JavaScript编码…’)</script>值部分使用了单引号 
<script>x = “…插入不可信数据前,进行JavaScript编码…”</script>
值部分使用了双引号
<div onmouseover=”x=’…插入不可信数据前,进行JavaScript编码…’ “</div>
值部分使用了引号,且事件处理属性的值部分也使用了引号
特别需要注意的是,在XSS防御中,有些JavaScript函数是极度危险的,就算对不可信数据进行JavaScript编码,也依然会产生XSS漏洞,例如:
<script>
window.setInterval(‘…就算对不可信数据进行了JavaScript编码,这里依然会有XSS漏洞…’);
</script>

[编码规则]

除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \xHH (以 \x 开头,HH则是指该字符对应的十六进制数字)

在对不可信数据做编码的时候,千万不能图方便使用反斜杠( \ )对特殊字符进行简单转义,比如将双引号 ” 转义成 \” ,这样做是不可靠的,因为浏览器在对页面做解析的时候,会先进行HTML解析,然后才是JavaScript解析,所以双引号很可能会被当做HTML字符进行HTML解析,这时双引号就可以突破代码的值部分,使得攻击者可以继续进行XSS攻击。

可以使用ESAPI提供的函数进行JavaScript编码:

1
String encodedContent = ESAPI.encoder().encodeForJavaScript(request.getParameter(“input”));

原则5:在将不可信数据插入到Style属性里时,对这些数据进行CSS编码

当需要往Stylesheet,Style标签或者Style属性里插入不可信数据的时候,需要对这些数据进行CSS编码。传统印象里CSS不过是负责页面样式的,但是实际上它比我们想象的要强大许多,而且还可以用来进行各种攻击。因此,不要对CSS里存放不可信数据掉以轻心,应该只允许把不可信数据放入到CSS属性的值部分,并进行适当的编码。除此以外,最好不要把不可信数据放到一些复杂属性里,比如url, behavior等,只能被IE认识的Expression属性允许执行JavaScript脚本,因此也不推荐把不可信数据放到这里。

1
2
<style>selector { property : …插入不可信数据前,进行CSS编码…} </style><style>selector { property : ” …插入不可信数据前,进行CSS编码… “} </style>
<span style= property : …插入不可信数据前,进行CSS编码… ”></span>

[编码规则]

除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \HH (以 \ 开头,HH则是指该字符对应的十六进制数字)

同原则2,原则3,在对不可信数据进行编码的时候,切忌投机取巧对双引号等特殊字符进行简单转义,攻击者可以想办法绕开这类限制。

可以使用ESAPI提供的函数进行CSS编码:

1
String encodedContent = ESAPI.encoder().encodeForCSS(request.getParameter(“input”));

原则6:在将不可信数据插入到HTML URL里时,对这些数据进行URL编码

当需要往HTML页面中的URL里插入不可信数据的时候,需要对其进行URL编码,如下:

1
<a href=”http://www.abcd.com?param=…插入不可信数据前,进行URL编码…”> Link Content </a>

[编码规则]

除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 %HH (以 % 开头,HH则是指该字符对应的十六进制数字)

在对URL进行编码的时候,有两点是需要特别注意的:

1) URL属性应该使用引号将值部分包围起来,否则攻击者可以很容易突破当前属性区域,插入后续攻击代码
2) 不要对整个URL进行编码,因为不可信数据可能会被插入到href, src或者其他以URL为基础的属性里,这时需要对数据的起始部分的协议字段进行验证,否则攻击者可以改变URL的协议,例如从HTTP协议改为DATA伪协议,或者javascript伪协议。

可以使用ESAPI提供的函数进行URL编码:

1
String encodedContent = ESAPI.encoder().encodeForURL(request.getParameter(“input”));

ESAPI还提供了一些用于检测不可信数据的函数,在这里我们可以使用其来检测不可信数据是否真的是一个URL:

1
2
3
4
String userProvidedURL = request.getParameter(“userProvidedURL”);boolean isValidURL = ESAPI.validator().isValidInput(“URLContext”, userProvidedURL, “URL”, 255, false); 
if (isValidURL) {
<a href=”<%= encoder.encodeForHTMLAttribute(userProvidedURL) %>”></a>
}

原则7:使用富文本时,使用XSS规则引擎进行编码过滤

Web应用一般都会提供用户输入富文本信息的功能,比如BBS发帖,写博客文章等,用户提交的富文本信息里往往包含了HTML标签,甚至是JavaScript脚本,如果不对其进行适当的编码过滤的话,则会形成XSS漏洞。但我们又不能因为害怕产生XSS漏洞,所以就不允许用户输入富文本,这样对用户体验伤害很大。

针对富文本的特殊性,我们可以使用XSS规则引擎对用户输入进行编码过滤,只允许用户输入安全的HTML标签,如<b>,<i>,<p>等,对其他数据进行HTML编码。需要注意的是,经过规则引擎编码过滤后的内容只能放在<div>,<p>等安全的HTML标签里,不要放到HTML标签的属性值里,更不要放到HTML事件处理属性里,或者放到<SCRIPT>标签里。

推荐XSS规则过滤引擎:OWASP AntiSamp或者Java HTML Sanitizer

Summary

  • 当输出点出现在HTML标签属性:
1
2
3
4
5
< -> &lt;
> -> &gt;
& -> &amp;
" -> &quot;
' -> &#39
  • 当输出点出现在<script>标签中。这种情况相当危险,不需要考虑xss触发,只需要考虑编写js即可
1
2
3
4
5
6
' -> \';
" -> \";
\ -> \\;
/ -> \/;
(换行符) -> \n;
(回车符) -> \r;
  • 当输出点出现在body中
1
2
3
4
5
< -> &lt;
> -> &gt;
& -> &amp;
" -> &quot;
' -> &#39
  • 当输出点出现在js事件中(onClick=”你的代码”)
1
2
3
4
5
6
7
8
9
< -> &lt;
> -> &gt;
& -> &amp;
" -> &quot;
' -> &#39
\ -> \\;
/ -> \/;
(换行符) -> \n;
(回车符) -> \r;
  • 输出在URL属性中<script src="你的代码">
    • URL编码

Escape

htmlspecialchars - php

1
2
> htmlspecialchars ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = TRUE ]]] ) : string
>

将特殊字符转换为 HTML 实体

string 待转换的 string

flags 位掩码,由以下某个或多个标记组成,设置转义处理细节、无效单元序列、文档类型。 默认是 ENT_COMPAT | ENT_HTML401

double_encode 关闭 double_encode 时,PHP 不会转换现有的 HTML 实体, 默认是全部转换。

encoding An optional argument defining the encoding used when converting characters.

字符 替换后
& (& 符号) &amp;
(双引号) &quot,除非设置了 ENT_NOQUOTES
(单引号) 设置了 ENT_QUOTES 后, &#039; (如果是 ENT_HTML401) ,或者 &apos; (如果是 ENT_XML1ENT_XHTMLENT_HTML5)。
< (小于) &lt;
> (大于) &gt;

htmlentities - php

​ 本函数各方面都和 htmlspecialchars() 一样, 除了 htmlentities() 会转换所有具有 HTML 实体的字符。

Front end

前端过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function xssCheck(str,reg){
return str ? str.replace(reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g, function (a, b) {
if(b){
return a;
}else{
return {
'<':'&lt;',
'&':'&amp;',
'"':'&quot;',
'>':'&gt;',
"'":'&#39;',
}[a]
}
}) : '';
}

HTTP Header

X-XSS-Protection

可以通过 http 头控制是否打开 xss-filter,默认为开启。

通常情况下, 在http header中加入以下字段表示启用 xss-filter。除了 Firefox ,连 IE 8 以上均支持 X-XSS-Protection

1
2
3
4
X-XSS-Protection: 0
X-XSS-Protection: 1
X-XSS-Protection: 1; mode=block
X-XSS-Protection: 1; report=<reporting-uri>
  • 0

    禁止XSS过滤。

  • 1

    启用XSS过滤(通常浏览器是默认的)。 如果检测到跨站脚本攻击,浏览器将清除页面(删除不安全的部分)。

  • 1;mode=block

    启用XSS过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。

  • 1; report= (Chromium only)

    启用XSS过滤。 如果检测到跨站脚本攻击,浏览器将清除页面并使用CSP report-uri指令的功能发送违规报告。

如上, 现代浏览器都对反射型 xss 有一定的防御力,其原理是检查 url 和 dom 中元素的相关性,但这并不能完全防止反射型 xss

另外, 浏览器对于存储型 xss 并没有抵抗力, 原因很简单, 用户的需求是多种多样的. 所以, 抵御xss这件事情不能指望浏览器。

Content Security Policy

还有就是我们之前介绍的 CSP 策略了。

为了缓解很大一部分潜在的跨站脚本问题,浏览器的扩展程序系统引入了 CSP。CSP 管理网站允许加载的内容, 并且使用白名单的机制对网站加载或执行的资源起作用。在网页中, 这样的策略通过 HTTP 头信息或者 meta 元素定义。

CSP 并不是用来防止 xss 攻击的,而是最小化 xss 发生后所造成的伤害。实际上, 除了开发者自己做好 xss 转义, 并没有别的方法可以防止 xss 的发生. CSP 可以说是 HTML5 给web安全带来的最实惠的东西。那么如何引入 CSP 呢?

  1. 通过响应头

    只允许脚本从本源加载Content-Security-Policy: script-src 'self'

  2. 通过 HTML 的 META 标签

    1
    <meta http-equiv="Content-Security-Policy" content="script-src 'self'">

那么 CSP 除了限制 script-src 之外还能限制什么呢?

  • base-uri : 限制这篇文档的uri
  • child-src :限制子窗口的源(iframe,弹窗等),取代 frame-src
  • connect-src :限制脚本可以访问的源
  • font-src : 限制字体的源
  • form-action : 限制表单能够提交到的源
  • frame-ancestors : 限制了当前页面可以被哪些页面以iframe,frame,object等方式加载
  • frame-src :deprecated with child-src,限制了当前页面可以加载哪些源,与frame-ancestors对应
  • img-src : 限制图片可以从哪些源加载
  • media-src : 限制video, audio, source, track 能够从哪些源加载
  • object-src :限制插件可以从哪些源加载
  • sandbox :强制打开沙盒模式

另外,CSP 还提供一个报告的头域 Content-Security-Policy-Report-Only,使用这个头域,浏览器会向服务器报告 csp 状态。

1
Content-Security-Policy-Report-Only: script-src 'self'; report-uri http://cspReport/

使用了上面的设置, 若页面上存在内联的 js,它依然会执行,不过浏览器会向发送一个 post 请求,包含如下信息:

1
2
3
4
5
6
7
8
9
{ 
"csp-report":{
"document-uri": "http://cspReport/test.php",
"referrer": "",
"violated-directive": "script-src 'self'",
"original-policy": "script-src 'self'; report-uri http://cspReport/",
"blocked-uri": ""
}
}

CSP 目前有两版, [CSP1][] 和 [CSP2][]. 两版的支持状态可以在 http://caniuse.com/#search=csp 中查到. 如下:

CSP 1.0:

CSP 2.0:

X-Frame-Options

​ X-Frame-Options 有三个值:

  • DENY

    表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。

  • SAMEORIGIN

    表示该页面可以在相同域名页面的 frame 中展示。

  • ALLOW-FROM *uri*

    表示该页面可以在指定来源的 frame 中展示。

X-Frame-Options 响应头是用来给浏览器指示允许一个页面可否在 frame, iframe 或者 object 等标签中展现的标记. 网站可以使用此功能, 来确保自己网站的内容没有被嵌到别人的网站中去, 也从而避免了点击劫持 (clickjacking) 的攻击. 但以后可能被CSP的 frame-ancestors取代。目前支持的状态比起 CSP frame-ancestors要好。

HttpOnly

当 Cookie 在消息头中被设置为 HttpOnly 时,这样支持 Cookie 的浏览器将阻止客户端 Javascript 直接访问浏览器中的 cookies ,从而达到保护敏感数据的作用。

X-Content-Type-Options

​ X-Content-Type-Options: nosniff

nosniff

​ 假如请求类型为以下两种,那么阻止请求的发生:

  • style“ 但是 MIME 类型不是 “text/css“,
  • script“ 但是 MIME 类型不是 JavaScript MIME 类型

X-Content-Type-Options 响应首部相当于一个提示标志,被服务器用来提示客户端一定要遵循在 Content-Type 首部中对 MIME 类型 的设定,而不能对其进行修改。这就禁用了客户端的 MIME 类型嗅探行为,换句话说,也就是意味着网站管理员确定自己的设置没有问题。

Reference

防御XSS的七条原则

xss攻防浅谈

版权声明:除文章开头有特殊声明的情况外,所有文章在取得作者允许授权转载的情况下,均可在遵从 CC BY 4.0 协议的情况下转载。

面试经验分享 Web安全从零开始 XSS III

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×