引入预检CORS请求的动机是什么?

518

跨域资源共享是一种机制,允许网页向另一个域名发起XMLHttpRequest(来自Wikipedia)。

我已经在研究CORS几天了,我认为我对所有东西的工作原理都有了很好的理解。

所以我的问题不是关于CORS / 预检如何工作的,而是关于提出预检作为新请求类型背后的原因。我看不出服务器A需要向服务器B发送预检(PR)来查明是否接受真实请求(RR)的任何原因 - B可以在没有任何先前PR的情况下接受/拒绝RR。

在搜索了很多内容之后,我在www.w3.org(7.1.5)找到了这段信息

为了保护资源免受跨源请求的攻击,这些请求在此规范之前无法从某些用户代理发起,需要进行预检请求以确保该资源意识到此规范的存在。
我发现这是最难理解的句子。我的解释(最好称之为“最佳猜测”)是它是关于保护服务器B免受来自不了解规范的服务器C的请求的。
有人能否解释一种场景/展示PR + RR比RR单独更好解决问题的问题?
9个回答

406

我曾经对预检请求的目的感到困惑,但现在我想我理解了。关键在于,预检请求不是一个安全的问题。相反,它们是一个不改变规则的问题。

预检请求与安全无关,对于那些意识到CORS的应用程序来说也没有影响。相反,预检机制有益于那些没有意识到CORS的服务器,并且它在客户端和服务器之间起到了一个合理性检查的作用,以确保它们都具备CORS意识。CORS的开发人员认为,有足够多的服务器依赖于这样一种假设,即它们永远不会接收到跨域DELETE请求,他们发明了预检机制,以允许双方选择加入。他们认为,另一种选择——仅仅启用跨域调用——将会破坏太多现有的应用程序。

这里有三种情况:

  1. 旧服务器,不再开发,并且在CORS出现之前开发。这些服务器可能会做出假设,即它们永远不会收到例如跨域DELETE请求。 这种情况是预检机制的主要受益者。 是的,这些服务已经可以被恶意或不合规的用户代理滥用(CORS对此毫无作为),但在CORS的世界里,预检机制提供了额外的“合理性检查”,以使客户端和服务器不会因为Web的基本规则发生变化而中断。

  2. 仍在开发中的服务器,但其中包含大量旧代码,并且审核所有旧代码以确保其在跨域环境下正常工作是不可行/不可取的。这种情况允许服务器逐步加入CORS,例如通过说“现在我将允许这个特定的标头”,“现在我将允许这个特定的HTTP动词”,“现在我将允许发送cookie/auth信息”等。 这种情况受益于预检机制。

  3. 新的服务器编写时意识到CORS。根据标准安全实践,服务器必须在面对任何传入请求时保护其资源--服务器不能相信客户端不会做恶意的事情。 这种情况不受预检机制的好处:预检机制对于已正确保护其资源的服务器不会带来额外的安全性。


25
如果是这样的话,为什么要在每个请求中发送它?每个服务器只需要一个请求就足以确定该服务器是否知道CORS。 - Douglas Ferguson
3
规范中包含一个 preflight-result-cache,浏览器可以使用它。因此,尽管它仍然感觉笨拙且安全性较差,但似乎可以配置新的服务器以无限期缓存预检请求结果。 - Michael Cole
12
我同意预检请求本身并不涉及安全问题,但似乎CORS使用预检请求是出于安全考虑。这不仅仅是为了防止相对无害的错误场景,而是为了防止用户代理盲目地向服务器发送请求,并错误地假设服务器实现了CORS。如果这样做,服务器很可能会接受跨站点请求伪造攻击。即使响应无法被JavaScript读取,服务器也可能已经执行了一些不良操作,比如删除帐户或进行银行转账。 - Alexander Taylor
7
问题在于,预检结果缓存基本上是无用的,因为:1. 它只适用于确切的请求,而不是整个域,所以所有请求第一次都要进行预检;2. 实现时,在大多数浏览器中,它只限制为10分钟,远远不能持续无限时间。 - davidgoli
3
现有服务器必须“选择加入”并同意通过配置如何响应预检请求来共享其资源跨源。如果它们不明确回答预检请求,浏览器将不会发出实际请求。毕竟,并非所有服务器都希望处理跨来源的请求。 - Kevin Lee
显示剩余11条评论

