前端隐秘角落 - web安全的演变历程

avatar
公众号:转转技术

这是一个平行时空,在这里,朱朝阳还是有写日记的习惯,不是在日记本,却是在qq中记录。这一天,在他身上发生了一个奇怪的事情:当他像往常一样登录自己的qq空间,却发现最新一条的说说是一个六峰山旅游的广告,但这并不是他写的……这条说说是怎么来的?针对这个问题,他去请教了自己的程序员舅舅张东升。

张东升摸了摸自己的秃头:”你这是被csrf攻击了!“

“接下来我带你来走进web发展史,了解下web安全一路走来的演变历程...”

这篇文章主要围绕web页面安全展开分析:从cookie的源起,到同源策略的出现,再到跨域的兴起,从而引申出csrf攻击。

浏览器的诞生:

追溯到1989年3月12日,万维网 (WWW, World Wide Web) 诞生的日子,蒂姆·伯纳斯-李爵士设计发明了第一个浏览器,架设了第一个 web 服务器 info.cern.ch。(详细的介绍在之前文章【http请回答】中有系统的介绍过,可翻阅)而浏览器web的设计之初是知识的共享,所以开放是基本理念。

Cookie

源起:

早期互联网只是用于简单的浏览文档信息、查看门户网站等等,并没有交互这个说法。随着互联网快速发展,交互式web开始兴起,如用户登录,购买商品,各种论坛等。怎么记录用户的操作行为呢?

于是出现了 隐藏域 , 把用户上一次操作记录放在form表单的input中,通过表单提交记录用户操作行为。但是每次都得创建隐藏域而且得赋值太麻烦,且易出错。

// 隐藏域写法
<input type="hidden" name="field_name" value="value">

Cookie最早是网景公司的前雇员Lou Montulli(卢-蒙特利)在1993年3月的发明,并于1994年将“cookies”的概念应用于网络通信。Cookie的出现是为了优化交互式web,同时也规避了隐藏域操作复杂的问题。

定义:

Cookie是由服务器发给客户端的特殊信息,而这些信息以文本文件的方式存放在客户端,然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息,用于服务器记录客户端的状态。

安全性:

cookie在行使自身使命的同时,也存在了安全隐患。

设想一下,你登录了银行系统,并把登录信息、账号密码存在了cookie中,cookie信息共享,大家的账号和密码也共享了,奋斗了一辈子的财富,被别人轻易的转走,将是多么可怕的事情。所以,为了维护互联网的隐私和数据安全,必须制定一定规则。

浏览器安全的基石是"同源政策"(same-origin policy) --- 阮一峰

同源策略

定义:

源是主机,协议,端口名的一个三元组。1995年,同源政策(SOP)由 Netscape 公司引入浏览器,规定了不同域之间访问的策略 协议://域名:端口。同domain(或ip),同端口,同协议视为同一个域,一个域内的脚本只能读写本域内的资源,**对于其它域的资源,它没有禁止脚本的执行,而是禁止读取HTTP回复,**这种安全限制称为同源策略。

为什么同源限定是三元组?不是二元组?也不是四元组?

如果将这个地址组成,比喻为一次快递小哥为合租的你服务的一次行为,那么 协议 则来区分是送快递还是点外卖行为;域名 则是你的地址,端口 则是你的房间号, 请求资源地址 则是你需要点的外卖内容,快递小哥根据你的行为和你的地址,房间号来完成这次任务,就算你临时替换了外卖内容,也不影响他完成这次输送任务。

限制范围

  • 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。
  • 无法接触非同源网页的 DOM。
  • 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。

同源策略提升了Web前端的安全性,但阻碍了Web拓展的灵活性。**若把html、js、css、flash,image等静态文件全部布置在一台服务器上,会加大这台服务器的压力,甚至威胁到web服务的可用性。**因此,在遵循同源策略的基础上,在安全性和可用性之间选择了一个平衡点。

tips:以下标签是允许跨域加载资源:
* <img src=XXX>
* <link href=XXX>
* <script src=XXX>
* <iframe>

在日益发展的互联网时代,在前端资源和后端数据请求趋向专业化分离的背景下,需要程序员基于同源策略下,打破常规,实现数据请求加载。这就要进行 跨域

跨域

什么是跨域?

由于同源策略的限制,当一个请求 url 的协议、域名、端口三者之间任意一个与当前页面 url 不同即为跨域,这是浏览器安全限制下的产物。

问:跨域只存在于浏览器吗?

答: 是的,这是浏览器的同源策略下的产物,上文中提到,同源策略没有禁止脚本的执行,而是禁止读取HTTP回复。跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。所以,服务端不存在,服务器间的调用,一般是为了获取接口数据的,单一功能。app也相对比较自由,可自行设置白名单等限定条件。

