JavaScript 安全知识: CORS 简明教程

概述

  • 浏览器会强制 同源策略 以禁止不同源的网站获得响应;
  • 『同源策略』不会阻止对其他来源的请求,但是会禁用 JavaScript 对响应内容的读取。
    -CORS 标头允许访问跨域响应。
    -CORS 与凭证一起需要谨慎。
    -CORS 是浏览器强制执行的策略。 其他应用程序不受此影响。

我们的例子

在这里我只展示处理请求的代码,然而你依旧可以在 GitHub 上看到完整实例

让我们从一段示例中起航吧。我们有这个一个公开的 API 来访问我们的网站 http://good.com:8000/public:\

app.get('/public', function(req, res) {
  res.send(JSON.stringify({
    message: 'This is public'
  }));
})

我们还有一个简单的登录功能,在用户输入了一个 secret时设置一个 cookie、确认他们的身份认证 :\

app.post('/login', function(req, res) {
  if(req.body.password === 'secret') {
    req.session.loggedIn = true
    res.send('You are now logged in!')
  } else {
    res.send('Wrong password.')
  }
})

我们用下面的方式来保护我们的私人数据,当用户访问 /private 时.\

app.get('/private', function(req, res) {
  if(req.session.loggedIn === true) {
    res.send(JSON.stringify({
      message: 'THIS IS PRIVATE'
    }))
  } else {
    res.send(JSON.stringify({
      message: 'Please login first'
    }))
  }
})

通过 AJAX 从其他域请求我们的 API

就目前来看我们的 API 并没有设计完善,不过我们已经允许通过访问/public 来获取数据了。假设,我们的 API 运行在 good.com:300/public 而我们的客户端托管在 thirdparty.com,那么客户端可能运行的代码如下:\

fetch('http://good.com:3000/public')
  .then(response => response.text())
  .then((result) => {
    document.body.textContent = result
  })

咦它们并没有在我们的浏览器上正常运行!

让我们看下 http://thirdparty.com 的网络选项卡:

网络请求成功

这个请求时成功的,但是结果不可用。可以在 JavaScript 控制台找到原因:

The console shows that a missing CORS header causes the problem

啊哈!我们缺少了 Access-Control-Allow-Origin 标题。但是为什么我们需要它,还有它有什么用?

同源策略

我们在 JavaScript 中无法获取响应的原因就是同源策略。这个策略旨在确保一个网站不能从另一个网站读取请求的结果,并且被浏览器强制执行。

看场景:如果你在浏览网站 example.org,你不会希望该网站向你的银行网站发起请求,并且获取你的账户余额以及交易信息。

同源策略正是用于阻止该事发生。

“源”包含了一下信息

  • 协议 (例如 http)
  • 域名 (例如 example.com)
  • 端口 (例如 8000)

因此 http://example.org 和 http://www.example.org 以及 https://example.org 其实是三个不同的源。

注意 CSRF

这个需要提醒你关注一下这个类攻击,叫做跨站请求伪造 。这种情况就无法通过同源策略来减少攻击。

在一个 CSRF 攻击中,攻击者在后台向第三方页面发起请求,例如向你的银行网站发起一个 POST 请求。如果你与银行之间有有效的 session,任意网站都可以在后台发起请求,而且会被执行,除非你的银行网站使用针对 CSRF 的抵御策略。

注意尽管同源策略已经生效,在我们的例子中,从 thirdparty.com 发起请求到 good.com 已经执行成功 - 我们仅仅是无法访问结果而已。对于 CSRF,我们并不需要什么结果...

举个例子,一个 API 允许通过一个发送一封邮件的 POST 请求来发送邮件,如果我们给它正确的数据 - 攻击者根本不关心返回结果,他们关心的是邮件的发送,而不管 API 的响应。

为我们的公共 API 开启 CORS

现在我们 就是 希望允许第三方网站(例如 thirdparty.com)上的 JavaScript 访问我们的 API 响应。我们可以根据错误提示这样做,开启 CORS 头部信息:\

app.get('/public', function(req, res) {
  res.set('Access-Control-Allow-Origin', '*')
  res.send(...)
})

这儿我们设置 Access-Control-Allow-Origin 头部信息为 * ,这就意味着:任意域名可以在浏览器中访问这个 URL 和响应:

The response is available once we set the CORS header

非简单请求和预检

前面的例子就是所谓的 简单请求。简单请求就是携带一些被允许的消息头和值的 GET 或者 POST 请求。

现在 thirdparty.com 稍微修改一下实现,以此来获取 JSON:\

fetch('http://good.com:3000/public', {
  headers: {
    'Content-Type': 'application/json'
  }
})
  .then(response => response.json())
  .then((result) => {
    document.body.textContent = result.message
  })

但是这再次在 thirdparty.com 上出现问题!\
这次网络面板告诉我的原因是:

The request has been preflighted with an OPTIONS request

任意请求不是使用 GET 或 POST 方法,或者 Content-Type 的值不是

  • text/plain
  • application/x-www-form-urlencoded
  • multipart/form-data

任意其他不允许简单请求 的请求头都需要发起一个预检请求

这个机制是为了允许 web 服务器可以决定是否允许这个实际请求。浏览器设置 Access-Control-Request-Headers 和 Access-Control-Request-Method 请求头以此来告诉服务器需要什么请求,以及服务器用该请求头响应。

我们的服务器还没有响应这些请求头,那么让我们加上它们吧:\

app.get('/public', function(req, res) {
  res.set('Access-Control-Allow-Origin', '*')
  res.set('Access-Control-Allow-Methods', 'GET, OPTIONS')
  res.set('Access-Control-Allow-Headers', 'Content-Type')
  res.send(JSON.stringify({
    message: 'This is public info'
  }))
})

现在 thirdparty.com 又可以正常访问响应了。

凭证和 CORS

现在让我们假设一下这种场景,我们已经在 good.com 登录并且可以使用敏感信息访问 /private URL。

通过我们所有的 CORS 设置,其他网站可以获取到这些隐私信息吗?例如 evil.com 。

拭目以待吧:\

fetch('http://good.com:3000/private')
  .then(response => response.text())
  .then((result) => {
    let output = document.createElement('div')
    output.textContent = result
    document.body.appendChild(output)
  })

无论我们是否在 good.com 已经登录,我们都将看到 “Please login first(译者注:请先登录)”。

原因就是当从其他源(本例中是 evil.com)发起请求,good.com 的 cookie 不会被发送。\
即便是跨源域,我们依旧可以要求浏览器发送 cookies:\

fetch('http://good.com:3000/private', {
  credentials: 'include'
})
  .then(response => response.text())
  .then((result) => {
    let output = document.createElement('div')
    output.textContent = result
    document.body.appendChild(output)
  })

但是这个依旧不可以在浏览器中工作。实际上,这是个好消息。

想象一下,任意网站都可以发起附带凭证的请求 - 这个请求并不会携带真实的 cookie被发出,并且响应是不可访问的。

那么,我们不希望 evil.com 可以访问私有数据 - 但是如果我们希望 thirdparty.com 可以访问 /private 呢?\
在这个例子中我们需要设置 Access-Control-Allow-Credentials 请求头为 true:\

app.get('/private', function(req, res) {
  res.set('Access-Control-Allow-Origin', '*')
  res.set('Access-Control-Allow-Credentials', 'true')
  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET')
  } else {
    res.send('Please login first')
  }
})

但是依旧没有正常工作。允许所有附带身份凭证的跨域请求是相当危险的做法

浏览器不会允许我们这么简单的错误。

当我们希望允许 thirdparty.com 访问/private 时,我们可以在请求头设置这个域名:\

app.get('/private', function(req, res) {
  res.set('Access-Control-Allow-Origin', 'http://thirdparty.com:8000')
  res.set('Access-Control-Allow-Credentials', 'true')
  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET')
  } else {
    res.send('Please login first')
  }
})

那么现在 http://thirdparty:8000 就可以正常访问隐私数据了,而 evil.com 会被锁定。

允许多个源

现在我们是允许一个源附带身份凭证进行跨源请求。但是如果我们有多个第三方?

在下面的例子中,我会使用一个白名单:\

const ALLOWED_ORIGINS = [
  'http://anotherthirdparty.com:8000',
  'http://thirdparty.com:8000'
]

app.get('/private', function(req, res) {
  if(ALLOWED_ORIGINS.indexOf(req.headers.origin) > -1) {
    res.set('Access-Control-Allow-Credentials', 'true')
    res.set('Access-Control-Allow-Origin', req.headers.origin)
  } else { // 允许其他源发起不附带身份凭证的 CORS 请求
    res.set('Access-Control-Allow-Origin', '*')        
  }

  // 让缓存知道这个是依赖于源的响应
  res.set('Vary', 'Origin');

  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET')
  } else {
    res.send('Please login first')
  }
})

再次声明:不要直接发送 req.headers.origin 作为 CORS 源请求头。这会允许任意网站携带身份凭证请求你的站点。\
这个规则可能会有例外,不过我们在实现没有白名单的附带身份凭证的 CORS 时,切记三思啊。

总结

在本文中,我们探究了同源策略 和如何使用 CORS 在需要时允许跨源请求。

这个需要服务端和客户端设置,根据请求会导致预检请求。

当处理附带身份凭证的跨源请求时需要额外注意。白名单可以帮助我们来允许多个源,并且没有隐私数据泄露的风险(在身份凭证的情况下被保护)。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/g33konaut/understanding-c...

译文地址:https://learnku.com/f2e/t/37488

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!