380

什么是引入预检请求的动机?

引入预检请求是为了让浏览器在发送某些请求之前确定它正在处理一个CORS感知服务器。这些请求被定义为潜在危险(改变状态)且新的(由于同源策略,在CORS之前不可能)。使用预检请求意味着服务器必须选择加入(通过正确响应预检请求)到CORS使得可能的新的、潜在危险的请求类型中。

这就是原始规范this part的含义:“为了保护资源免受跨域请求的影响,这些请求在此规范出现之前无法从某些用户代理发起,因此需要进行预检请求以确保该资源已经意识到此规范。”

你能给我一个例子吗?

假设浏览器用户已登录其银行网站 A.com。当他们导航到恶意的 B.com 时,该页面包括一些 JavaScript,试图向 A.com/account 发送一个 DELETE 请求。由于用户已登录 A.com,如果发送该请求,它将包括标识用户的 cookie。
在 CORS 之前,浏览器的同源策略会阻止它发送此请求。但是,由于 CORS 的目的是使这种跨源通信成为可能,因此这不再适用。
浏览器可以简单地发送 DELETE 并让服务器决定如何处理它。但是,如果 A.com 不知道 CORS 协议怎么办?它可能会继续执行危险的 DELETE。它可能已经假定由于浏览器的同源策略,它永远不会收到这样的请求,因此可能从未针对这种攻击进行过硬化。
为了保护这些不支持CORS的服务器,协议要求浏览器首先发送一个预检请求(preflight request)。只有支持CORS的服务器才能正确响应这种新类型的请求,让浏览器知道是否安全地发送实际的DELETE请求。 为什么要大费周折涉及到浏览器呢?攻击者不能直接从他们自己的计算机发送DELETE请求吗? 当然可以,但这样的请求不会包含用户的cookies。这个设计旨在防止一种攻击行为,该攻击行为依赖于浏览器将来自其他域的cookie(特别是用户的身份验证信息)与请求一起发送。 听起来像是跨站请求伪造(Cross-Site Request Forgery),其中站点B.com上的表单可以提交到A.com,并使用用户的cookie造成破坏。 没错。换句话说,预检请求的创建是为了不增加对不支持CORS的服务器的CSRF攻击面。

但是POST列为不需要预检请求的方法。像DELETE一样,它可以改变状态并删除数据!

没错!CORS不能保护您的站点免受CSRF攻击。但是,如果没有CORS,您也无法免受CSRF攻击。预检请求的目的只是将您的CSRF暴露限制在预CORS世界中已经存在的范围内。

唉。好吧,我勉强接受预检请求的必要性。但是为什么我们必须为服务器上的每个资源(URL)都这样做呢?服务器要么处理CORS,要么不处理。

你确定吗?多个服务器为单个域处理请求并不罕见。例如,可能情况是对A.com/url1的请求由一种服务器处理,而对A.com/url2的请求由另一种服务器处理。通常情况下,处理单个资源的服务器无法对该域上的所有资源提供安全保证。

好的。让我们妥协,创建一个新的CORS头,允许服务器明确说明它可以代表哪些资源,以避免对这些URL进行额外的预检请求。

好主意!事实上,头文件Access-Control-Policy-Path就是为此目的而提出的。不过,最终该规范被省略了,显然是因为一些服务器错误地实现了URI规范,以致于浏览器认为路径安全,但在这些有问题的服务器上却并非如此。

这是一个明智的决定,优先考虑安全性,使浏览器可以立即实现CORS规范,而不会让现有的服务器面临风险吗?还是说这是短视的,为了适应特定时间的某个服务器中的错误而浪费带宽和加倍延迟,从而注定了互联网的浪费?

意见不一。

那么至少浏览器会为单个URL缓存预检吗?

是的。不过可能时间不会很长。在WebKit浏览器中,预检请求缓存的最大时间为目前为10分钟
唉。如果我知道我的服务器支持CORS并且不需要预检请求提供的保护,有没有什么方法可以避免它们?
你唯一的选择是确保你的请求使用CORS安全的方法头文件。这可能意味着省略你本来会包括的自定义头文件(如X-Requested-With),更改Content-Type或其他操作。

无论你做什么,都必须确保你有适当的CSRF保护措施,因为CORS不会阻止所有不安全的请求。正如原始规范所述:“除了检索之外,简单请求具有其他含义的资源必须保护自己免受跨站点请求伪造的攻击。”


2
@Yos:浏览器会包含这些cookie,因为这就是浏览器的预期工作方式(在诸如RFC 6265的标准中 codified)。无论浏览器是否使用单独的进程来处理选项卡都是实现细节,它不会阻止其发送cookie。 - Kevin Christopher Henry
@KevinChristopherHenry 当用户访问恶意网站B.com时,该页面包含一些JavaScript代码,试图向A.com/account发送DELETE请求。由于用户已登录到A.com,如果发送该请求,它将包括标识用户的cookie。在CORS之前,浏览器的同源策略会阻止它发送此请求。为什么浏览器会阻止从同一域(即从A.com)触发的DELETE请求?这段话对我来说很困惑,请澄清一下。 - divine
1
@divine:它不是由相同的域触发的,而是由来自B.com页面的Javascript触发对A.com的请求。 - Kevin Christopher Henry
1
@Number945:前面的段落解释了问题所在。危险在于,如果浏览器允许发送DELETE请求,服务器可能会执行该请求。这就是我们试图通过预检来防止的问题。对于安全请求,我们不必保护服务器,因此在这种情况下不需要进行预检,浏览器可以仅依靠头部信息来决定如何处理请求。 - Kevin Christopher Henry
1
@CiroSantilli新疆棉花TRUMPBANBAD:它会被阻止,因为这是一个“DELETE”操作。您提供的示例使用的是“POST”和“GET”,通常不会被阻止,我稍后在答案中讨论这个问题。 - Kevin Christopher Henry
显示剩余12条评论

70
在CORS出现之前,跨域请求的世界是这样的:你可以使用标准表单POST,或使用scriptimage标签发出GET请求。除了GET/POST,你无法进行任何其他类型的请求,并且在这些请求中不能发出任何自定义头部。
随着CORS的出现,规范作者面临一个挑战:引入一种新的跨域机制,而不破坏Web的现有语义。他们选择通过为服务器提供一种选择来支持任何新请求类型。这个选择就是预检请求。
因此,没有任何自定义头部的GET/POST请求不需要预检请求,因为在CORS出现之前已经可以实现这些请求。但是任何具有自定义头部的请求,或PUT/DELETE请求,都需要进行预检请求,因为它们是CORS规范中的新内容。如果服务器对CORS一无所知,它将回复不带任何CORS特定头部的响应,而实际请求将不会被执行。
如果没有预检请求,服务器可能会开始看到浏览器发送的意外请求。如果服务器没有准备好处理这些类型的请求,这可能会导致安全问题。CORS预检请求使得可以以安全的方式引入跨域请求到Web中。

2
你不能这样做。我的意思是你可以使用表单POST,或者使用脚本/图像进行GET。我编辑了帖子,希望能够澄清这一点。 - monsur
6
谢谢您的回答,它确实完善了我的认识!不幸的是,我仍然看不到预检操作背后的核心。关于您的回答:什么是“意外请求”?在没有预检操作的世界中,它如何比预检操作的世界(例如遗失预检或恶意浏览器“忘记”进行预检)更“意外”/不安全? - Jan Groth
13
可能有一些 API 依赖于浏览器的同源策略来保护它们的资源。它们应该有额外的安全措施,但是它们只依赖于同源策略。如果没有预检请求,不同域上的用户现在可以对该 API 发出请求。该 API 将假设请求是有效的(因为它不知道 CORS),并执行该请求。浏览器可以阻止响应到达用户,但此时可能已经造成了损失。如果请求是 PUT/DELETE,资源可能已被更新或删除。 - monsur
1
@小鹏 - ZenUML.com 攻击者必须以某种方式让受害者使用恶意浏览器。如今,大多数人都使用知名的安全浏览器。 - David Klempfner
1
@小鹏 - ZenUML.com 它可以保护双方。如果防止了 XSRF 攻击,银行因此不会删除客户账户,则客户/用户和银行都会感到满意。 - David Klempfner
显示剩余5条评论

47
CORS允许您指定比跨源
更多的标头和方法类型。 一些服务器可能已经(糟糕地)受到保护,认为浏览器不能进行例如跨源DELETE请求或带有X-Requested-With标头的跨源请求,因此这些请求是“可信赖的”。 为了确保服务器确实支持CORS而不仅仅是响应随机请求,执行预检。

13
这应该是被采纳的答案。它最清晰明确且直截了当。本质上,预检请求的唯一目的就是将CORS前Web标准与CORS后Web标准相结合。 - chopper draw lion4
2
我喜欢这个答案,但我觉得它不能是完整的原因...“信任假设”只适用于浏览器才能完成的事情(具体来说,发送限制在其域中的浏览器用户信息 - 也就是说,cookie)。如果这不是假设的一部分,那么跨源浏览器请求可以执行的任何操作都已经可以由第三方非浏览器代理完成了,对吗? - Fabio Beltramini
3
@FabioBeltramini 没错,非浏览器可以发送任何他们想要的东西。然而,通过浏览器进行的攻击是特殊的,因为你可以让其他人的浏览器从他们自己的IP地址、使用他们自己的cookie等方面执行操作。 - Kornel
我开始看到真正的问题了。感谢@FabioBeltramini和Kronel的评论和回复。如果没有预检查,攻击者将能够在自己的网站上放置一些JavaScript代码,但从许多其他人的计算机上执行。所有其他客户端都很难“雇佣”其他人来做这件事,包括移动应用程序。 - Devs love ZenUML

20

我认为其他答案没有关注预检请求增强安全性的原因。

场景:

1)使用预检请求。攻击者伪造了一个来自dummy-forums.com网站的请求,而用户已通过safe-bank.com进行身份验证。如果服务器不检查来源,并且存在缺陷,则浏览器将发出预检请求,即OPTION方法。服务器不知道浏览器期望作为响应的CORS,因此浏览器不会继续(完全没有危害)。

2)不使用预检请求。在与上述情况相同的情况下,攻击者伪造请求,浏览器将立即发出POST或PUT请求,服务器接受并可能处理该请求,这可能会导致一些危害。

如果攻击者直接从某个随机主机发出跨源请求,那么很可能是在考虑一个没有身份验证的请求。这是一个伪造的请求,但不是xsrf攻击。因此,服务器将检查凭据并失败。尽管白名单可以帮助减少此攻击向量,但CORS并不试图防止具有凭据发出请求的攻击者。

预检机制增加了客户端和服务器之间的安全性和一致性。我不知道这是否值得每个请求额外进行一次握手,因为缓存在那里很难使用,但这就是它的工作原理。


同意@michael-iles回复中提到的“新服务器”仍然可能受到CSRF攻击问题。 - eel ghEEz
1
但是为什么像Content-Type为text/plain的POST请求不需要进行预检请求呢?在我看来,如果安全是一个问题,每个“写”请求(POST、PUT、DELETE)都应该有这个预检请求。 - Israel Fonseca
使用text/plain的POST请求被认为是一个简单的请求 - 请注意,如果源不匹配(如果服务器未配置CORS,则会出现这种情况),浏览器将不会显示响应。 - Hirako
在攻击方面,有一些有趣的事情可以做,利用大多数浏览器容忍并将发送简单请求的事实。例如:这个链接 - Hirako
@Hirako 如果 origin header 的值是不同的域名,浏览器可能无法显示响应,但损害仍将发生。 - David Klempfner

18

这是另一种看待它的方式,使用代码:

<!-- hypothetical exploit on evil.com -->
<!-- Targeting banking-website.example.com, which authenticates with a cookie -->
<script>
jQuery.ajax({
  method: "POST",
  url: "https://banking-website.example.com",
  data: JSON.stringify({
    sendMoneyTo: "Dr Evil",
    amount: 1000000
  }),
  contentType: "application/json",
  dataType: "json"
});
</script>

在CORS出现之前,上述的攻击尝试会失败,因为它违反了同源策略。设计成这样的API不需要XSRF保护,因为它受到浏览器本地安全模型的保护。在Pre-CORS时代,浏览器无法生成跨源JSON POST。

现在,如果不需要通过预检请求来选择支持CORS,则突然间该网站将具有巨大的漏洞,而这并非他们自己的过错。

要解释为什么允许跳过预检请求的一些请求,可以通过规范回答:

已定义简单跨源请求与当前部署的不符合此规范的用户代理可能生成的那些一致。

