Chromium Cookies 被清除的机制

翻译自:https://blog.yoav.ws/posts/how_chromium_cookies_get_evicted/

我最近被问及是否了解 Chromium 浏览器如何清理或回收其 Cookie。当时我并不清楚,但决定深入研究代码以找出答案。以下是我在探索过程中发现的内容,希望对他人(或至少对未来的自己)有所帮助。

这篇文章中没有太多的叙述性内容,主要是代码指引和参考资料。

TL;DR – 如果您希望某些 Cookie 比其他 Cookie 保留得更久,请将它们设置为高优先级并使用安全(Secure)标记。如果您想明确删除某些 Cookie,请设置一个已过期的 Cookie,其 “domain” 和 “path” 与要删除的 Cookie 匹配。

Cookie Monster 的限制

事实证明,Chromium 的网络堆栈中有一个被称为 “Cookie Monster” 的组件。但即使是怪兽也有其限制。在这种情况下,Cookie Monster 能持有的 Cookie 数量是有限的。它对总的 Cookie 数量(最大 3300 个)以及每个域名的 Cookie 数量(最大 180 个)都有上限。一旦达到这些限制,为了腾出空间,系统会删除一定数量的 Cookie——总共删除 300 个,或每个域名删除 30 个。

鉴于 Cookie 现在是分区存储的,每个分区域名也有相应的限制,但目前这些限制与每个域名的限制相同(即 180 个 Cookie)。

另一个有趣的常量是:在过去 30 天内被访问过的 Cookie 不会被全局清理。也就是说,其他网站无法导致您最近使用的 Cookie 被删除,但如果总的 Cookie 数量超过限制,可能会导致您较旧的 Cookie 被删除。

清理机制

Chromium 的 Cookie Monster 使用了一种变体的最近最少使用(LRU)算法来进行清理。Cookie 会根据访问的时间进行排序,最久未被访问的会首先被删除。访问时间被定义为 Cookie 的创建时间,或最后一次被使用的时间(例如,在请求头中),可能有一分钟的误差

当设置一个新的 Cookie 时,会触发垃圾回收过程删除该域名下最久未被访问的 Cookie,以及全局最久未被访问的 Cookie。Cookie Monster 会确定其 “清理目标“——即为了腾出空间需要删除多少个 Cookie。首先,所有已过期的 Cookie 会被删除。然后,清理过程会分轮次进行,直到达到清理目标。

第一轮会针对非安全、低优先级的 Cookie,然后是安全的低优先级 Cookie。至少会保留 30 个此类 Cookie,优先保留安全的。接下来,非安全的中等优先级 Cookie 会被考虑,随后是非安全的高优先级 Cookie。最后,依次考虑安全的中等优先级和高优先级 Cookie。对于中等优先级的 Cookie,至少会保留 50 个(优先保留安全的),而高优先级的则至少保留 100 个。

如果您想明确删除某些较旧的 Cookie,以便为您关心的 Cookie 腾出空间,设置一个已过期的 Cookie 会删除之前的副本,并且不会重新添加它。要设置一个匹配的 Cookie,您需要确保其 “domain” 和 “path” 与您要删除的 Cookie 完全相同

请注意:Cookie 的优先级是 Chromium 的一个专有功能。它确实很有用,因此未被标准化有些可惜。默认情况下,Cookie 的优先级为中等

总结

再次希望上述内容对您有所帮助。如果您希望确保特定的 Cookie 比其他的存留时间更长,或者您想删除旧的 Cookie,现在您知道该怎么做了(至少在 Chromium 中是这样)。我尚未深入研究其他浏览器引擎如何处理 Cookie,但我猜测 Safari 可能在操作系统层面(且是闭源的)进行处理。

最后,我希望 Cookie 的优先级功能能够被标准化。这似乎是一个有用的功能,在深入研究代码之前,我并未意识到它的存在。

更新:Mike West 告诉我,曾有一个标准化 “priority” 的草案,但未被工作组采纳。此外,”secure” Cookie 的优先级提升已包含在 RFC6265bis 中。

(本文最初发表于 2024 年 6 月 18 日,作者为 Yoav Weiss。)

Nginx 配合 CloudFlare 获取真实 IP 并限制访问

在最近一次网站部署中,考虑到安全因素,希望将源站完全隐藏在 CF 后,只允许 CF 访问,并尽可能的减少信息泄漏。

获取真实客户端 IP

由于经过 CloudFlare 代理后,Nginx 看到的 remote ip 其实是 CF 的 IP 地址。因此需要通过 Nginx 的 ngx_http_realip_module,还原出真实的客户端 IP。这一步在 CloudFlare 有详细的文档说明

简单来说,当发现请求 IP 为 CF 的 IP 段时,读取 CF-Connecting-IP 头,并将其中的 IP 设置为客户端 IP。CF 的 IP 段可以通过 API 获取。以下是一个脚本可以生成相关的配置:

#!/bin/bash

# 脚本:获取 Cloudflare IP 列表并生成 Nginx 配置

# 输出文件
IP_CONFIG_FILE="cf_ips.conf"

# 清空输出文件
> "$IP_CONFIG_FILE"

# Cloudflare IP 列表 API 端点
CLOUDFLARE_API_URL="https://api.cloudflare.com/client/v4/ips"

# 生成 set_real_ip_from 配置
echo "# Cloudflare IPv4 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv4_cidrs[]' | while read -r ip; do
    echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done

echo -e "\n# Cloudflare IPv6 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv6_cidrs[]' | while read -r ip; do
    echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done

# 添加 real_ip_header 配置
echo -e "\n# Set real IP header" >> "$IP_CONFIG_FILE"
echo "real_ip_header CF-Connecting-IP;" >> "$IP_CONFIG_FILE"

脚本运行后会生成 cf_ips.conf 文件,内含 set_real_ip_fromreal_ip_header 配置,在 httpinclude 该文件,即可自动为所有 server 自动设置真实客户端 IP。

限制仅 CloudFlare IP 访问

解决了真实客户端 IP 的问题后,接下来就是如何做到仅限 CF IP 访问。由于该 Nginx 托管了多个 server,因此并不希望直接从防火墙层面拒绝所有非 CF IP 访问。那么从 Nginx 侧,第一时间想到的就是用 ngx_http_access_module 的方式,仅放过 CF IP 段:

# Cloudflare IPv4 Allow
allow 173.245.48.0/20;
allow 103.21.244.0/22;
allow 103.22.200.0/22;
allow 103.31.4.0/22;
allow 141.101.64.0/18;
allow 108.162.192.0/18;
allow 190.93.240.0/20;
allow 188.114.96.0/20;
allow 197.234.240.0/22;
allow 198.41.128.0/17;
allow 162.158.0.0/15;
allow 104.16.0.0/13;
allow 104.24.0.0/14;
allow 172.64.0.0/13;
allow 131.0.72.0/22;

# Cloudflare IPv6 Allow
allow 2400:cb00::/32;
allow 2606:4700::/32;
allow 2803:f800::/32;
allow 2405:b500::/32;
allow 2405:8100::/32;
allow 2a06:98c0::/29;
allow 2c0f:f248::/32;

# Default deny
deny all;

但是添加了这段配置后,发现不论是否通过 CF 访问,所有请求均会被拦截,返回 403。搜索相关文章1后发现,这是因为 ngx_http_realip_module 已经先执行完毕了,此时 ngx_http_access_module 拿到的已经是被修改了的真实客户端 IP,自然无法通过检测,被拒绝访问。

为了解决该问题,我们需要使用其他手段来进行访问控制。查阅相关资料后发现,ngx_http_geo_module 是个非常好的选择。相关配置如下:

geo $realip_remote_addr $is_cf {
  default 0;
  # Cloudflare IPv4 Allow
  173.245.48.0/20 1;
  103.21.244.0/22 1;
  103.22.200.0/22 1;
  103.31.4.0/22 1;
  141.101.64.0/18 1;
  108.162.192.0/18 1;
  190.93.240.0/20 1;
  188.114.96.0/20 1;
  197.234.240.0/22 1;
  198.41.128.0/17 1;
  162.158.0.0/15 1;
  104.16.0.0/13 1;
  104.24.0.0/14 1;
  172.64.0.0/13 1;
  131.0.72.0/22 1;

  # Cloudflare IPv6 Allow
  2400:cb00::/32 1;
  2606:4700::/32 1;
  2803:f800::/32 1;
  2405:b500::/32 1;
  2405:8100::/32 1;
  2a06:98c0::/29 1;
  2c0f:f248::/32 1;
}

其中,$realip_remote_addrngx_http_realip_module 提供的变量,存储了原始的客户端地址。通过 CF 访问的情况下,这个变量存储的就是原始的 CF 地址。$is_cf 是用于储存中间结果的变量。如果原始客户端地址在 CF IP 段,则会被设置为 1,否则为 0。

有了这个变量之后,我们就可以使用 ngx_http_rewrite_module 来进行判断,并拒绝访问。在需要的地方增加如下代码:

if ($is_cf = 0) {
    return 444;
}

即可拒绝掉所有非 CF 访问的情况。

使用 OpenResty

如果在使用 OpenResty,那么有更简单的方式2可以做这个事情。借助 access_by_lua_block,我们可以直接在 lua 里访问相关变量进行判断,并拒绝非 CF 的访问:

access_by_lua_block {
    if ngx.var.remote_addr == ngx.var.realip_remote_addr then
            return ngx.exit(ngx.HTTP_FORBIDDEN)
    end
}

原理其实也很简单,如果 ngx.var.remote_addrngx.var.realip_remote_addr 是相同的,则证明 ngx_http_realip_module 没有起作用,那么说明原始访问 IP 一定不在 CF IP 段内。

注意事项

  1. 有条件的情况下(即 Nginx 所有 server 都托管在 CF),应使用防火墙直接拒绝所有非 CF IP 的 TCP 链接
  2. 由于 if 指令只能使用在 serverlocation 使用,因此该方法在多个 server 内,需要重复配置
  3. 由于 ssl_reject_handshake 无法在 if 内使用,因此如果攻击者猜对了 server_name,那么证书信息依然会泄漏,存在被探测到源站的可能

参考文档

  1. https://serverfault.com/questions/601339/how-do-i-deny-all-requests-not-from-cloudflare/826428#826428 ↩︎
  2. https://stackoverflow.com/questions/39176931/nginx-allowdeny-realip-remote-addr/39183303#39183303 ↩︎