开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
跨源资源共享 (CORS) 是一种支持来自外部源(域、方案或端口)的安全请求和数据传输的机制。
例如,example.com
使用托管在fonts.com
的字体访问时,用户的浏览器会从fonts.com
请求字体. 因为fonts.com
和example.com
是两个不同的来源,所以这是一个跨域请求。如果fonts.com
允许跨源资源共享example.com
,那么浏览器将继续加载字体。否则,浏览器将取消请求。
更具体地说,CORS 是 Web 服务器表示“接受来自此来源的跨域请求”或“不接受来自此来源的跨域请求”的一种方式。
这很重要,因为跨源请求可能非常可怕。我可以登录到我的银行帐户,并在访问恶意网站时,它可以在我不知情的情况下向银行的服务器发出请求。如果 CORS 规则不存在,请求就会通过——可能会更改或泄露我的帐户信息。
CORS是一种定义跨源请求限制的协议。这些限制由浏览器强制执行。因此,因此,我们仍然可以在保持高级别安全性的同时发出跨源请求。通过指定允许哪些源发出请求以及允许哪些方法和头,浏览器确保恶意行为者无法通过跨源请求检索敏感数据。
CORS 背景
CORS 的发明是为了扩展和增加同源策略 (SOP) 的灵活性。
同源策略本质上就是资源只能从同源加载。如果协议、端口(如果指定)和主机相同,则两个源定义为相同。
从技术角度来看,一个来源仍然可以从另一个来源请求资源,但浏览器阻止了响应的可读性。
然而,我们有时需要访问其他来源的资源——例如fonts.com
. 这就是 CORS 的用武之地。CORS 通过定义受信任或允许的来源、方法和标头来放宽同源策略。
CORS 预检请求
现在,让我们进入到从另一个域请求资源时发生的实际情况。
我们已经建立了一个示例网站,在那里我们可以看到完整的CORS动议。该网站调用https://api-emailpassword.demo.supertokens.com
上的API。尽管这两个域都是demo.supertokens.com
的子域,但浏览器将它们注册为不同的源,因此就出现了CORS。
在登录期间,如果打开浏览器的开发工具并查看网络选项卡,将看到正在发出预检请求。更具体地说,预检请求是OPTIONS
向我们的 API 域发出的带有几个标头的请求。让我们来看看当我们点击登录时会发生什么——
OPTIONS /auth/signin HTTP/1.1
Host: api-emailpassword.demo.supertokens.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,fdi-version,rid
Origin: https://emailpassword.demo.supertokens.com
这就是我们的预检请求。分解这些,我们有四个关键的事情要看。
Host
- 我们请求的资源的“主机”。对我们来说,那是api-emailpassword.demo.supertokens.com
Access-Control-Request-Method
- 操作提出请求的方法。这可以是任何 HTTP 请求方法,包括GET
、POST
、PUT
、DELETE
和CONNECT
。Access-Control-Request-Headers
-将在实际请求中使用的以逗号分隔的HTTP 标头列表。Origin
- 请求来自哪里。那是https://emailpassword.demo.supertokens.com
在获得此预检OPTIONS
请求后,API 服务器发送预检前响应。以下是我们的回应api-emailpassword.demo.supertokens.com
。
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://emailpassword.demo.supertokens.com/
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,PUT,POST,DELETE
Access-Control-Allow-Headers: content-type,rid,fdi-version,anti-csrf
让我们分解这个响应。
Access-Control-Allow-Origin
— API 服务器已列入白名单的来源。请注意,此值与我们的网站域相同。这告诉浏览器服务器期望来自该客户端的请求。Access-Control-Allow-Credentials
— 服务器告诉我们实际请求是否可以包含 cookie 或实际请求的响应可以设置 cookie。在我们的例子中,cookie 指的是用户的会话令牌,一旦用户登录,它就充当用户的凭据。Access-Control-Allow-Methods
— API 域允许跨源请求的 HTTP 方法的逗号分隔列表。Access-Control-Allow-Headers
— API 域允许跨源请求的 HTTP 标头的逗号分隔列表。
浏览器然后从 API 服务器获取此响应以确定是否应发送实际请求。如果来自 API 的响应不包括请求的来源、方法或来自预检请求的标头,则浏览器将不会发送实际请求。
CORS 实际请求
如果来自 API 的响应包含请求的来源,那么就该发送实际POST
的登录请求了。
POST /auth/signin HTTP/1.1
Host: http://api-emailpassword.demo.supertokens.com/
content-type: application/json
fdi-version: 1.15
rid: emailpassword
Content-Length: 92
Origin: https://emailpassword.demo.supertokens.com/
请注意,发送的来源和标头 ( fdi-version
, rid
, content-type
) 已被服务器列入白名单,并且由于预检前响应,浏览器知道这一点。
现在让我们看一下服务器的响应。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://emailpassword.demo.supertokens.com/
Access-Control-Allow-Credentials: true
front-token: ...
Access-Control-Expose-Headers: front-token, id-refresh-token
Set-Cookie: ...
在这里,我们从服务器获得带有 cookie 和令牌的响应,使我们能够继续进行登录操作。需要注意的一件事是,与我们的预检相比,我们现在还有一个额外的Access-Control-Expose-Headers
标题。
Access-Control-Expose-Headers
— 服务器指示哪些响应标头可用于浏览器中运行的脚本。有趣的是,它存在的原因不是因为安全性,而是因为在 CORS 不存在时向后兼容。
有了这个,我们现在已经完成了我们的第一个预检前请求/响应,以及我们实际的登录请求/响应。
CORS 中的通配符
配置CORS的一个常见错误是使用通配符。通常,在定义CORS允许的起源、方法或头文件时,开发人员会选择使用通配符*
。
虽然通配符适用于简单请求(没有HTTP cookie或 HTTP 身份验证信息的请求),但带有凭据的请求通常会遇到 CORS 未授权错误。
这是因为,在具有凭据(cookie)的请求中,通配符*
被视为没有特殊语义的字面方法名或起源名。这在Access-Control-Allow-Origin
和Access-Control-Allow-Methods
中都会发生,而且像Safari这样的一些浏览器根本不支持通配符。
总而言之,在配置 CORS 时避免使用通配符并使用逗号分隔列表是一种很好的习惯。
CORS漏洞
如果配置不当,CORS 可能会导致重大漏洞。下面,我们将列出配置 CORS 时的几个常见问题。
错误处理来源白名单
实施 CORS 时最容易犯的错误之一是错误处理源白名单。将来源列入白名单时,使用 URL 前缀或后缀或使用正则表达式进行简单匹配通常很容易。但是,这可能会导致很多问题。
假设我们授予访问所有后缀为whiteliste-website.com
的网站的权限。这使得我们可以轻松地授予对api.whitelisted.website.com
的访问权限。
但是攻击者可以使用诸如maliciouswhitelisted-website.com
此类的网站并获得访问权限。
这里避免潜在滥用的最佳方法是在实现CORS时为敏感操作显式地在白名单上定义起源(例如指定字符串)"https://whitelisted-website.com"
和"https://api.whitelisted-website.com"
将仅授予对这些域的访问权限。
来源为空的请求
另一个常见的错误配置是将值为null
的起源列入白名单。在以下情况下,浏览器可能会在原始标头中发送空值
- 带文件请求;
- 沙盒化跨源请求。
在这种情况下,攻击者可以使用各种技巧来生成包含值 null 作为来源的请求,该请求已在我们的配置中列入白名单。例如,攻击者可以使用以下沙盒 iframe 漏洞——
<iframe src="data:text/html" sandbox="allow-scripts allow-top-navigation allow-forms allow-same-origin">
function reqlistener() {
console.log(this.responseText)
}
var req = new XMLHttpRequest();
req.onload = reqlistener;
req.open("GET", 'vulnerable.com/sensitive', true);
req.withCredentials = true;
req.send();
</iframe>
结论
我们希望本文能帮助了解 CORS 背后的基本原则以及实施 CORS 时的一些常见陷阱。