跨域方式

  • 1、 通过jsonp跨域(get请求)
  • 2、 document.domain + iframe 跨域(主域相同,子域不同, 设置document.domain为基础主域)
  • 3、 location.hash + iframe 跨域(不同域之间利用iframe的location.hash传值)
  • 4、 window.name + iframe 跨域(name值[2MB]在不同的页面加载后依旧存在)
  • 5、 postMessage 跨域 (跨文档通信)
  • 6、 跨域资源共享(CORS)(W3C 标准,跨源ajax请求的根本解决方式)
  • 7、 nodejs中间件代理跨域(服务器向服务器请求)
  • 8、 nginx代理跨域(类似于node中间键,使用中转nginx服务器来转发请求)
  • 9、 WebSocket协议跨域 (利用双向通信协议)

这里主要分析下,jsonp 和 cors 跨域方式

JSONP 跨域

  • JSONP是JSON with Padding的略称。它是一个非官方的协议.

上文讲到,<script> 标签是没有被同源策略限制的,jsonp利用这个漏洞,向网页中动态插入script标签,向服务端发送请求,并允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样前端可以随意定制自己的函数来自动处理返回数据了。

// index.html
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
// 调用
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'HowAreYou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // HowAreYou
  console.log(callback) // show
  res.end(`${callback}('IAmFine')`)
})
app.listen(3000)

jsonp 属于非同源策略(跨域请求)需要服务器支持。又因为script标签仅支持get,所以jsonp也只能对get请求跨域,不支持post。因此jsonp 适合获取资源(只读)

另外,callback参数的传入是在后端进行了一次拼接,存在注入的可能,如果设计不当,是有可能出现安全风险的。

CORS 跨域资源共享

为了更安全的跨域资源访问,于是出现了CORS (Cross-Origin Resource Sharing 跨域资源共享)。它支持所有的请求,包含GET、POST、OPTOIN、PUT、DELETE等,通过对请求头中Origin设置,进而实现跨域请求。

浏览器将CORS请求分成两类: 简单请求和非简单请求

只要同时满足以下两大条件,就属于简单请求。

条件一:请求方法是HEAD/GET/POST方法之一
条件二:HTTP头部信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

非简单请求会发出一次预检测请求,进行跨域校验,检测完毕才可真正发送请求

CORS 实现跨域:

CORS 需要浏览器和后端同时支持,浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就可以实现跨域。

  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS
// 设置支持跨域的源,设置*为任意源,避免使用,必要时可添加多个源
header("Access-Control-Allow-Origin: http://m.zhuanzhuan.com"); 
// 允许携带cookie信息,禁止携带可设置false
header("Access-Control-Allow-Credentials:true"); 
  • 前端设置 withCredentials 字段
// 前端设置是否带cookie
var xhr = new XMLHttpRequest(); 
xhr.withCredentials = true;

安全性

CORS 极大的增强了请求的安全性,但还是会有安全隐患,比如:服务端设置 Access-Control-Allow-Origin = * ;同源策略下对于部分标签的开放非同源限定;浏览器对跨域请求不接收响应也并不拦截请求;在这个背景下,出现了csrf攻击...

CSRF 跨站请求伪造

CSRF(Cross-site request forgery)跨站请求伪造,也被称为:one click attack/session riding。2000年被国外的安全人员提出,2006年开始被国内关注,2008年国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI......而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。

攻击者在用户已经登录目标网站之后,诱使用户访问一个攻击页面,利用目标网站对用户的信任,以用户身份在攻击页面对目标网站发起伪造用户操作的请求,达到攻击目的。

接下来图解下开头的csrf攻击事件

由此推断出 csrf的攻击特点:

csrf 攻击特点

  • 攻击者借助于受害者的cookie等浏览器信息骗取服务器信任,攻击者拿不到cookie。
  • 由于浏览器同源策略,攻击者拿不到响应结果,只是发起请求。
  • csrf攻击主要是发送修改数据请求。

常见的 csrf 攻击

  • 投票系统,自动投票。
  • 游戏中被迫送礼物。
  • 交易平台,自动收货等

受到csrf攻击,或是因为一个无意的点击,也或是一次随意的浏览,因此,针对以上分析,得出以下防御策略:

CSRF防御

  • 合理的cookie存储

登录信息及过期时间的设定考量,避免攻击方获取后可长时间攻击

  • Get 请求不对数据进行修改;

get 请求只做数据的查询,不做数据增删改的操作,减小被攻击的损失

  • 请求头设置 refere, 验证 HTTP Referer 字段;

页面中请求方的域名限定,杜绝第三方网站的访问

  • 在请求地址中添加 本次操作的唯一标识(sessionId) 并验证;

方案一:sessionId 可以是页面中生成图片验证码、短信验证码,将得到的sessionId传入请求中,并验证,这个方法操作成本有点高,会阻碍用户行为。
方案二:先通过一个请求实时获取sessionId,再将这个值传入请求中并验证。
两种方案都是同样的思路,方案二适用场景要更加广泛些

  • 在 HTTP 头中自定义属性并验证。

属性值需要同时满足以下两点限定条件:
1、前后端要协定,保持一致;
2、每个用户有唯一的值

总结

web安全的历史长河中,从来都不是风平浪静的,每一个技术的出现都有它的使命,每一个技术的陨落也都有它的必然。浏览器诞生,开启了互联网的篇章,到如今互联网功能日益丰富,缺点也暴露的很明显,信息全球化的同时,也要考虑网络中的信息安全与隐私。

参考文章: