参考
- 白帽子讲web安全(书)
- XSS前端防火墙
- JavaScript防http劫持与XSS
- 内容安全策略(Content Security Policy,CSP)介绍
- 浅谈CSRF攻击方式
安全世界观
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攻击主动进行检测和拦截。
内联事件
例如在页面中需要用户输入图片的地址如
,但攻击者们可以通过引号提前关闭属性,并添加一个极易触发的内联事件如
。
防范思路
对于内联事件,还是遵循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开头的事件外,还有一些特殊形式,其中使用最为广泛和常见,这种就需要单独对待。
document.addEventListener(eventName.substr(2), function(e) {
//... 其他拦截策略
var element = e.target;
// 扫描 的脚本
if (element.tagName == 'A' && element.protocol == 'javascript:') {
// ...
}
});
对于一些常用的事件如鼠标移动会非常频繁的调用,因此有必要考虑性能方面的优化。
一般来说内联事件在代码运行过程中并不会改变,因此对某个元素的特定事件,扫描一次后置个标志位,之后再次执行的话检测标志位后可以考虑是否直接跳过。
可疑模块
XSS最简单和常见的方法就是动态加载个站外的脚本,模拟代码如下:
防范思路
在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) {
// 匹配到
// 接收窗口
服务器端安全
注入攻击
注入攻击是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事先会在系统中定义不同的角色,不同的角色拥有不同的权限,所有用户会被分配到不同的角色,一个用户可以拥有多个角色。在系统验证权限时,只需要验证用户所属的角色,就可以根据角色所拥有的权限进行授权了。