跨域相关问题
8月 21, 2021
本文先从 CSRF 攻击的介绍开始,说明了跨域存在的问题,然后引入浏览器同源策略的概念,明白了要限制跨域的原因后,再给出了标准的跨域请求方案:CORS,所以总体上就三部分:CSRF、同源策略、CORS,文章有点长但不用慌,按顺序看就可以了。
跨站请求伪造(CSRF)#
CSRF 跨站请求伪造(Cross-site request forgery, 通常缩写为 CSRF 或 XSRF),是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求是发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
场景举例#
- 用户 A 登陆银行页面
http://realbank.com进行资金操作,银行网站为了用户体验,在用户的 Cookie 里面保留了用户的登陆信息,下次 A 打开网站就可以免登陆。 - A 后来又浏览了一个恶意网站
http://fakebank.com,并以为是之前真实的银行网站。 - 恶意网站
http://fakebank.com在页面里了恶意代码,利用了 A 在浏览器里的 Cookie 里的身份信息,请求了http://realbank.com进行了资金操作。 http://realbank.com收到恶意请求,但是无法识别出来是恶意操作,于是当做 A 自己的操作进行了资金操作,A 的钱就这样被转走了。
通过这个例子可以见得,CSRF 通常会发生在第三方网站,因为更容易操作,但其实也可以在本域的社区内通过发布图片 <img src="http://eval.link"> 等方式加载恶意请求,从而发起同域 CSRF 攻击。
跨域的攻击随着 2008 年前后浏览器同源策略的制定,现在看起来已经很难有这种问题,但是在之前这种场景是真实存在的,并且包括 Google、百度一些大厂也都中过招。而在同域发起的攻击,一直都存在,目前通常是使用 CSRF Token 来实现的防范方法。
CSRF Token#
同源策略是浏览器主动制定的,如果有的“恶意”浏览器没有这个功能呢?或者恶意者在同域发起 CSRF 攻击呢?那么还需要服务器端主动控制,比较广泛的解决方案是 CSRF Token 机制。
我们知道,CSRF 利用的是浏览器(用户)自动“帮”恶意链接提交了 Cookie,服务器无法分辨是真实用户还是恶意请求。但是恶意链接不能拿到 Cookie 的具体内容,只能是利用。基于这个原因,我们可以不使用 Cookie 来保存用户的认证信息即可,即通过加密算法生成一个 Token,并将其保存在其他地方,比如 DOM 树中的特定表单,正常功能的表单在提交请求时会自动带着这个 Token,恶意链接请求服务器时就无法带着这个 Token。
这样服务端的整个认证体系就脱离了使用 Cookie 进行认证的机制,从而避免了 CSRF 攻击的问题。
使用 POST 会更安全吗?#
可以想到,不管是 GET 还是 POST,都可以发起 CSRF 攻击,POST 并不会更加安全,页面也可以通过下面的方式主动发起 POST 请求。
<body onload="javascript:document.forms[0].submit()">
<form></form>
</body>这里顺带一提关于 POST 方法的使用,下面这段话来自 CoolShell 的 “一把梭:REST API 全用 POST”。
很多同学以为
GET的请求数据在URL中,而POST的则不是,所以以为POST更安全。不是这样的,整个请求的 HTTP URL PATH 会全部封装在 HTTP 的协议头中。只要是 HTTPS,就是安全的。当然,有些网关如 nginx 会把 URL 打到日志中,或是会放在浏览器的历史记录中,所以有人会说GET请求不安全,但是,POST也没有好到哪里去,在 CSRF 这个最常见的安全问题上,则完全就是针对POST的。 安全是一件很复杂的事,无论你用哪方法或动词都会不能代表你会更安全。
跨站脚本攻击(XSS)#
说完 CSRF,不得不提一下 XSS(Cross-site scripting,为了区分 CSS,故叫 XSS),它通常通过在一些输入框输入一些不常用的非法字符来触发 js 脚本的执行,可以说有输入框的地方,就要警惕 XSS 攻击。
举个搜索引擎的搜索框的例子,输入关键词完毕后,当浏览器请求 http://google.com/search?keyword="><script>alert('XSS');</script> 时,服务端会解析出请求参数 keyword,得到 "><script>alert('XSS');</script>,拼接到 HTML 中返回给浏览器。形成了如下的 HTML:
<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
您搜索的关键词是:"><script>alert('XSS');</script>
</div>浏览器无法分辨出 <script>alert('XSS');</script> 是恶意代码,因而将其执行。一般的解决方案是转义输入的 HTML 字符、使用 CSP 安全规范等等,此外还有一些扫描工具来帮你扫描 XSS 漏洞。
类似上面的例子,XSS 攻击可以用来注入脚本的方法还有很多,主要分为以下三类。
存储型 XSS#
注入型脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器上传回并执行。
反射型 XSS#
当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web 服务器将注入脚本,比如一个错误信息,搜索结果等 返回到用户的浏览器上。由于浏览器认为这个响应来自“可信任”的服务器,所以会执行这段脚本。
基于 DOM 的 XSS#
通过修改原始的客户端代码,受害者浏览器的 DOM 环境改变,导致有效载荷的执行。也就是说,页面本身并没有变化,但由于 DOM 环境被恶意修改,有客户端代码被包含进了页面,并且意外执行。
CSP#
CSP(内容安全策略,Content Security Policy) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击(比如 XSS),CSP 的实质就是白名单制度,开发者可以通过 HTTP 响应头 Content-Security-Policy 来允许服务端控制浏览器能够为指定的页面加载哪些资源,等同于提供白名单。
它的实现和执行全部由浏览器完成,开发者只需提供配置。具体的参数介绍太长,与本文主题不太相关,可以后面参考阮一峰的博客或者 MDN 文档。
CSRF 和 XSS 区别#
CSRF 是伪造请求,盗用真实用户的身份,可以发起跨域或同域请求。
XSS 是主动向网站注入代码,不能通过盗用用户真实身份实现大批量攻击,但是只要发现 XSS 漏洞,很多时候就可以实现包含 CSRF 在内的更多方式的攻击。
XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
同源策略#
浏览器的 同源策略(Same Origin Policy)是其核心的安全功能。它用于限制一个 Origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
目的:安全、安全,还是安全#
也就是说为了安全,如无特殊办法(比如下面的 CORS 机制),浏览器就会阻止任何的跨域请求,如果你能理解上面的非法攻击(比如 XSS、CSRF)所能导致的严重损失,你应该会理解浏览器这么做其实是完全合理的。
同源的定义#
如果两个 URL 的 协议 + 域名 + 端口 都一样的话,那么这两个 URL 就是同源。举一些例子,下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:
| URL | 结果 | 原因 |
|---|---|---|
http://store.company.com/dir2/other.html | 同源 | 只有路径不同 |
http://store.company.com/dir/inner/another.html | 同源 | 只有路径不同 |
https://store.company.com/secure.html | 失败 | 协议不同 |
http://store.company.com:81/dir/etc.html | 失败 | 端口不同 ( http:// 默认端口是80) |
http://news.company.com/dir/other.html | 失败 | 主机不同 |
Origin#
浏览器在请求跨域的服务端时,会自动带上 Origin 头,用于询问服务端是否支持 CORS 机制。
GET /posts/http-cross-domain-problem/ HTTP/1.1
Accept: */*
Origin: http://localhost:1313
Referer: http://localhost:1313/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36Referer 和 Host#
这里顺便介绍下 Origin 与 Referer、Host 的区别。Referer 指示了请求来自于哪个具体页面,包含服务器名和路径的详细 URL,浏览器自动将其添加到 HTTP 请求 Header 中,无需手动设置。服务端一般使用 Referer 请求头识别访问来源,进行统计分析、日志记录、缓存优化、异常访问等。
HTTP/1.1 200 OK
Referer: http://localhost:1313/posts/http-cross-domain-problem/
Host: localhost:1313Host 描述请求将被发送的目的地,仅仅包括域名和端口号。在任何类型请求中,浏览器请求都会包含此信息。
限制范围#
AJAX#
不能发起发起跨域的 AJAX 请求,这个是最基本的。
iframe#
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。
Cookie#
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置 document.domain 共享 Cookie。document.domain 只能从子域设置到主域,往下设置以及往其他域名设置都是不允许的, 在 Chrome 中给出的错误是这样的:
Uncaught DOMException: Failed to set the 'domain' property on 'Document': 'test.yindongliang.com' is not a suffix of 'yindongliang.com'.注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法。
本地存储#
不能访问另一个源存储在浏览器中的数据,如 localStorage 和 IndexedDB,它们在浏览器中以源进行分割。每个源都拥有自己单独的存储空间,一个源中的 JavaScript 脚本不能对属于其它源的数据进行读写操作。
突破限制#
反向代理#
这个方案是通过后端来实现的,浏览器还是在同源策略的框架下进行请求。
CORS#
CORS 是一套专门解决跨域的官方标准方案,服务器只要遵守这套规则进行响应,就可以使自己的网站内容实现可控的跨域请求,后面会详细介绍。
JSONP#
JSONP 是一种非官方的 Hack 手段,利用 js 的 <script> 标签的跨域能力发起请求,由服务器返回一个回调本地函数的函数的字符串,可由 js 直接执行,但 JSONP 只支持 GET 请求。另外 img、CSS 标签都可以发起跨域请求,加载下来就属于当前域了。
WebSocket#
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。服务器可以根据 Origin 字段,判断是否许可本次通信。
跨源资源共享(CORS)#
浏览器不能完全避免任何的跨域请求,这不符合现在实际情况的需要,于是引入 CORS 来合法地支持跨域访问的需求。CORS(Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,服务器通过这些 HTTP 头来告诉浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

如果不使用 CORS 策略,运行在 https://domain-a.com 的 JavaScript 代码只能请求当前域的服务器资源,加上属于 CORS 的 HTTP 请求头后浏览器就允许发起跨域请求了,下面就详细说一下 CORS 的内容。
浏览器将 CORS 请求分为两种:简单请求和预检请求,本节最下面也附带了涉及 CORS 的请求首部和响应首部。
简单请求#
满足所有的下列条件,就可以被视为简单请求(simple request):
- 使用下列方法之一:
- GET
- HEAD
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type,只限于下面三个值:
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
请求#
对于跨域的简单请求,浏览器直接发出该请求,并附带上 Origin 字段。
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example响应#
如果服务器允许该跨域请求,也会在响应的 HTTP 头里表现出来,比如下面例子的 Access-Control-Allow-Origin: * ,表明该资源可以被任意外域访问。:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml如果 Origin 指定的域不在服务器的许可范围内,服务器会返回一个正常的 HTTP 回应,浏览器发现回应的头没有包含 Access-Control-Allow-Origin 字段或者指定的值不是 Origin,浏览器就会抛出一个错误。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://foo.example预检请求#
对于“非简单”的请求,就需要浏览器首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求,服务器确认允许之后,才发起实际的 HTTP 请求。
请求#
预检请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段还是 Origin,表示请求来自哪个源。
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type其中:
- 首部字段
Access-Control-Request-Method告知服务器,实际请求将使用 POST 方法。 - 首部字段
Access-Control-Request-Headers告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER与Content-Type。
服务器据此决定,该实际请求是否被允许。
附带身份认证的请求#
请求时可以在浏览器中选择是否发送 Cookie,首先 XMLHttpRequest 请求时要打开 withCredentials 属性(即请求头的 Access-Control-Allow-Credentials 值为 true),同时服务器的 Access-Control-Allow-Credentials 响应头的值也要为 true,才表明服务器接受通过 Cookie 发送身份认证的请求。需要注意的是,在处理这种请求时:
- 服务器不能将
Access-Control-Allow-Origin的值设为通配符*,而应将其设置为特定的域,如:Access-Control-Allow-Origin: https://example.com。 - 服务器不能将
Access-Control-Allow-Headers的值设为通配符*,而应将其设置为首部名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。 - 服务器不能将
Access-Control-Allow-Methods的值设为通配符*,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET。
响应#
同简单请求的响应内容一样,浏览器也会通过检查预检请求的响应中的 CORS 相关头信息来判断服务器是否支持当前的请求域。
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive一旦服务器通过了预检请求,以后的每次浏览器正常的 CORS 请求,就和简单请求一样,每次请求都会携带 Origin 头,每次响应也会附带 Access-Control-Allow-Origin 头,预检请求的整体流程参考下图。

请求首部字段#
Origin#
Origin 首部字段表明预检请求或实际请求的源站。
Access-Control-Request-Method#
Access-Control-Request-Method 首部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。
Access-Control-Request-Headers#
Access-Control-Request-Headers 首部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。
响应首部字段#
Access-Control-Allow-Origin#
响应首部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: <origin> | *其中,origin 参数的值指定了允许访问该资源的外域 URI。当响应的是附带身份凭证的请求时,服务端 必须 明确 Access-Control-Allow-Origin 的值,而不能使用通配符“*”。
Access-Control-Expose-Headers#
Access-Control-Expose-Headers 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。例如:
HTTP/1.1 200 OK
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header这样浏览器就能够通过 getResponseHeader 访问 X-My-Custom-Header 和 X-Another-Custom-Header 响应头了。
Access-Control-Max-Age#
Access-Control-Max-Age 头指定了preflight请求的结果能够被缓存多久,在这个时间以内,预检请求就和非简单请求一样不需要每次发送 OPTION 请求。
HTTP/1.1 200 OK
Access-Control-Max-Age: <delta-seconds>delta-seconds 参数表示 preflight 预检请求的结果在多少秒内有效。
Access-Control-Allow-Credentials#
Access-Control-Allow-Credentials 该字段可选,请求和响应时都可以指定。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为true,即表示服务器明确许可,Cookie 可以包含在请求中一起发给服务器。
这个值只能设为true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
Access-Control-Allow-Methods#
Access-Control-Allow-Methods 首部字段用于预检请求的响应,其指明了实际请求所允许使用的 HTTP 方法。
Access-Control-Allow-Headers#
Access-Control-Allow-Headers 首部字段用于预检请求的响应,其指明了实际请求中允许携带的首部字段。
参考#
https://tech.meituan.com/2018/10/11/fe-security-csrf.html
https://tech.meituan.com/2018/09/27/fe-security.html
https://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html