简单的跨源GET请求不需要预检请求,因为它是由7.1.5定义的“简单方法”。 (头也必须是“简单的”,以避免预检请求)。这样做的理由是,例如,可以通过<script src="">(这就是JSONP的工作方式)执行“简单”的跨源GET请求。由于带有src属性的任何元素都可以触发没有预检请求的跨源GET请求,因此在“简单”XHR上要求预检请求没有安全性好处。


1
@MilesRout:Telnet不是预检模型旨在缓解的威胁范畴。Preflight适用于依赖存储的“环境权限”(例如cookie)的浏览器,并且可以被第三方欺骗以误用该权限(例如跨站点请求伪造)。这种广义模型被称为困惑的代理问题 - Dylan Tack
要求在简单的XHR上进行预检测确实会带来安全好处,但这样做会违反规则。突然之间,以前有效的GET请求将不再起作用。 - David Klempfner

3
在支持CORS的浏览器中,读取请求(如GET)已经受到同源策略的保护:恶意网站试图进行身份验证的跨域请求(例如访问受害者的网上银行网站或路由器配置界面)将无法读取返回的数据,因为银行或路由器没有设置Access-Control-Allow-Origin头。

然而,对于写入请求(如POST),当请求到达Web服务器时,损害已经造成。* Web服务器可以检查Origin头以确定请求是否合法,但是这种检查通常没有实现,因为要么Web服务器不需要CORS,要么Web服务器比CORS旧,因此假定跨域POST完全被同源策略禁止。

这就是为什么Web服务器有机会选择加入接收跨域写请求的原因。

* 本质上是CSRF的AJAX版本。


Web服务器比CORS旧,因此假定跨域POST完全受同源策略的禁止。跨域POST一直是允许的,只是您无法查看响应。这就是为什么Web服务器有机会选择接收跨域写请求的原因。它们只能选择PUT/DELETE请求(或带有非标头的GET/POST请求)。 一个简单的POST请求将被允许,无论是否了解CORS。 - David Klempfner
@DavidKlempfner 我之前不知道这个,我想知道为什么允许这样做。可能是因为禁止会破坏太多现有的解决方案(例如通过POST获取搜索结果的链接),而且在没有凭据的情况下被认为相对安全。 - AndreKR
1
这个链接解释了它:https://stackoverflow.com/questions/64769030/why-does-the-same-origin-policy-not-block-post-requests - David Klempfner

2
此外,对于那些可能会对用户数据造成副作用的HTTP请求方法(特别是除了GET之外的HTTP方法,或使用某些MIME类型的POST),规范要求浏览器进行“预检”请求。来源

0
预检请求不是关于性能的吗?通过预检请求,客户端可以在发送大量数据(例如使用PUT方法的JSON)或在通过身份验证标头传输敏感数据之前快速确定操作是否被允许。
默认情况下,PUT、DELETE和其他方法以及自定义标头都不被允许(需要使用“Access-Control-Request-Methods”和“Access-Control-Request-Headers”进行显式授权),这听起来像是一个双重检查,因为这些操作可能对用户数据产生更多的影响,而不是GET请求。所以,它听起来像这样:
“我看到你允许来自http://foo.example的跨站点请求,但你确定你会允许DELETE请求吗?你考虑过这些请求可能对用户数据造成的影响吗?”
我没有理解预检请求和旧服务器优势之间的引用关系。在CORS之前实现或没有CORS意识的Web服务将永远不会收到任何跨站点请求,因为它们的响应首先不会有“Access-Control-Allow-Origin”标头。

4
你误解了Access-Control-Allow-Origin。缺少该头信息并不会阻止浏览器发送请求,只是防止JS能够读取响应数据。 - Dylan Tack
你能再解释一下这句话“缺少该标头并不会阻止浏览器发送请求,它只是防止JS能够读取响应中的数据”吗?我没有完全理解。 - bhagwans
@DylanTack 说得好。不过我想知道,为什么 GET xhr 没有被预检呢?虽然不太可能,GET 请求也可能具有危害性/数据突变性。此外,既然所有这些都可以通过 CSRF 解决,这对我来说就像浏览器对那些太疏忽实施常见安全措施的服务器过于保护一样。 - Peleg
被接受的答案解释得很好,作为一个“不改变规则的事情”(与CORS存在之前创建的网站的向后兼容性)。但看到代码仍然很有趣,所以我发布了另一个带有代码示例的答案。 - Dylan Tack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接