这几天遇到一些涉及 web 安全的问题,然后在自己的博客中翻到了这篇两年前的文章,觉得有不错的参考价值,在阅读过程中,我不断的在想外面的世界这么精彩,写这篇文章的家伙为什么会窝在自己的电脑前没日没夜的做试验,写这么长的一篇东西,这也许就是程序员的一种原始的创作欲望吧。文章比较长,demo: https://github.com/baya/websafe 代码可能更适合阅读。
本文主要描述了 XSS, CSRF, Session Fixation 和 Brute Force 等四种威胁 WEB 安全的攻击手段的概念原理以及相关预防方法。 对于怎么预防 XSS 攻击,本文给出了九种方法,比如 HTML Escape Before Inserting Untrusted Data into HTML Element Content 等, 并结合 rails 对各种预防方法进行了一系列试验,尤其是对 HTTPOnly 和 Content Security Policy 两种方法不厌其烦地进行了十次试验。 同样对于怎么防御 CSRF攻击,本文从其原理着手,结合 rails 进行了一系列有针对性的攻防试验,以帮助我们理解 CSRF 攻击的原理并积累预防 CSRF 攻击的经验。
平时开发项目时会注意一些比较基本的安全问题,比如数据库里不能用明文保存密码,线上日志过滤掉密码等敏感信息,把任何来自用户的数据看作不安全数据,防止 SQL 注入,防止用户在网页上运行脚本等。 我想现在是时候开始对 WEB 安全方面的知识做一个比较系统的学习了,那就从 XSS, CSRF, Session Fixation, Brute Force Attack 等开始吧。
XSS 全称 Cross Site Scripting 即跨站点脚本攻击, 本来 Cross Site Scripting 的缩写应该是 CSS, 但是 CSS 在开发者心中早已经根深蒂固地表示为 Cascading Style Sheet 即层叠样式表的缩写了,还好 Cross 有十字架的意思,而 X 这个字母又像十字架,所以 XSS 这个缩写在某种意义上来说是非常贴近原意的。
XSS 简单的来说就是在那些本身善良,受信任的目标网站上注入恶意代码,通过这些恶意代码,攻击者可以实施盗取用户数据,盗取用户的 session 等等危害行为。那攻击者是怎么注入恶意代码的呢? 一个应用或多或少需要接收用户输入的数据,同时如果你的应用在输出用户数据时没有对它们进行验证或者编码或者采取的相关措施不够仔细严格,那么就会给攻击者以可乘之机,攻击者会在很多让你意想不到的地方向你的应用注入恶意代码。
这些招数来自于 https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet, 为方便以后查阅,我根据自己的理解对其作了一些试验和梳理:
这招非常狠,我根本不让你插入任何我不信任的数据,纵你再狡猾你也无法对我发起 XSS 攻击了,这就像把电脑网线拔掉来阻止电脑被黑一样。比如在下面的地方,我们不放任何不可信数据:
任何 HTML 元素里 <body>...不放不可信数据..</body>
script 标签里 <script>...不放不可信数据..</script>
HTML 注释里 <!--...不放不可信数据...-->
元素的属性名称里 <div ...不放不可信数据...=test />
标签名称里 <不放不可信数据 ... href="/test" />
CSS 里 <style>...不放不可信数据 ...</style>
如前面所说这招杀伤力过大,如果死板执行的话,会导致我们的应用基本不可用,但是招数#0的最重要的意义是提醒我们插入任何用户数据之前请三思:必须插入这种数据吗?这种数据可信吗?若不可信, 怎么让它变的可信或者至少无害呢?后面的 1-5 招都是在这一基础上进行的:如果我们要插入不可信的数据,那么我们必须让这些不可信数据至少变得无害。 为了方便后面的试验,我建立了一个叫 websafe 的 Rails 项目,代码在此:websafe,后面的所有试验如果没有特殊说明,都是在此项目里进行的。
HTML 转义一般来说是进行下面的一些操作,从而阻止可执行内容的产生。
& --> &
< --> <
> --> >
" --> "
' --> '
/ --> /
我们看一段 erb 代码,
<h2>Html Escape</h2>
<p class="dangerous"><%=raw @evil_user_data %></p>
<p class="safe"><%=h @evil_user_data %></p>
<p class="safe"><%= @evil_user_data %></p>
其中 p.dangerous 是危险的,因为它输出的是原始的用户数据,p.safe 是安全的因为它对用户数据进行了 HTML 转义,如果
@evil_user_data = 'alert("see you")'
那么 p.dangerous 会导致浏览器执行 alert("see you")
, p.safe 则会包含一个经过转义的无害的数据:
<script>alert("see you")</script>
早期 Rails 的版本中 <%= @evil_user_data %>
等价于 <%=raw @evil_user_data %>
,我们必须显示的使用 h
方法进行 HTML 转义才能达到
安全的目的,不过现在的 rails 已经改正了这一做法,现在 <%= @evil_user_data %>
和 <%=h @evil_user_data %>
是等价的,也就是说在现代的 Rails
中只要我们不显式的去调用 raw
,Rails 能够帮助我们做好 HTML Escape 这一工作的。
首先我们做一个试验,控制器代码,
class AttributeEscapeController < ApplicationController
def index
@evil_user_data = "\"><script>alert(\"XSS\")</script><\""
@evil_user_data_2 = "\"\"><script>alert(\"XSS\")</script><\"\""
@evil_user_data_3 = "javascript:var a='XSS'; alert(a)"
end
end
模板代码,
<h2>Attribute Escape</h2>
<p class="dangerous1" value="<%=raw @evil_user_data %>">Hello Danger</p>
<p class="dangerous2" value=<%=raw @evil_user_data_2 %>>Hello Danger</p>
<a class="dangerous3" href="<%=h @evil_user_data_3 %>">XSS</a> <br/>
<p class="safe" value="<%=h @evil_user_data %>">Hello Safe</p>
<p class="safe" value="<%= @evil_user_data %>">Hello Safe</p>
p.dangerous1, p.dangerous2 和 a.dangerous3 会被恶意数据侵入,变成:
<p class="dangerous" value=""><script>alert("see you")</script><"">Hello Danger</p>
<p class="dangerous" value=""><script>alert("see you")</script><"">Hello Danger</p>
<a class="dangerous3" href="javascript:var a='XSS'; alert(a)">XSS</a>
注意 a.dangerous3 仅通过 h
方法还是不够的,我们需要去掉 'javascript' 这种能够引起恶意代码执行的字符串,所以我们有 a.safe2,
<a class="safe2" href="<%=h @evil_user_data_3.gsub('javascript', '') %>">Hello Safe</a> <br/>
当我们需要在 JavaScript 代码里插入用户数据时,我们需要进行 JavaScript Escape, 现在我们进行一次 XSS 攻击试验,
控制器代码,
class JsEscapeController < ApplicationController
def index
@evil_user_data = "\";alert(\"XSS\");//"
end
end
模板代码,
<h2>Js Escape</h2>
<script>
var user_dangerous = "<%=raw @evil_user_data %>";
var user_safe1 = "<%=h @evil_user_data %>";
var user_safe2 = "<%= @evil_user_data %>";
var user_safe3 = "<%= escape_javascript @evil_user_data %>";
var user_safe4 = <%=raw @evil_user_data1.to_json %>;
</script>
user_dangerous 会引入 XSS 攻击,它会变成:
var user_dangerous = "";alert("XSS");//";
user_safe1, user_safe2, user_safe3, user_safe4 虽然可能导致数据不正确,但至少都是安全的。
基于这一方法,如果我们需要响应浏览器 JSON 数据,那么我们应该保证响应头的 Content-Type 为 application/json 而不是 text/html, 否则浏览器 会直接执行响应报文里地 javascript 代码。试验如下,
控制器代码:
class JsonEscapeController < ApplicationController
def index
@evil_user_data = "{\"Message\":\"No HTTP resource was found that matches the request URI 'dev.net.ie/api/pay/.html?HouseNumber=9&AddressLine
=The+Gardens<script>alert(XSS)</script>&AddressLine2=foxlodge+woods&TownName=Meath'.\",\"MessageDetail\":\"No type was found
that matches the controller named 'pay'.\"}"
render text: @evil_user_data
end
end
响应为:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
{"Message":"No HTTP resource was found that matches the request URI 'dev.net.ie/api/pay/.html?HouseNumber=9&AddressLine
=The+Gardens<script>alert('XSS')</script>&AddressLine2=foxlodge+woods&TownName=Meath'.","MessageDetail":"No type was found
that matches the controller named 'pay'."}
这种响应会导致浏览器执行代码 alert('XSS')
, 也就是说我们的应用被 XSS 了。但是如果我们将响应头的 Content-Type 设置为 application/json
就可以避免这种侵入,在 Rails 中可以像下面这样做:
class JsonEscapeController < ApplicationController
def index
@evil_user_data = "{\"Message\":\"No HTTP resource was found that matches the request URI 'dev.net.ie/api/pay/.html?HouseNumber=9&AddressLine
=The+Gardens<script>alert(XSS)</script>&AddressLine2=foxlodge+woods&TownName=Meath'.\",\"MessageDetail\":\"No type was found
that matches the controller named 'pay'.\"}"
render json: @evil_user_data.to_json
end
end
此时响应为:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
"{\"Message\":\"No HTTP resource was found that matches the request URI 'dev.net.ie/api/pay/.html?HouseNumber=9\u0026AddressLine\n =The+Gardens\u003cscript\u003ealert('XSS')\u003c/script\u003e\u0026AddressLine2=foxlodge+woods\u0026TownName=Meath'.\",\"MessageDetail\":\"No type was found\n that matches the controller named 'pay'.\"}"
往 HTML 样式的属性里插入不可信数据时需要进行 CSS Escape 并且对数据进行严格校验,比如 URLs 只能以 "http"开头而不是"javascript",同时各属性值永远不要以 "expression" 开头。原文中给了两个例子:
{ background-url : "javascript:alert(1)"; } // and all other URLs
{ text-size: "expression(alert('XSS'))"; } // only in IE
第一个例子在 chrome 和 safari 浏览器里做过测试,不会引导致 XSS,应该是现代的浏览器已经把这个漏洞堵住了。第二个例子,没有 IE 就没试验了。
向 HTML URL 的参数里插入不可信数据之前,需要对不可信数据做 URL Escape。在 Rails 里可以通过 CGI.escape 或者 URI.encode 做这个工作。
在 Rails 中可以使用方法 sanitize
做这方面的工作。sanitize
使用白名单制度,只有被允许的标签和属性才能作为可信数据插入到页面中。
DOM-based XSS 就是在操作 DOM 环境时向受害者的浏览器注入恶意代码。文中给出了一个攻击例子,
Select your language:
<select><script>
document.write("<OPTION value=2>English</OPTION>");
document.write("<OPTION value=1>"+document.location.href.substring(document.location.href.indexOf("default=")+8)+"</OPTION>");
</script></select>
在我的试验中,预想访问 http://localhost:3000/dom_base_xss?default=<script>alert('XSS')</script>
会生成恶意代码:
<script>alert('XSS')</script>
但是浏览器会自动把 <script>alert('XSS')</script>
转义为 "%3Cscript%3Ealert(%27xss%27)%3C/script%3E", 所以试验没有成功,但是我们还是需要在 DOM-based XSS 这方面引起重视,毕竟道高一尺,魔高一丈。
在 HTTP 的 Set-Cookie 响应头里增加一个 HTTPOnly 标识就可以使用 HTTPOnly cookie 了,当然必须浏览器支持,否则浏览器会忽略此标识,如果浏览器支持 HTTPOnly 标识, 那么客户端的代码就无法访问到这些受到保护的 cookie。我觉得这是一个非常有用的功能,这样即使站点被 XSS 污染,攻击者也拿不到 cookie 这类非常重要的数据,从而 能最大程度地减少站点和用户的损失。现在我们用一个实际例子来试验设置 HTTPOnly cookie flag。
控制器代码:
class HttponlyFlagController < ApplicationController
def true
cookies["user_name"] = { value: "david", httponly: true }
end
def false
cookies["user_name"] = { value: "david", httponly: false }
end
end
httponly_flag/true.html.erb
<h2>HTTPOnly True</h2>
<script>
alert(getCookie('user_name'));
function getCookie(c_name)
{
if (document.cookie.length>0)
{
c_start=document.cookie.indexOf(c_name + "=")
if (c_start!=-1)
{
c_start=c_start + c_name.length+1
c_end=document.cookie.indexOf(";",c_start)
if (c_end==-1) c_end=document.cookie.length
return unescape(document.cookie.substring(c_start,c_end))
}
}
return ""
}
</script>
设置 HTTPOnly flag 时,其试验结果如下图所示:
我们看到 cookies["user_name"] 受到保护,客户端无法访问到 cookies["user_name"]。
httponly_flag/false.html.erb
<h2>HTTPOnly False</h2>
<script>
alert(getCookie('admin_name'));
function getCookie(c_name)
{
if (document.cookie.length>0)
{
c_start=document.cookie.indexOf(c_name + "=")
if (c_start!=-1)
{
c_start=c_start + c_name.length+1
c_end=document.cookie.indexOf(";",c_start)
if (c_end==-1) c_end=document.cookie.length
return unescape(document.cookie.substring(c_start,c_end))
}
}
return ""
}
</script>
未设置 HTTPOnly flag 时,试验结果如下图所示,
我们看到客户端此时可以访问 cookies["admin_name"]。
我们再看看 cookies['user_name'] 和 cookies['admin_name'] 的属性,注意看最后一条属性:'脚本可访问'
cookies['user_name']:
名字: user_name
内容: david
域: localhost
路径: /
发送用途: 各种连接
脚本可访问: 否(仅 Http)
cookies['admin_name']:
名字: admin_name
内容: jim
域: localhost
路径: /
发送用途: 各种连接
脚本可访问: 是
Web 世界在经历 XSS 这个魔鬼多年的折磨和迫害之后, CSP 的出现可以说是大势所趋,民心所向。CSP 之前的各种抵抗 XSS 的方案虽然有效果,并且广泛使用,但是这些方案无论 从哪个角度来看都像是在缝缝补补,并且在 XSS 疯狂的攻击下疲于应付,始终摆脱不了道高一尺,魔高一仗的咒语。而 CSP 的出现,让我们终于拥有了强大而系统化的武器去抗击 XSS 的侵害。
在响应头 Content-Security-Policy 里添加一些 CSP 相关的指令就可以实现 CSP 了,有些浏览器支持 X-WebKit-CSP 或者 X-Content-Security-Policy, 不过向 前看,现代的浏览器都会支持 Content-Security-Policy 头,而忽略掉那些带前缀的 CSP 头,所以我们应该使用 Content-Security-Policy 头,在这里我们的 CSP 头如无 说明即指 Content-Security-Policy 头。CSP 在控制页面加载资源的粒度上为我们开发者提供了丰富的指令,我们可以先快速了解下这些指令:
如我们前面所看到的,XSS 攻击最主要的威胁来自于内联脚本的嵌入,所以我们着重研究下指令 script-src, 并做一系列和 script-src 指令有关的试验,在试验之前我们需要说明两点:
CSP 指令之间用 ";" 号分开,指令的值用空格分开,比如,
Content-Security-Policy: script-src self https://apis.google.com; report-uri /my_csp_report_parser;
在试验之前我们认识一个新指令:report-uri, 这个指令的作用是指定一个 URI, 当用户代理发现有违背 CSP 的事件时就会向这个 URI 发送报告。
测试环境:Rails-4.1.5, Chrome38, 为了能够接收到浏览器发送的 CSP 违规报告,我们的每一次试验都会设置 report-uri 为:'/csp_report'。
与 '/csp_report' 对应的控制器是 CspReportController
, 在此控制器里我们会打印出 CSP 违规报告,控制器的代码如下:
class CspReportController < ApplicationController
skip_before_filter :verify_authenticity_token
def create
Rails.logger.info "received csp report: #{pp JSON.parse(request.raw_post)}"
render text: 'ok'
end
end
script-src 'none'
'none' 的意思是禁止加载任何脚本,无论此脚本来自本站还是外站,也不允许执行任何内联脚本。
控制器内容:
class Csp::ScriptSrcController < ApplicationController
def none
set_csp "script-src 'none'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: script-src 'none'</h2>
<%= javascript_include_tag "//upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js" %>
<script>
alert("I will be disabled by csp");
</script>
试验的结果是,
我们可以通过 Chrome 的终端查看这个结果,
同时我们可以在 '/csp_report' 后台看到相关的 CSP 违规报告,如下所示,
加载 'http://localhost:3000/assets/application.js' 时触发的违规报告:
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/none",
"referrer"=>"",
"violated-directive"=>"script-src 'none'",
"original-policy"=>"script-src 'none'; report-uri /csp_report",
"blocked-uri"=>"http://localhost:3000/assets/application.js",
"status-code"=>200}}
我们注意到 "blocked-uri"=>"http://localhost:3000/assets/application.js", 即来自 "http://localhost:3000/assets/application.js" 的脚本资源被阻止加载。
加载 'http://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js' 时触发的违规报告:
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/none",
"referrer"=>"",
"violated-directive"=>"script-src 'none'",
"original-policy"=>"script-src 'none'; report-uri /csp_report",
"blocked-uri"=>"http://upcdn.b0.upaiyun.com",
"status-code"=>200}}
我们注意到 "blocked-uri"=>"http://upcdn.b0.upaiyun.com", 即来自 "upcdn.b0.upaiyun.com" 的脚本资源被阻止加载。
执行内联脚本时触发的违规报告,
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/none",
"referrer"=>"",
"violated-directive"=>"script-src 'none'",
"original-policy"=>"script-src 'none'; report-uri /csp_report",
"blocked-uri"=>"",
"status-code"=>200}}
script-src 'self'
'self' 表示允许加载同源的脚本,不允许加载不同源的脚本,不允许执行内联脚本。
控制器内容:
class Csp::ScriptSrcController < ApplicationController
def self
set_csp "script-src 'self'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: script-src 'self'</h2>
<%= javascript_include_tag "//upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js" %>
<script>
alert("I will be disabled by csp");
</script>
试验的结果是,
相关 CSP 违规报告如下所示,
加载 'http://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js' 时触发的违规报告:
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/self",
"referrer"=>"",
"violated-directive"=>"script-src 'self'",
"original-policy"=>"script-src 'self'; report-uri /csp_report",
"blocked-uri"=>"http://upcdn.b0.upaiyun.com",
"status-code"=>200}}
执行内联脚本时触发的违规报告,
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/self",
"referrer"=>"",
"violated-directive"=>"script-src 'self'",
"original-policy"=>"script-src 'self'; report-uri /csp_report",
"blocked-uri"=>"",
"status-code"=>200}}
script-src 'self' ${EXTERNAL-SCRIPT-LIST}
既可以加载同源脚本,也可以加载外部的与资源列表相匹配的脚本,但是不允许执行内联脚本。
控制器内容:
class Csp::ScriptSrcController < ApplicationController
def self_ext
set_csp "script-src 'self' http://example.com *.google.com"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: script-src 'self external'</h2>
<%= javascript_include_tag "//example.com/a1.js"%>
<%= javascript_include_tag "//example.com/a2.js"%>
<%= javascript_include_tag "//evil.example.com/b1.js"%>
<%= javascript_include_tag "//apis.google.com/d1.js"%>
<%= javascript_include_tag "//apis.google.com/d2.js"%>
<%= javascript_include_tag "//apis.google.com/d3.js"%>
<script>
alert("I will be disabled by csp");
</script>
我们可以通过 Chrome 的 console 查看试验结果,
试验结果:
'http://localhost:3000/assets/application.js' 匹配 'self', 浏览器会对正常请求此脚本。
'http://example.com/a1.js', 'http://example.com/a2.js' 匹配 'http://example.com', 浏览器会正常请求这些脚本。
'http://evil.example.com/b1.js', 不匹配任何 script-src 指令,浏览器拒绝请求此脚本
'https://plus.google.com/d1.js', 'https://plus.google.com/d2.js' 和 'https://plus.google.com/d3.js' 匹配 '*.google.com', 浏览器正常请求这些脚本。
内联脚本不匹配任何 script-src 指令,浏览器拒绝执行内联脚本。
CSP 违规报告:
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/self-ext",
"referrer"=>"",
"violated-directive"=>"script-src 'self' http://example.com *.google.com",
"original-policy"=>
"script-src 'self' http://example.com *.google.com; report-uri /csp_report",
"blocked-uri"=>"http://evil.example.com",
"status-code"=>200}}
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/self-ext",
"referrer"=>"",
"violated-directive"=>"script-src 'self' http://example.com *.google.com",
"original-policy"=>
"script-src 'self' http://example.com *.google.com; report-uri /csp_report",
"blocked-uri"=>"",
"status-code"=>200}}
script-src 'self' 'nonce-$RANDOM'
阻止内联脚本的执行能够很大程度上减轻 XSS 对站点的攻击,但是完全阻止执行内联脚本也是很困难或者说不现实的,所以 script-src 指令引入了 nonce 值,通过设置 nonce 值,我们 可以指定我们认为安全的内联脚本被允许执行。
控制器内容:
class Csp::ScriptSrcController < ApplicationController
def self_nonce
@rand = SecureRandom.hex
set_csp "script-src 'self' 'nonce-#{@rand}'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: script-src 'self-nonce'</h2>
<script class="blocked">
alert("I will be blocked by csp");
</script>
<script class="blocked" nonce="123">
alert("I will be blocked by csp");
</script>
<script class="allowed" nonce="<%= @rand %>">
alert("Allowed because nonce is valid.")
</script>
我们可以通过 Chrome 终端查看试验结果,
试验结果:
script.blocked 都会被阻止执行,第二个 script.blocked 虽然有 nonce 值,但是与服务器生成的 nonce 值不匹配,所以仍然会被阻止执行。
script.allowed 被允许执行
'http://localhost:3000/assets/application.js' 匹配 'self', 浏览器会对正常请求此脚本。
CSP 违规报告:
script.blocked 触发了两次,但是只生成了一份违规报告,
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/self-nonce",
"referrer"=>"",
"violated-directive"=>
"script-src 'self' 'nonce-ca36b262e9019c9e320ac0628f0fa727'",
"original-policy"=>
"script-src 'self' 'nonce-ca36b262e9019c9e320ac0628f0fa727'; report-uri /csp_report",
"blocked-uri"=>"",
"status-code"=>200}}
script-src 'unsafe-inline'
'unsafe-inline' 匹配内联脚本,即允许执行内联脚本,即使允许执行,也通过 unsafe 提醒开发者内联脚本是 不安全的。
与 'nonce' 可以指定某一块的内联脚本允许执行不同,'unsafe-inline' 可以让整个页面的内联脚本被允许执行。
控制器内容:
class Csp::ScriptSrcController < ApplicationController
def unsafe_inline
set_csp "script-src 'unsafe-inline'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: script-src 'unsafe-inline'</h2>
<script class="allowed">
alert("I be allowed by 'unsafe-inline'");
</script>
试验结果:
script.allowed 匹配 'unsafe-inline',被允许执行;
CSP 违规报告:
加载 'http://localhost:3000/assets/application.js' 触发违规报告,
"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/unsafe-inline",
"referrer"=>"",
"violated-directive"=>"script-src 'unsafe-inline'",
"original-policy"=>"script-src 'unsafe-inline'; report-uri /csp_report",
"blocked-uri"=>"http://localhost:3000/assets/application.js",
"status-code"=>200}}
default-src 'self'
如果没有指定 script-src, 浏览器会使用 default-src 'self' 进行匹配。
在试验六我们看到加载 'http://localhost:3000/assets/application.js' 会触发违规报告,
于是我们想通过设置 default-src 'self'
, 试验能否加载 'http://localhost:3000/assets/application.js'
我们先做第一个试验,只设置 default-src 'self'
, 而不设置 script-src
,
控制器内容:
class Csp::DefaultSrcController < ApplicationController
def self
set_csp "default-src 'self'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: default-src 'self'</h2>
<%= javascript_include_tag "//evil.example.com/b1.js"%>
<script class="blocked">
alert("I will be blocked by csp");
</script>
试验结果:
第二个试验,设置 default-src 'self'; script-src 'unsafe-inline''
,
控制器内容:
class Csp::MiscController < ApplicationController
def default_script_src
set_csp "default-src 'self'; script-src 'unsafe-inline'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: default-src 'self'; script-src 'unsafe-inline'</h2>
<script class="allowed">
alert("I be allowed by 'unsafe-inline'");
</script>
试验结果,
'http://localhost:3000/assets/application.js' 不被允许加载,即使设置了 default-src 'self'
, 这说明一旦设置了 script-src
的值,default-src
设置的值对 script-src
会失效;
script.allowed 允许执行;
script-src 'unsafe-eval'
一些侵入的文本可以把 eval(), new Function(), setTimeout([string], ...), and setInterval([string], ...) 等作为载体执行一些恶意任务,首先我们试验下 CSP 是否是默认阻止这些载体的,
控制器内容:
class Csp::ScriptSrcController < ApplicationController
def block_unsafe_eval
set_csp "script-src 'unsafe-iinline'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: script-src 'block unsafe-eval'</h2>
<script class="allowed">
eval("alert('XSS')");
</script>
<script class="allowed">
setTimeout("document.querySelector('a').style.display = 'none';", 10);
setInterval("clock()",50);
var myFunction = new Function("a", "b", "return a * b");
function clock() {
};
</script>
试验结果:
我们看到 eval, setTimeout, setInterval, new Function() 都被阻止执行了。
CSP 违规报告,
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"referrer"=>"",
"violated-directive"=>"script-src 'self' 'unsafe-inline'",
"original-policy"=>
"script-src 'self' 'unsafe-inline'; report-uri /csp_report",
"blocked-uri"=>"",
"source-file"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"line-number"=>16,
"column-number"=>2,
"status-code"=>200}}
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"referrer"=>"",
"violated-directive"=>"script-src 'self' 'unsafe-inline'",
"original-policy"=>
"script-src 'self' 'unsafe-inline'; report-uri /csp_report",
"blocked-uri"=>"",
"source-file"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"line-number"=>21,
"column-number"=>2,
"status-code"=>200}}
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"referrer"=>"",
"violated-directive"=>"script-src 'self' 'unsafe-inline'",
"original-policy"=>
"script-src 'self' 'unsafe-inline'; report-uri /csp_report",
"blocked-uri"=>"",
"source-file"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"line-number"=>23,
"column-number"=>19,
"status-code"=>200}}
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"referrer"=>"",
"violated-directive"=>"script-src 'self' 'unsafe-inline'",
"original-policy"=>
"script-src 'self' 'unsafe-inline'; report-uri /csp_report",
"blocked-uri"=>"",
"source-file"=>"http://localhost:3000/csp/script-src/block-unsafe-eval",
"line-number"=>22,
"column-number"=>2,
"status-code"=>200}}
报告中通过 'line-number', 'column-number' 帮我们指出了违规处的具体的行号和列号。
如果我们需要在脚本中执行 eval(), new Function(), setTimeout([string], ...), and setInterval([string], ...) 等方法,那么我们可以设置 script-src 'unsafe-eval''
,现在我们再做一个试验,
控制器内容:
class Csp::ScriptSrcController < ApplicationController
def unsafe_eval
set_csp "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
end
private
def set_csp csp_str
response.headers['Content-Security-Policy'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP: script-src 'unsafe-eval'</h2>
<script class="allowed">
eval("alert('XSS')");
</script>
<script class="allowed">
setTimeout("document.querySelector('a').style.display = 'none';", 10);
setInterval("clock()",50);
var myFunction = new Function("a", "b", "return a * b");
function clock() {
};
</script>
试验结果:
我们看到 eval(), new Function(), setTimeout([string], ...), and setInterval([string], ...) 等方法可以执行,后台也没有收到任何违规报告。
Content-Security-Policy-Report-Only: default-src 'self'
如果你对 CSP 不够熟悉,那么在开始应用 CSP 时很可能会出问题,并且造成站点的可用性大打折扣,前期我们可以使用 Content-Security-Policy-Report-Only 代替
Content-Security-Policy, 这样浏览器不会阻止哪些违规的资源,但是会将违规报告发到我们指定的接收地址,这样有助于我们监控哪些地方可能会受到 XSS攻击,并采取适当的 措施增强应用安全。现在我们来做一个试验来熟悉 Content-Security-Policy-Report-Only 的工作机制。
控制器内容:
class Csp::ReportOnlyController < ApplicationController
def index
set_csp_report_only "script-src 'self'"
end
private
def set_csp_report_only csp_str
response.headers['Content-Security-Policy-Report-Only'] = csp_str << "; report-uri /csp_report"
end
end
视图内容:
<h2>CSP-Report-Only: Index</h2>
<%= javascript_include_tag "//evil.example.com/a1.js"%>
<script>
alert("I should be blocked by csp");
</script>
试验结果:
我们可以到虽然浏览器识别出了违规的地方,但是并没有阻止它们继续执行,同时我们可以收到违规报告,
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/report-only",
"referrer"=>"",
"violated-directive"=>"script-src 'self'",
"original-policy"=>"script-src 'self'; report-uri /csp_report",
"blocked-uri"=>"http://evil.example.com",
"status-code"=>200}}
{"csp-report"=>
{"document-uri"=>"http://localhost:3000/csp/report-only",
"referrer"=>"",
"violated-directive"=>"script-src 'self'",
"original-policy"=>"script-src 'self'; report-uri /csp_report",
"blocked-uri"=>"",
"status-code"=>200}}
现在可以对预防 XSS 攻击做一个小结了,
raw
方法能够防止大部分的恶意代码侵入页面;CSRF 全称 Cross-Site Request Forgery, 意思是跨站点请求伪造。CSRF 的攻击原理比较好理解,比如我在站点 A 登录过了,然后我用同一个浏览器去访问站点 B 的某个页面, 很不幸,此页面上被某些攻击者添加了一些恶意代码或者链接,如果我在 A 上的会话 (session) 没有过期,那么这些恶意代码或者链接就可能以我的名义绕过认证去作一些恶意的操作等。CSRF 攻击的过程 大致如下图所示,
通过实际的动手操作,能够加深我们对概念的理解,所以我觉得很有必要自己动手模拟下 CSRF 的攻防,现在我们开始
试验环境:
使用 Pow 是为了可以使用类似于 webapp1.dev, webaap2.dev 的域名访问本地应用,详细的介绍可以参考: Pow 用户手册
首先建立两个站点,一个用作被攻击的站点:V, 另外一个用作发起攻击的站点 A, 为此我们将 websafe 这个项目分别拷贝为 'websafe-csrf-victim' 和 'websafe-csrf-attacker'
cd ~/workspace
cp -r websafe websafe-csrf-victim
cp -r websafe websafe-csrf-attacker
$ cd ~/.pow
ln -s ~/workspace/websafe-csrf-victim
ln -s ~/workspace/websafe-csrf-attacker
此时我们就已经建立好了两个用于试验的站点了。
站点 V(在此试验中可以通过 'websafe-csrf-victim.dev' 访问站点 V, 在后面的描述中,站点 V 即代表 'websafe-csrf-victim.dev'), 有一个地址:'/csrf/admin/admin_user/unsafe_create' 可以用于创建管理员, 访问此地址的请求目前是支持 GET 方法的,比如可以 GET 请求 'http://websafe-csrf-victim.dev/csrf/admin/admin_user/unsafe_create?admin%5Blogin%5D=admin002&admin%5Bpassword%5D=111111\' 来创建一个帐号为 'admin002', 密码为 '111111' 的管理员,虽然创建管理员之前会做用户认证,但是攻击者通过在站点 A(在此试验中可以通过 'websafe-csrf-attacker.dev' 访问站点 A, 在后面的描述中,站点 A 即代表 'websafe-csrf-attacker.dev') 上创建一个叫 hook 的页面来进行 CSRF 从而绕过站点 V 的验证来创建管理员。
hook 页面的 url: 'http://websafe-csrf-attacker.dev/csrf/attacker/hook'
hook 页面的内容:
<h2>攻击者在这个页面发起 CSRF 攻击</h2>
<p>下面这个 image 标签会创建一个叫 "admin002", 密码为 "111111" 的管理员</p>
<%= image_tag "http://websafe-csrf-victim.dev/csrf/admin/admin_user/unsafe_create?admin%5Blogin%5D=admin002&admin%5Bpassword%5D=111111" %>
站点 V 有一个初始的管理员,帐号是 'admin', 密码是 'verycomplex', 可以执行 bundle exec rake seed:admin_user
创建这个初始管理员。当 'admin' 成功登录站点 V 后,如果 'admin' 使用同一浏览器访问
'http://websafe-csrf-attacker.dev/csrf/attacker/hook' 后,站点 V 会在 'admin' 没有察觉的情况下增加一个新的管理员,
AdminUser.count #=> 2
AdminUser.last #=> #<AdminUser id: 7, login: "admin002", password: "111111", created_at: "2014-10-19 15:38:18", updated_at: "2014-10-19 15:38:18">
如果一个请求会改变资源的状态,比如下单请求,创建用户请求等,那么建议使用 POST 方法去完成这个请求,这样就不容易像上面的用例 1 一样被轻易的 CSRF, 所以我们在用例 2 中,将创建管理员的请求改为 POST 请求。
在此用例中,攻击者把 hook 页面的内容变成了,
<h2>攻击者在这个页面发起 CSRF 攻击</h2>
<p>下面这个 image 标签会创建一个叫 "admin002", 密码为 "111111" 的管理员</p>
<%= image_tag "http://websafe-csrf-victim.dev/csrf/admin/admin_user/create?admin%5Blogin%5D=admin002&admin%5Bpassword%5D=111111" %>
我们注意到 unsafe_create 方法被改为了 create 方法。 当然在真正的攻击中,攻击者不会告诉受害者这是一个 CSRF 页面,这里为了更清楚的演示试验过程,就加上了一些说明。我们看到攻击者使用一个新的链接 'http://websafe-csrf-victim.dev/csrf/admin/admin_user/create', 此链接的也是创建一个管理员,但是要求 POST 方法,其对应的路由和控制器内容如下,
路由内容,
namespace :csrf do
scope :admin do
post 'admin_user/create' => 'admin_user#create'
end
end
控制器内容,
class Csrf::AdminUserController < ApplicationController
skip_before_filter :verify_authenticity_token
before_filter :auth_admin
def create
admin_user = AdminUser.create admin_user_params
if admin_user
render text: 'ok'
else
redirect_to :new
end
end
private
def admin_user_params
params.required(:admin).permit(:login, :password)
end
def auth_admin
if current_admin.nil?
render text: '请登录'
end
end
def current_admin
@current_admin ||= AdminUser.where(id: session[:auid]).first
end
helper_method :current_admin
end
我们注意到控制器里的一段代码 skip_before_filter :verify_authenticity_token
, 这段代码可以让我们的 create 方法不会去验证浏览器请求应用时发送过来的一个防止 CSRF 的 token, 这样我们的试验才可以进行下去。按照用例 1 的步骤,V 的管理员 'admin' 登录站点 V 后,使用同一浏览器访问攻击者设置的 'hook' 页面并不会增加一个新的管理员,这说明在应用设计中,对会改变资源状态的请求设置其请求方法为 POST 方法,能够抵挡一些比较初级简陋的 CSRF 攻击,如果攻击者动点心思在 'hook' 页面上增加一些恶意脚本,那么站点 V 的防线又被攻破了。
增加了恶意代码的 hook 页面的内容,
<h2>攻击者在这个页面发起 CSRF 攻击</h2>
<p>下面这个 image 标签会创建一个叫 "admin002", 密码为 "111111" 的管理员</p>
<%= image_tag "http://websafe-csrf-victim.dev/csrf/admin/admin_user/create?admin%5Blogin%5D=admin002&admin%5Bpassword%5D=111111" %>
<img src="/images/harmless.jpg" width="400" height="400" onmouseover="
var f = document.createElement('form');
f.style.display = 'none';
this.parentNode.appendChild(f);
f.method = 'POST';
f.action = 'http://websafe-csrf-victim.dev/csrf/admin/admin_user/create?admin%5Blogin%5D=admin002&admin%5Bpassword%5D=111111';
f.submit();
return false;
" />
我们注意到攻击者在页面上增加了一张本身无害的图片,但是他给这张图片增加了一个 'onmouseover' 事件,通过此事件,当受害者把鼠标移动到图片上时,脚本会创建一个 form 表单然后在受害者毫无察觉的情况下 POST 请求 'http://websafe-csrf-victim.dev/csrf/admin/admin_user/create' 地址,最后创建一个管理员。
hook 页面在浏览器上的显示内容:
我们注意到在用例 2 中,控制器 'Csrf::AdminUserController' 里有一段代码:skip_before_filter :verify_authenticity_token
, 现在我们把这段代码注释掉,然后重复用例 2 的步骤,这时候我们发现站点 V 抛出一个 ActionController::InvalidAuthenticityToken 的异常,这个异常是什么意思呢?我们首先查看下 V 的布局文件 application.html.erb, 其内容为:
<!DOCTYPE html>
<html>
<head>
<title>Websafe</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
我们注意到 csrf_meta_tags
这段代码,它会帮我们在页面上嵌入一个存储 csrf-token 的 meta 标签,
比如:'',
如果我们使用 form_tag
, form_for
之类的表单辅助方法,rails 会自动帮我们生成一个包含 csrf-token 的隐藏 input 标签比如,
<div style="display:none">
<input name="authenticity_token" type="hidden" value="OI3TMyEUCXA00tKNqjovRVf+6iHX49jdlne4S76H1qU=">
</div>
上面的 csrf-token meta 标签用于 jquery-ujs, 这样做 ajax post 请求时,jquery-ujs 会自动在请求头里加入 csrf-token meta 标签的内容,比如,
"authenticity_token"=>"OI3TMyEUCXA00tKNqjovRVf+6iHX49jdlne4S76H1qU="
而 form 表单里的 authenticity_token input 标签会在表单提交时将 authenticity_token 的值发送到服务器以供验证,这样只要是站内的请求就不用担心 不能通过 csrf-token 认证了。
这个 token 是随机生成的,并且此 token 生成后会被保存到用户的会话中,即 session[:csrf_token], 而攻击者几乎是没有可能通过访问同一个页面来获得这个 token 的,因为 rails 会根据攻击者的会话生成一个不同的 token, 所以我们可以认为攻击者不知道这个 token, 在这种情况下,攻击者即使仿照了一个请求, 也没有办法通过 rails 的 csrf-token 认证了,这样就预防了 CSRF 攻击了。
我们总结下在 rails 中怎么预防 CSRF 攻击,
Session Fixation 的攻击过程大致如下,
假设攻击者对站点 A 进行攻击
攻击者通过加载站点 A 的登录页面创建了一个合法的 session id, 然后从 A 的响应的 cookie 里取出此 session id 用于后面的操作;
攻击者通过一定的方法保持这个 session id 不过期,比如不断地访问站点 A;
同时攻击者可能通过某种方式让受害者的浏览器去使用这个 session id, 比如攻击这在站点 A 的某个页面注入了恶意代码,
<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>
很显然这里用到了 XSS 攻击,当受害者访问到这个页面时,她浏览器的 session id 会被强制地使用被攻击者掌握的 session id 了;
受害者登录站点 A, 这时候攻击者和受害者就共享同一个 session id 了,这就意味着攻击者可以使用和受害者一样的身份使用站点 A 了,并且受害者对攻击没有丝毫察觉;
在 rails 中预防 Session Fixation 攻击要注意以下几点,
1.做好预防 XSS 攻击的工作,因为 Session Fixation 攻击的第三步就用到了 XSS, 而怎么预防 XSS 攻击我们在前面已经说了很多了;
2.使用 reset_session
方法, reset_session
方法会创建一个新的 session,这样攻击者持有的那个 session id 就失效了,所以每次用户登录成功后,我们先执行 reset_session
, 然后再填充新的用户 session, 如下所示:
class Csrf::SessionController < ApplicationController
def create
@admin = authenticate_admin
if @admin
reset_session
session[:auid] = @admin.id
render text: '登录成功'
else
redirect_to :new
end
end
private
def authenticate_admin
admin = AdminUser.where(login: params[:login]).first
admin and admin.password == params[:password] ? admin : nil
end
end
根据我的理解做好第 1,第 2 点就能很好的预防 Session Fixation 攻击了,但是第 3 点也是非常有意义的, 比如,如果频繁的发现用户通过不同的 ip 或者浏览器登录,这预示着可能有 Session Fixation 攻击了。
这是一种使用自动程序暴力破解的攻击手段,假设某个站点 A 有一个登录页面,当用户填写了错误的登录名后,页面上会显示 '用户名不存在的',那么攻击者 就很可能使用这个登录页面获得一批有效的用户名,然后配合密码字典等破解用户的帐号。我们预防 Brute Force 攻击可以从两个方面着手,
阻止自动程序的暴力请求,比如我们可以在登录页面或者其他需要识别自动程序的页面上增加验证码;
模糊出错信息,比如如果用户登录失败,无论是因为用户名错误还是密码错误,我们都可以显示为'用户名或者密码错误';