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 控制台找到原因:
啊哈!我们缺少了 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 和响应:
非简单请求和预检
前面的例子就是所谓的 简单请求。简单请求就是携带一些被允许的消息头和值的 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 上出现问题!\
这次网络面板告诉我的原因是:
任意请求不是使用 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: