技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# 再聊 Cookie 和 CSRF 攻击 XSS 攻击

HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 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;

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);

Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)

某个域下的 · 如果希望能够被他的子域具有可见性(即可以读取),必须要注意的一点是,应该保证这个 cookie 在被 Set 的时候,应该以"."开头.

举个例子,如果当前域为now.qq.com,如果不设置 cookie,则 domin 默认为 now.qq.com; now 是无法获取到 qq.comcookie 信息的,如果想要获取则需要将 domin 设置为.qq.com;

注意点:

  1. 不能在 test.com 设置 Cookie 的域为 a.test.com,不过反过来是可以的,即在 a.test.com 中设置 Cookie 域为 test.com。同样, a.test.com 中不能设置 Cookie 域为 b.test.com,更不能设置成其他网站,例如 baidu.com,这样最大程度保证了安全性.

  2. Cookie 的作用域与端口号无关

  3. Cookie 的作用域与协议(http/https)无关

所以这里千万不要跟跨域的同源策略搞混,Cookie 只区分域,不区分端口和协议,只要域相同,即使端口号或协议不同,cookie 也能共享。

这个属性可以指定可以共享 Cookie 的子目录,在开发中其实很少用到,基本上都不设置,默认就是 / 根目录,因为设置为根目录,所有子目录可以共享,如果指定子目录的话,其上级目录则无法访问该 Cookie,例如:

res.setHeader("Set-Cookie", [
  "name=mrgaogang; Domain=mrgaogang.github.com; Path=/javascript;",
  "age=10",
]);

那么这个 Cookie 只能在目录 /javascript 以及子目录 /javascript/xx/xx 中共享。如果访问其他目录,例如根目录 //ios 目录中是看不到的.

这个属性是用得最多的,用于设置 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 优先级更高。

为避免跨域脚本 (XSS) 攻击,通过 javascriptdocument.cookie API 无法访问带有 HttpOnly 标记的 Cookie,它们只应该发送给服务端。如果包含服务端 Session 信息的 Cookie 不想被客户端 JavaScript 脚本调用,那么就应该为其设置 HttpOnly 标记。

res.setHeader('Set-Cookie', ['name=mrgaogang; httpOnly=true;', 'age=30'])

这个时候通过 document.cookie 是获取不到 name 值的,只能得到 age。

标记为 SecureCookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。 。如果服务器是 HTTP 的,但是设置了 Secure,那么客户端是收不到这个 Cookie 的;

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"

# 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 等信息进行攻击的

在 CSRF 攻击中,就 Cookie 相关的特性:

1、http 请求,会自动携带 Cookie。

2、携带的 cookie,还是 http 请求所在域名的 cookie。

明白了 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 有一个 http-only 属性,表示只能被 http 请求携带。

假如你的网站遭受到 XSS 攻击,攻击者就无法通过 document.cookie得到你的 cookie 信息; 上面已经讲过我们可以设置指定的 cookie 为HttpOnly,这样就可以避免一些隐私数据被获取到。

# 2. 数据转义

将特殊自出进行编码

以下表格详细列出了防御 XSS 所需的关键编码方法。

编码类型 编码机制
HTML 实体编码 把 & 转化为 &amp;
把 < 转化为 &lt;
把 > 转化为 &gt;
把 " 转化为 &quot;
把 ' 转化为 &#x27;
把 / 转化为 &#x2F;
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 漏洞。

# 参考

【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM