译者:写bug
https://segmentfault.com/a/1190000015597029结语
布吕马这三个字就像几块寡廉鲜耻一样包在每三个前端开发者身上,不论你在组织工作上或者复试中不可避免会碰到那个难题。为的是应对复试,我每天都就行了背几个计划,也不晓得为何要此种干,再说面完就可以扔了,我想组织工作上也不能加进那么多支离破碎的计划。
到了真正组织工作,开发环境有webpack-dev-server搞掂,上架了服务器端的元老们也会配好,配了甚么我无论,再说不能布吕马是了。好日子也就那么混过去了,终于有一天,我觉得不能再继续此种混下去了,我一定要全盘比如说那个小东西!于是就有了这首诗。
要掌控布吕马,首先要晓得为何会有布吕马那个难题出现
的确,他们此种搬砖工人是为的是稚鳕嘛,回去的调个USB告诉我布吕马了,此种妨碍他们随心所欲搬砖的事真头痛!为何会布吕马?是谁在搞事?为的是找到那个难题的主谋,请点选浏览器的相混思路[1]。
那么非官方的小东西真晦涩,说实话,最少你晓得了,因为应用程序的相混思路导致了布吕马,是应用程序在搞事。
因此,应用程序为何要搞事?是不该给萨德基他们过?对于此种的反问,应用程序甩锅道:“相混思路管制了从同三个源读取的文档格式或JAVA如何与来自另三个源的资源进行可视化。这是三个用于隔绝潜在性蓄意文档的重要安全可靠监督机制。”
那么非官方的话术真晦涩,说实话,最少你晓得了,似乎这是个安全可靠监督机制。因此,到底为何需要此种的安全可靠监督机制?此种的安全可靠监督机制解决了甚么难题?喽,让他们继续研究下去。
没有相混思路管制的三大脆弱情景
据我了解,应用程序从三个方面去做那个相混思路的,一是特别针对USB的允诺,并有特别针对Dom的查阅。换言之一下没有此种的管制上述两种动作有甚么脆弱。
没有相混思路管制的USB允诺
有三个小小的小东西叫cookie大家应该晓得,一般用来处理登录等情景,目的是让服务器端晓得谁发出的这次允诺。
如果你允诺了USB进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发允诺的时候,应用程序会自动将cookie附加在HTTP允诺的头字段Cookie中,服务器端就能晓得那个用户已经登录过了。
晓得那个之后,他们来看情景:
1.你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车小东西那么少,不行,还得买多点。
2.你在看有甚么小东西买的过程中,你的好基友发给你一个链接www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着www.nidongde.com,谁知那个网站暗地里做了些不可描述的事!由于没有相混思路的管制,它向www.maimaimai.com发起了允诺!聪明的你一定想到上面的话“服务器端验证通过后会在响应头加入Set-Cookie字段,然后下次再发允诺的时候,应用程序会自动将cookie附加在HTTP允诺的头字段Cookie中”,此种一来,那个不法网站就相当于登录了你的账号,可以为所欲为的是!如果这不是三个买买买账号,而是你的银行账号,那……
这是传说中的CSRF攻击浅谈CSRF攻击方式[2]。
看了这波CSRF攻击我在想,即使有了相混思路管制,但cookie是明文的,还不是一样能拿下来。
于是我看了一些cookie相关的文章聊一聊 cookie[3]、Cookie/Session的监督机制与安全[4]Web安全可靠测试之XSS[5];设置secure,则保证在https的加密通信中传输以防截获。
没有相混思路管制的Dom查阅
1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
2.睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,那个钓鱼网站做了甚么呢?
// HTML <iframe name=“yinhang” src=“www.yinhang.com”></iframe> // JS // 由于没有相混思路的管制,钓鱼网站可以直接拿到别的网站的Dom const iframe = window.frames[yinhang] const node = iframe.document.getElementById(你输入账号密码的Input) console.log(`拿到了那个 ${node},我还拿不到你刚刚输入的账号密码吗`)由此他们晓得,相混思路的确能规避一些脆弱,不是说有了相混思路就安全可靠,只是说相混思路是一种应用程序最基本的安全可靠监督机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。
布吕马正确的打开方式
经过对相混思路的了解,他们应该要消除对应用程序的误解,相混思路是应用程序做的一件好事,是用来防御来自邪门歪道的攻击,但总不能为的是不让坏人进门而把全部人都拒之门外吧。没错,他们此种正人君子只要打开方式正确,就应该可以布吕马。
下面将三个个演示正确打开方式,但在此之前,有些准备组织工作要做。为的是本地演示布吕马,他们需要:
1.就行了跑起一份前端代码(以下前端是就行了跑起来的vue),地址是http://localhost:9099。
2.就行了跑起一份后端代码(以下后端是就行了跑起来的node koa2),地址是http://localhost:9971。
相混思路管制下USB允诺的正确打开方式
1.JSONP
后端写个小USB
// 处理成功失败返回格式的工具 const {successBody} = require(../utli) class CrossDomain { static async jsonp (ctx) { // 前端传过来的参数 const query = ctx.request.query // 设置三个cookies ctx.cookies.set(tokenId, 1) // query.cb是前后端约定的方法名字,其实是后端返回三个直接执行的方法给前端,由于前端是用script标签发起的允诺,因此返回了那个方法后相当于立马执行,并且把要返回的数据放在方法的参数里。 ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, success))})` } } module.exports = CrossDomain简单版前端
<!DOCTYPE html> <html> <head> <meta charset=“utf-8”> </head> <body> <script type=text/javascript> // 后端返回直接执行的方法,相当于执行那个方法,由于后端把返回的数据放在方法的参数里,因此这里能拿到res。 window.jsonpCb = function (res) { console.log(res) }</script> <script src=http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb type=text/javascript></script> </body> </html>简单封装一下前端那个套路
/** * JSONP允诺工具 * @param url 允诺的地址 *@param data 允诺的参数 * @returns {Promise<any>} */ const request = ({url, data}) => { return new Promise((resolve, reject) => { // 处理传参成xx=yy&aa=bb的形式 const handleData = (data) => { const keys = Object.keys(data) const keysLen = keys.length return keys.reduce((pre, cur, index) => { const value = data[cur] constflag = index !== keysLen –1 ? & : return `${pre}${cur}=${value}${flag}` }, ) } // 动态创建script标签 const script = document.createElement(script) window.jsonpCb = (res) => { document.body.removeChild(script) delete window.jsonpCb resolve(res) } script.src = `${url}?${handleData(data)}&cb=jsonpCb` document.body.appendChild(script) }) } // 使用方式 request({ url: http://localhost:9871/api/jsonp, data: { // 传参 msg: helloJsonp } }).then(res => { console.log(res) })2.空iframe加form细心的朋友可能发现,JSONP只能发GET允诺,因为本质上script读取资源是GET,那么如果要发POST允诺怎么办呢?
后端写个小USB
// 处理成功失败返回格式的工具 const {successBody} = require(../utli) class CrossDomain { static async iframePost (ctx) { let postData = ctx.request.body console.log(postData) ctx.body = successBody({postData: postData}, success) } } module.exports = CrossDomain前端
const requestPost = ({url, data}) => { // 首先创建三个用来发送数据的iframe. const iframe = document.createElement(iframe) iframe.name = iframePost iframe.style.display = none document.body.appendChild(iframe)const form = document.createElement(form) const node = document.createElement(input) // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话. iframe.addEventListener(load, function () { console.log(post success) }) form.action = url// 在指定的iframe中执行form form.target = iframe.name form.method = post for (let name indata) { node.name = name node.value = data[name].toString() form.appendChild(node.cloneNode()) }// 表单元素需要添加到主文档格式中. form.style.display = none document.body.appendChild(form) form.submit()// 表单提交后,就可以删除那个表单,不影响下次的数据发送. document.body.removeChild(form) } // 使用方式 requestPost({ url: http://localhost:9871/api/iframePost, data: { msg: helloIframePost } })3.CORS
CORS是三个W3C标准,全称是”布吕马资源共享”(Cross-origin resource sharing)布吕马资源共享 CORS 详解[6]。看名字就晓得这是处理布吕马难题的标准做法。CORS有两种允诺,简单允诺和非简单允诺。
这里引用上面链接阮一峰老师的文章说明一下简单允诺和非简单允诺。应用程序将CORS允诺分成两类:简单允诺(simple request)和非简单允诺(not-so-simple request)。
只要同时满足以下三大条件,就属于简单允诺。(1) 允诺方法是以下三种方法之一:
HEADGETPOST(2)HTTP的头信息不超出以下几种字段:
AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain1.简单允诺后端
// 处理成功失败返回格式的工具 const {successBody} = require(../utli) class CrossDomain { static async cors (ctx) { const query = ctx.request.query // *时cookie不能在http允诺中带上 ctx.set(Access-Control-Allow-Origin, *) ctx.cookies.set(tokenId, 2) ctx.body = successBody({msg: query.msg}, success) } } module.exports = CrossDomain前端甚么也不用干,是正常发允诺就可以,如果需要带cookie的话,前后端都要设置一下,下面那个非简单允诺例子会看到。
fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res => { console.log(res) })2.非简单允诺非简单允诺会发出一次预检测允诺,返回码是204,预检测通过才会真正发出允诺,这才返回200。这里通过前端发允诺的时候增加三个额外的headers来触发非简单允诺。
后端
// 处理成功失败返回格式的工具 const{successBody} = require(../utli) class CrossDomain { static async cors (ctx) { const query = ctx.request.query // 如果需要http允诺中带上cookie,需要前后端都设置credentials,且后端设置指定的origin ctx.set(Access-Control-Allow-Origin, http://localhost:9099) ctx.set(Access-Control-Allow-Credentials, true) // 非简单允诺的CORS允诺,会在正式通信之前,增加一次HTTP查阅允诺,称为”预检”允诺(preflight) // 此种情况下除了设置origin,还需要设置Access-Control-Request-Method以及Access-Control-Request-Headers ctx.set(Access-Control-Request-Method, PUT,POST,GET,DELETE,OPTIONS) ctx.set(Access-Control-Allow-Headers, Origin, X-Requested-With, Content-Type, Accept, t) ctx.cookies.set(tokenId, 2) ctx.body = successBody({msg: query.msg}, success) } } module.exports = CrossDomain三个USB就要写那么多代码,如果想所有USB都统一处理,有甚么更优雅的方式呢?见下面的koa2-cors。
const path = require(path) const Koa = require(koa) const koaStatic = require(koa-static) const bodyParser = require(koa-bodyparser) const router = require(./router) const cors = require(koa2-cors) const app = new Koa() const port = 9871 app.use(bodyParser()) // 处理静态资源 这里是前端build好之后的目录 app.use(koaStatic( path.resolve(__dirname, ../dist) )) // 处理corsapp.use(cors({origin: function (ctx) { return http://localhost:9099 }, credentials: true, allowMethods: [GET, POST, DELETE], allowHeaders: [t, Content-Type] })) // 路由app.use(router.routes()).use(router.allowedMethods())// 监听端口 app.listen(9871) console.log(`[demo] start-quick is starting at port ${port}`)前端
fetch(`http://localhost:9871/api/cors?msg=helloCors`, { // 需要带上cookie credentials: include, // 这里添加额外的headers来触发非简单请求 headers: { t: extra headers } }).then(res => { console.log(res) })4.代理想一下,如果他们允诺的时候还是用前端的域名,然后有个小东西帮他们把那个允诺转发到真正的后端域名上,不就避免布吕马了吗?这时候,Nginx出场了。Nginx配置
server{ # 监听9099端口 listen 9099; # 域名是localhostserver_name localhost;#凡是localhost:9099/api那个样子的,都转发到真正的服务器端地址http://localhost:9871 location ^~ /api { proxy_pass http://localhost:9871; } }前端就不用干甚么事了,除了写USB,也没后端甚么事了
// 允诺的时候直接用回前端这边的域名http://localhost:9099,这就不能布吕马,然后Nginx监听到凡是localhost:9099/api那个样子的,都转发到真正的服务器端地址http://localhost:9871 fetch(http://localhost:9099/api/iframePost, { method: POST, headers: { Accept: application/json, Content-Type: application/json }, body: JSON.stringify({ msg: helloIframePost }) })Nginx转发的方式似乎很方便!但此种使用也是看情景的,如果后端接
相混思路管制下Dom查阅的正确打开方式
1.postMessagewindow.postMessage() 是HTML5的三个USB,专注实现不同窗口不同页面的布吕马通讯。为的是演示方便,他们将hosts改一下:127.0.0.1 crossDomain.com,现在访问域名crossDomain.com就等于访问127.0.0.1。
这里是http://localhost:9099/#/crossDomain,发消息方
<template> <div> <button @click=“postMessage”>给http://crossDomain.com:9099发消息</button> <iframe name=“crossDomainIframe” src=“http://crossdomain.com:9099”></iframe> </div> </template> <script>export default{ mounted () {window.addEventListener(message, (e) => { if (e.origin === http://crossdomain.com:9099) { console.log(e.data) } }) },methods: { // 向http://crossdomain.com:9099发消息 postMessage () { const iframe = window.frames[crossDomainIframe] iframe.postMessage(我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom,http://crossdomain.com:9099) } } }</script>这里是
http://crossdomain.com:9099,接收消息方<template> <div> 我是http://crossdomain.com:9099 </div> </template> <script>export default{ mounted () {window.addEventListener(message, (e) => { if (e.origin === http://localhost:9099) { // http://localhost:9099发来的信息 console.log(e.data) // e.source可以是回信的对象,其实是http://localhost:9099窗口对象(window)的引用 // e.origin可以作为targetOrigin e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这是你想晓得的结果:${document.getElementById(app) ? 有id为app的Dom : 没有id为app的Dom}`, e.origin); } }) } }</script>结果可以看到:
2.document.domain
此种方式只适合主域名相同,但子域名不同的iframe布吕马。比如主域名是
http://crossdomain.com:9099,子域名是
http://child.crossdomain.com:9099,此种情况下给三个页面指定一下document.domain即document.domain = crossdomain.com就可以访问各自的window对象了。3.canvas操作图片的布吕马难题那个应该是三个比较冷门的布吕马难题,张大神已经写过了我就不再班门弄斧了解决canvas图片getImageData,toDataURL布吕马难题[7]
最后
希望看完这首诗之后,再有人问布吕马的难题,你可以嘴角微微上扬,冷笑一声:“不要再问我布吕马的难题了。” 扬长而去。
如果学到了可以点在看让更多的小伙伴看到哦 。
参考资料
[1]应用程序的相混思路:
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy[2]浅谈CSRF攻击方式:
http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html[3]聊一聊 cookie:
https://segmentfault.com/a/1190000004556040#articleHeader6[4]Cookie/Session的监督机制与安全可靠:
https://harttle.land/2015/08/10/cookie-session.html[5]Web安全可靠测试之XSS:
https://www.cnblogs.com/TankXiao/archive/2012/03/21/2337194.html[6]布吕马资源共享 CORS 详解:
http://www.ruanyifeng.com/blog/2016/04/cors.html[7]解决canvas图片getImageData,toDataURL布吕马难题:
https://www.zhangxinxu.com/wordpress/2018/02/crossorigin-canvas-getimagedata-cors/