# 再聊 Cookie 和 CSRF 攻击 XSS 攻击
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。
# Cookie 简介
# 1.1. 用途
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等)
# 1.2. 创建过程
服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
HTTP/1.0 200 OK Content-type: text/html Set-Cookie: yummy_cookie=mrgaogang
Set-Cookie: tasty_cookie=strawberry
客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。
GET /sample_page.html HTTP/1.1 Host: mrgaogang.github.io Cookie:
yummy_cookie=mrgaogang; tasty_cookie=strawberry
# 1.3. 分类
- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
- 持久性 Cookie:指定过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2021 07:28:00 GMT;
# 1.4 Cookie 属性 - Name 和 Value
Cookie 本质上就是一个键值对,其中 Name 表示 Cookie 的键,Value 表示 Cookie 的值。一个简单的例子
const http = require("http");
const url = require("url");
const routes = {
"/": (req, res) => res.end("hello mrgaogang"),
"/get": (req, res) => res.end(req.headers.cookie),
"/set": (req, res) => {
res.setHeader("Set-Cookie", ["name=mrgaogang", "age=30"]);
res.end("done");
},
};
// 响应网络请求
function onRequest(req, res) {
const { pathname } = url.parse(req.url);
const route = routes[pathname]; // 根据路径选择不同的路由来处理
if (route) return route(req, res);
res.statusCode = 404 && res.end("Not Found"); // 如果未匹配到路由则返回404
}
// 创建 HTTP 服务
http.createServer(onRequest).listen(3000);
# 1.5 Cookie 属性 - Domin (important)
Domain
标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。
某个域下的 · 如果希望能够被他的子域具有可见性(即可以读取),必须要注意的一点是,应该保证这个 cookie 在被 Set 的时候,应该以"."开头.
举个例子,如果当前域为
now.qq.com
,如果不设置cookie,则
domin 默认为now.qq.com
;now
是无法获取到qq.com
的cookie
信息的,如果想要获取则需要将 domin 设置为.qq.com
;
注意点:
不能在
test.com
设置 Cookie 的域为a.test.com
,不过反过来是可以的,即在a.test.com
中设置 Cookie 域为test.com
。同样,a.test.com
中不能设置 Cookie 域为b.test.com
,更不能设置成其他网站,例如baidu.com
,这样最大程度保证了安全性.Cookie 的作用域与端口号无关
Cookie 的作用域与协议(http/https)无关
所以这里千万不要跟跨域的同源策略搞混,Cookie 只区分域,不区分端口和协议,只要域相同,即使端口号或协议不同,cookie 也能共享。
# 1.6 Cookie 属性 - Path
这个属性可以指定可以共享 Cookie 的子目录,在开发中其实很少用到,基本上都不设置,默认就是 /
根目录,因为设置为根目录,所有子目录可以共享,如果指定子目录的话,其上级目录则无法访问该 Cookie,例如:
res.setHeader("Set-Cookie", [
"name=mrgaogang; Domain=mrgaogang.github.com; Path=/javascript;",
"age=10",
]);
那么这个 Cookie 只能在目录 /javascript
以及子目录 /javascript/xx/xx
中共享。如果访问其他目录,例如根目录 /
和 /ios
目录中是看不到的.
# 1.7 Cookie 属性 - Expires/Max-Age
这个属性是用得最多的,用于设置 Cookie 的有效期,如果没有设置,默认是 Session,即会话期间有效。所谓的「会话期间」是指当客户端被关闭时,cookie 就会被移除。但是一定要注意,这个不是严格意义上的浏览器关了,Cookie 就没了,因为:
很多 Web 浏览器支持会话恢复功能,用户重新打开浏览器的时候 cookie 也会恢复。
- Expires 用于指定具体的过期时间:
res.setHeader("Set-Cookie", [
`name=mrgaogang; expires=${new Date(Date.now() + 10 * 1000).toGMTString()}`,
]);
注意这里一定要是
toGMTString
,写成toISOString
的话被认为是会话级别的 Cookie。
- 而 Max-Age 则以秒为单位设置多少秒之后过期,例如 10 秒后过期:
res.setHeader("Set-Cookie", ["name=mrgaogang; max-age=10;"]);
不过需要注意:
如果 Max-Age 和 Expires 同时存在,那么 Max-Age 优先级更高。
# 1.8 Cookie 属性 - HttpOnly
为避免跨域脚本 (XSS) 攻击,通过 javascript
的 document.cookie
API 无法访问带有 HttpOnly
标记的 Cookie,它们只应该发送给服务端。如果包含服务端 Session 信息的 Cookie 不想被客户端 JavaScript 脚本调用,那么就应该为其设置 HttpOnly
标记。
res.setHeader('Set-Cookie', ['name=mrgaogang; httpOnly=true;', 'age=30'])
这个时候通过 document.cookie
是获取不到 name 值的,只能得到 age。
# 1.9 Cookie 属性 - Secure
标记为 Secure
的 Cookie
只应通过被 HTTPS
协议加密过的请求发送给服务端。
。如果服务器是 HTTP
的,但是设置了 Secure
,那么客户端是收不到这个 Cookie
的;
# 1.10 Cookie 属性 - SameSite
SameSite
: Cookie 允许服务器要求某个 Cookie 在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。
Set-Cookie: key=value; SameSite=Strict
Strict
浏览器将只发送相同站点请求的 Cookie(即当前网页 URL 与请求目标 URL 完全一致)。如果请求来自与当前 location 的 URL 不同的 URL,则不包括标记为 Strict 属性的 Cookie;Lax
在新版本浏览器中,为默认选项,Same-site Cookies 将会为一些跨站子请求保留,如图片加载或者 iframe 不会发送,而点击<a>
标签会发送;
请求类型 | 示例 | 正常情况 | Lax |
---|---|---|---|
链接 | <a href="..."></a> | 发送 Cookie | 发送 Cookie |
预加载 | <link rel="prerender" href="..."/> | 发送 Cookie | 发送 Cookie |
GET 表单 | <form method="GET" action="..."> | 发送 Cookie | 发送 Cookie |
POST 表单 | <form method="POST" action="..."> | 发送 Cookie | 不发送 |
iframe | <iframe src="..."></iframe> | 发送 Cookie | 不发送 |
AJAX | $.get("...") | 发送 Cookie | 不发送 |
Image | <img src="..."> | 发送 Cookie | 不发送 |
None
浏览器会在同站请求、跨站请求下继续发送 Cookies,不区分大小写;
需要注意的是,如果设置为 None
的话,必须开启 Secure
属性,否则会提示这个警告 ⚠️
This Set-Cookie was blocked because it had the "SameSite=None" attribute but did not have the "Secure" attribute, which is required in order to use "SameSite=None"
# Cookie 和 CSRF 攻击
# CSRF 的理解
CSRF
,中文名叫跨站请求伪造,发生的场景就是,用户登陆了 a 网站,然后跳转到 b 网站,b 网站直接发送一个 a 网站的请求,进行一些危险操作,就发生了 CSRF
攻击!
这时候,懂得这个 CSRF
了吗?我认为一部分同学依然不懂,因为我看过太多这样的描述了!
因为有这么一些疑惑,为什么在 b 网站可以仿造 a 网站的请求?Cookie 不是跨域的吗?什么条件下,什么场景下,会发生这样的事情?
这时候,我们要注意上面对 cookie 的定义,在发送一个 http 请求的时候,携带的 cookie 是这个 http 请求域的地址的 cookie。
也就是我在 b 网站,发送 a 网站的一个请求,携带的是 a 网站域名下的 cookie!很多同学的误解,就是觉得 cookie 是跨域的,b 网站发送任何一个请求,我只能携带 b 网站域名下的 cookie。
当然,我们在 b 网站下,读取 cookie 的时候,只能读取 b 网站域名下的 cookie,这是 cookie 的跨域限制。所以要记住,不要把 http 请求携带的 cookie,和当前域名的访问权限的 cookie 混淆在一起。
还要理解一个点:CSRF 攻击,仅仅是利用了 http 携带 cookie 的特性进行攻击的,但是攻击站点还是无法得到被攻击站点的 cookie。这个和 XSS 不同,XSS 是直接通过拿到 Cookie 等信息进行攻击的。
# Cookie 相关特性?
在 CSRF 攻击中,就 Cookie 相关的特性:
1、http 请求,会自动携带 Cookie。
2、携带的 cookie,还是 http 请求所在域名的 cookie。
# Cookie 如何应对的 CSRF 攻击?
明白了 CSRF 的本质,就能理解如何防御 CSRF 的攻击。
方案一:放弃 Cookie、使用 Token
由于 CSRF 是通过 Cookie 伪造请求的方式,欺骗服务器,来达到自己的目的。那么我们采取的策略就是,不使用 Cookie 的方式来验证用户身份,我们使用 Token!
Token 的策略,一般就是登陆的时候,服务端在 response 中,返回一个 token 字段,然后以后所有的通信,前端就把这个 token
添加到 http
请求的头部。
这是当前,最常用的防御 CSRF 攻击的策略。
方案二:SameSite Cookies
前面已经介绍了,只能当前域名的网站发出的 http 请求,携带这个 Cookie。
当然,由于这是新的 cookie 属性,在兼容性上肯定会有问题。
方案三:服务端 Referer 验证
我们发送的 http 请求中,header 中会带有 Referer 字段,这个字段代表的是当前域的域名,服务端可以通过这个字段来判断,是不是“真正”的用户请求。
也就是说,如果 b 网站伪造 a 网站的请求,Referer 字段还是表明,这个请求是 b 网站的。也就能辨认这个请求的真伪了。
不过,目前这种方案,使用的人比较少。可能存在的问题就是,如果连 Referer 字段都能伪造,怎么办?
# Cookie 和 XSS 攻击
我们知道攻击者可以通过一系列的手动获取到当前用户的 cookie;那么我们应该如何应对 XSS 攻击呢?
# 1. 使用 Cookie 的 HttpOnly
Cookie 有一个 http-only
属性,表示只能被 http
请求携带。
假如你的网站遭受到 XSS
攻击,攻击者就无法通过 document.cookie
得到你的 cookie 信息; 上面已经讲过我们可以设置指定的 cookie 为HttpOnly
,这样就可以避免一些隐私数据被获取到。
# 2. 数据转义
将特殊自出进行编码
以下表格详细列出了防御 XSS 所需的关键编码方法。
编码类型 | 编码机制 |
---|---|
HTML 实体编码 | 把 & 转化为 & 把 < 转化为 < 把 > 转化为 > ; 把 " 转化为 " 把 ' 转化为 ' 把 / 转化为 / |
HTML 属性编码 | 除字母数字字符外,请使用 HTML 实体&#xHH;的格式编码所有字符,包括空格。(HH=十六进制值) |
URL 编码 | 标准编码,请参阅:http://www.w3schools.com/tags/ref_urlencode.asp。 网址编码只能用于编码参数值,而不能用于 URL 的整个 URL 或路径片段。 |
JavaScript 编码 | 除字母数字字符外,请使用\uXXXX unicode 转义格式(X=整数)转义所有字符。 |
CSS 16 进制编码 | 除了字母数字字符以外,使用\HH 格式来转义 ASCII 值小于 256 的所有字符。CSS 转义支持\XX 和\XXXXXX。但是这种转义方式可能导致吃字符的问题(例如转义!1111,\XX1111 可能导致 1111 被吃掉)。有两种解决方案: (a)在 CSS 转义之后添加一个空格(将被 CSS 解析器忽略,例如\XX 1111) (b)通过零填充使 CSS 转义位数饱和(例如 \000000XX)。 |
# 3. 经验总结
以下经验总结摘自《【基本功】 前端安全系列之一:如何防止 XSS 攻击? | 美团技术团队》 (opens new window)
- 利用模板引擎
开启模板引擎自带的 HTML 转义功能。例如:在 ejs 中,尽量使用
<%= data %>
而不是<%- data %>
;在 doT.js 中,尽量使用{{! data }
而不是{{= data }
。 - 避免内联事件
尽量不要使用
onLoad="onload('')"
、onClick="go('')"
这种拼接内联事件的写法。在 JavaScript 中通过.addEventlistener()
事件绑定会更安全。 - 避免拼接 HTML 前端采用拼接 HTML 的方法比较危险,如果框架允许,使用 createElement、setAttribute 之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。
- 时刻保持警惕 在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
- 增加攻击难度,降低攻击后果 通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,降低攻击的后果。
- 主动检测和发现 可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞。
# 参考
- 本文链接: https://mrgaogang.github.io/javascript/base/%E5%86%8D%E8%81%8ACookie%E5%92%8CCSRF%E6%94%BB%E5%87%BBXSS%E6%94%BB%E5%87%BB.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!