★封锁一个 JA4 指纹值,你封锁的可能不是一个攻击者——而是所有使用同一 TLS 软件栈的合法用户。
”
以下案例改编自 tinker.expert 的博客文章[1]。
2024 年某个深夜,某公司的 SRE 盯着告警面板,入站流量图上一条几乎垂直的红线,DDoS 攻击正在进行。
他打开 WAF 日志,攻击流量几乎全部集中在一个 JA4 指纹值上。查了一下:Chrome 119,Windows。
看起来很明确。他写了一条速率限制规则,对该 JA4 值的请求每分钟限制 10 次。
30 分钟后,另一条告警响了,订单量断崖式下跌,生产流量跌了 30%。
复盘发现:那个 JA4 指纹,属于当时全球最常见的浏览器/系统组合之一,Chrome 119 + Windows。规则没拦住多少攻击者(他们很快切换了工具),倒是拦住了大量真实用户。
事故报告里有一句话:"指纹识别的是 TLS 软件栈,不是客户端个体。所有 Chrome 119 Windows 用户,指纹完全相同。"
类似的误操作在行业里反复发生,原因都一样:对 JA4 的能力边界缺乏准确认知。这篇文章就是要把这个边界讲清楚。
TLS 加密在建立之前需要先「谈判」。客户端向服务器发送一条 ClientHello 消息,宣布自己支持哪些加密套件、TLS 版本、扩展功能。
这条消息在加密开始之前就已经发出,网络链路上任何观察者都看得见:WAF、CDN、路由器,全都能看到。
ClientHello 里的信息量不小:
不同的 TLS 库(BoringSSL、OpenSSL、NSS、SChannel、Go crypto/tls)生成的 ClientHello 结构各不相同。不同版本的同一浏览器,结构也会变化。这就形成了可识别的「指纹」。
TLS 指纹最初用于威胁情报,不是 WAF 封锁:
从这里到「用 JA4 封锁 DDoS 攻击或识别恶意爬虫」,中间有一个逻辑鸿沟。很多 WAF 工程师没有充分理解这个鸿沟,就把 JA4 用在了它不擅长的场景里。
2017 年,Salesforce 研究员 John Althouse、Jeff Atkinson、Josh Atkins(三人姓名首字母,加上「3」代表三人)发布了 JA3。
算法很简单:从 ClientHello 提取 TLS 版本、cipher suites、extensions、elliptic curves、EC point formats,拼成字符串,做 MD5 哈希,得到 32 个十六进制字符。
JA3 的覆盖人群太宽了。2023 年 2 月,Fastly 发布了一篇分析 Chrome TLS ClientHello 随机化的报告[2],其中有一个数据点:
★"在 2023 年 1 月的变化之前,Chrome 在大多数平台上最常见的 JA3 指纹是
”cd08e31494f9531f560d64c695473da9。"
一个哈希值,代表了所有平台上大约 60% 的 Chrome 用户。如果在 DDoS 攻击期间,攻击者恰好使用了 Chrome 的 TLS 栈(完全可以),封锁那个 JA3 就等于封锁了互联网上几亿个正在用 Chrome 的真实用户。
JA3 的问题不是「Chrome 随机化之后才出现的」,而是从一开始就有的:同一个 JA3 值背后可能有数以亿计的合法用户。
2023 年初,Chrome 110 引入了一个改变:随机化 TLS 扩展的排列顺序。每次新连接,extensions 的顺序都不一样,导致同一个 Chrome 浏览器每次连接可能生成不同的 JA3 哈希。
随机化带来了一个看似合理的推论:「既然合法浏览器现在随机化,固定 JA3 = 可疑,对吗?」
这个推论站不住脚。
攻击者早就有了随机化工具。uTLS[3] 的 HelloRandomized 模式,以及 curl_cffi[4] 的 tls_permute_extensions=True,让攻击者在 Chrome 引入随机化之前就已经能随机化扩展顺序。「固定顺序 = 攻击者」的假设从来就不成立。
Firefox 至今没有默认启用随机化。Mozilla 在 2023 年初将随机化代码合并进了 NSS 库(Bug #1789436),但触发该功能的 Firefox 偏好设置 Bug #1816878[5] 至今仍处于 NEW/未分配状态。也就是说,Firefox 在默认情况下仍然使用固定的扩展顺序。如果「固定顺序 = 可疑」,那么所有 Firefox 用户都是嫌疑人。
Safari 同样从不随机化。Safari 使用 Apple 的 Security.framework,扩展顺序完全固定,每个 iOS/Safari 版本对应一个稳定的 JA3 值。
因此,「固定 JA3 = 可疑」是个危险的偏见。主流浏览器里只有 Chrome 随机化了;攻击工具也完全可以模拟固定或随机的 extensions 顺序。
FoxIO 的 John Althouse(也是 JA3 原作者)于 2023 年 9 月发布了 JA4,其核心改变是规范化(Normalization):
a_b_c 三段,可单独比对,支持部分匹配一个 JA4 值的示例:
t13d1516h2_8daaf6152771_02713d6af862各字段含义:
t | q = QUIC,d = DTLS) | |
13 | ||
d | i = IP 地址 | |
15 | ||
16 | ||
h2 | ||
8daaf6152771 | ||
02713d6af862 |
JA4 的「稳定性」让它适合流量分类,但也决定了它不能单独用于封锁。因为 JA4 识别的是 TLS 软件栈版本,不是个人:
注:Chrome 从 2024 年起逐步灰度启用后量子密码学(PQC)key agreement,处于不同灰度组的用户可能产生不同 JA4,详见 4.5 节
这不是设计缺陷,这是设计本意。JA4 就是用来识别「什么软件在访问」,不是「谁在访问」。但 WAF 封锁需要的恰恰是后者。
封锁一个 JA4 = 封锁所有使用该软件版本的用户。前言里那个工程师踩的就是这个坑。
在 DDoS 攻击中,如果一个 JA4 值的请求量极高,它有可能攻击者的 JA4,也有可能是合法客户端的 JA4——因为攻击者在模拟它。
这个推理的前提是:攻击者有意规避 JA4 检测,因此选择模拟最常见的客户端指纹。如果攻击者不在乎被识别(比如用默认 Python 脚本的低级攻击),他们的 JA4 会是一个不常见的值,反而容易被黑名单拦截。但有能力发起大规模 DDoS 的攻击者,也有可能会做指纹伪装。下面描述的就是这种情况:
safari_ios 预设可以覆盖;Android OkHttp 没有现成预设,但 curl_cffi 支持通过 ja3=... 和 extra_fp=... 手动指定任意 TLS 指纹,技术上同样可行这就是那位 SRE 踩坑的原因:他看到的「高 RPS JA4」其实就是 Chrome 119/Windows 的指纹,合法流量最多的 JA4 之一。
4.1 节提到攻击者会模拟常见客户端的 JA4。这个门槛到底有多低?看一下实际工具就知道了。
★声明:以下展示的工具和技术均来自公开的开源项目文档,展示目的是帮助防御方理解攻击面,以便制定更有效的防护策略,而非鼓励滥用。
”
curl_cffi 是目前最主流的 TLS 指纹伪装工具。基于 curl-impersonate[6],在编译层深度修改 curl,使其使用真实浏览器的 TLS 库(例如 Chrome 的 BoringSSL),完整复现目标浏览器的 TLS 设置:
from curl_cffi import requests# JA4 伪装成 Chrome 145,User-Agent 也自动设置成 Chrome 145resp = requests.get("https://target.com", impersonate="chrome145")截至 2026 年 3 月(v0.15.0 beta),curl_cffi 支持多个主流浏览器版本的 TLS 指纹预设,覆盖 Chrome(99 到 145)、Safari(15.3 到 26.0,含 iOS)、Firefox(133 到 147),以及 Tor Browser。
攻击者用一行代码,就能让请求在 JA4 层面与真实 Chrome 145 用户完全一致。
curl_cffi 对 JA4 各段的伪装效果:
JA4 c 段的差异来源:签名算法扩展
c 段的计算方式是:SHA256(排序后的扩展列表 + 排序后的 signature_algorithms)
其中 signature_algorithms(TLS 扩展类型 13)是客户端声明支持的签名算法列表。Safari 等浏览器为了兼容仍运行着 SHA-1 证书的老旧服务器,仍然在 ClientHello 里列出遗留算法(如 0x0203 = SHA-1 + ECDSA)。curl_cffi 的默认列表省略了这些遗留条目,导致 c 段哈希不同。
修复方法:extra_fp 参数
curl_cffi 提供了 extra_fp 参数,用于填补这些细节差异:
# 伪代码,仅示意——safari18_ja3 需替换为实际的 JA3 字符串# 修复 Safari JA4 c 段不匹配:补充遗留 SHA-1 签名算法extra_fp = {"tls_signature_algorithms": ["ecdsa_secp256r1_sha256", "rsa_pss_rsae_sha256", "rsa_pkcs1_sha256","ecdsa_secp384r1_sha384", "ecdsa_secp521r1_sha512","rsa_pss_rsae_sha384", "rsa_pss_rsae_sha512","rsa_pkcs1_sha384", "rsa_pkcs1_sha512","ecdsa_sha1", # 0x0203 — Safari 仍广播以兼容老服务器"rsa_pkcs1_sha1", # 0x0201 ]}resp = curl_cffi.get(url, ja3=safari18_ja3, extra_fp=extra_fp)对于 Chrome,正确的设置是启用 GREASE 和扩展随机化:
extra_fp = {"tls_grease": True, # Chrome 注入 GREASE 值"tls_permute_extensions": True, # 模拟 Chrome 的每次连接扩展随机化}resp = curl_cffi.get(url, impersonate="chrome145", extra_fp=extra_fp)uTLS 是一个 Go 语言的 TLS 库,提供了比 curl_cffi 更激进的指纹策略:
// 每次连接生成全新的随机 TLS 指纹uTlsConn := tls.UClient(tcpConn, &config, tls.HelloRandomized)HelloRandomized 不只是随机化扩展顺序,而是随机选择哪些 cipher suites 和哪些 extensions 出现,并随机排列两者的顺序。结果是每次连接都是不同的 JA3 和不同的 JA4。
这与 Chrome 的随机化有本质区别:Chrome 只随机化扩展的顺序,而 JA4 对顺序不敏感,所以 Chrome 的随机化对 JA4 无效。但 uTLS HelloRandomized 连扩展集合本身也随机化,导致 JA4 也随之变化。
前面几节主要围绕桌面浏览器。但现实中,WAF 面对的流量远不止浏览器,移动端原生 App 和服务器到服务器的 API 调用占了大量份额。这两类流量在 JA4 层面有一个共同点:指纹完全固定,多样性很低。
桌面浏览器的 JA4 至少还会随版本迭代变化。Chrome 每次更新 TLS 配置(比如 2024–2025 年间引入后量子密码学 PQC 支持),JA4 的 b 段和 c 段哈希就会改变。但每个版本内,所有用户的 JA4 仍然完全相同。移动端和 API 流量更极端:
iOS URLSession 不随机化 TLS 扩展,所有运行同一 iOS 版本的 iPhone 指纹完全相同。Android OkHttp 的 cipher suites 列表由 ConnectionSpec 明确定义,每个版本指纹确定。Python requests、Node.js、Java HttpClient 同样不随机化,每个库/OpenSSL 版本组合对应一个固定指纹。
对于移动端和 API 流量,由于其 JA4 的多样性很低,封锁任何一个常见 JA4 就是误封整个用户群体。偏偏这些流量是很多 web 应用的主要来源。
不过反过来看:多样性低也意味着已知合法软件的 JA4 集合是有限的、可枚举的,维护白名单的成本反而可控。
既然单一 JA4 值信息量不够,能不能通过维护指纹库(黑名单、白名单、信誉库)来弥补?行业已经在这样做了。JA4 采用 BSD 许可,自 2023 年发布以来,形成了一个由原厂数据库引领、头部安全厂商跟进的生态。
这个生态的主干是 FoxIO 维护的 ja4db.com 指纹数据库,收录了一些已知恶意客户端和合法浏览器/应用的指纹。威胁情报平台(VirusTotal、GreyNoise、Hunt.io)用 JA4 追踪恶意软件家族和 C2 基础设施;WAF/CDN 厂商(Cloudflare、F5、长亭科技等)在产品中集成了 JA4 匹配和信誉评估;NDR/SIEM 平台(NetQuest、TheHive)用于加密流量的威胁捕获。
这个生态有价值,但价值有边界。对 WAF 工程师来说,更重要的问题是「JA4 指纹库能帮我做什么、不能做什么」。
JA4 黑名单收录的是已知恶意工具的原生指纹。使用默认 Python/OpenSSL、Go crypto/tls、特定恶意软件 TLS 库的客户端,其 JA4 与任何主流浏览器都不同。
黑名单有效的场景:
t10d070600_c50f5591e341_1a3805c3aa63,这个指纹对应 .NET 的旧版 TLS 实现,与任何浏览器都不同)这些场景有一个共同特征:攻击工具使用的 TLS 栈与主流浏览器明显不同,黑名单命中不会误杀合法用户。
黑名单失效的场景:
如第四节所述,当攻击者用 curl_cffi 伪装成 Chrome 145 时,他们的 JA4 就是合法的 Chrome 145 指纹,黑名单无从区分。黑名单的设计前提是「恶意指纹与合法指纹不重叠」,伪装直接打破了这个前提。
JA4 黑名单能拦住不伪装的攻击者,拦不住愿意花一行代码伪装的攻击者。黑名单的定位是基础防线,过滤大量低级自动化流量,但不能作为对抗高级攻击者的主要手段。
4.5 节提到,移动端和 API 流量的 JA4 多样性极低。所有同版本 iOS 的 URLSession、同版本 OkHttp 的 Android App,指纹完全相同。反过来说:已知合法软件的 JA4 集合是有限的、可枚举的,维护白名单成本可控。
FoxIO 的指纹数据库收录了主流浏览器和应用的合法指纹,正是为此目的。白名单的逻辑是:如果一个请求的 JA4 匹配已知合法软件,给予更高的信任分;如果不匹配任何已知软件,标记为可疑。
白名单有效的场景:
白名单的局限:
白名单能回答「这个 JA4 是否属于已知合法软件」,但回答不了「使用这个合法软件的是不是合法用户」。curl_cffi 伪装后的 JA4 与真实 Chrome 145 完全一致,白名单同样无法区分。移动端也不例外:4.1 节提到,curl_cffi 的 safari_ios 预设可以直接伪装 iOS URLSession 的指纹,extra_fp 参数可以手动构造 OkHttp 的指纹,白名单里的移动端条目同样会被攻击者命中。这是 JA4 本身的属性:它识别 TLS 软件栈,不是使用者的身份或意图。
JA4 指纹库(黑名单、白名单、信誉库)在多层防御体系中有用:黑名单过滤低级自动化流量,白名单为信任评估提供基准,信誉库为异常检测提供上下文。
但指纹库不改变 JA4 的属性:它识别的是软件栈,不是个人。所有基于 JA4 的策略,黑名单封锁、白名单放行、信誉评分,都建立在「JA4 值能区分合法与恶意流量」的假设上。攻击者用 curl_cffi 或 uTLS 伪装后,这个假设就不成立了。
所以行业共识正在收敛到同一个方向:JA4 是多信号模型中的一个输入,不是独立的决策标准。真正可靠的区分手段,需要验证客户端的实际能力(能否执行 JavaScript、浏览器环境是否真实),而不是它声称的身份。
理解了 JA4 的局限,下一个问题是:那该怎么做?
JA4 适合做的事:
JA4 不能可靠做到的事:
impersonate="chrome145",他们的 JA4 与真实 Chrome 145 用户的 JA4 相同或高度相似,WAF 无法区分如果需要基于 JA4 限速,不要用 JA4 作为单独的限速 key,这会误杀所有使用该 TLS 栈的合法用户。应该用 IP + JA4 的组合 key,把「同一 IP 上的同一软件栈」作为限速单元。合法用户来自不同 IP,不会被波及。
但需要清醒认识 IP + JA4 组合限速的局限:
IP + JA4 组合 key 能正确聚合并触发阈值IP + JA4 组合限速的核心价值是减少误杀,而非有效拦截所有类型的攻击者。它是多层防御中的一环,不是银弹。
既然 JA4 不够用,那用什么?
JavaScript Challenge 是目前对抗恶意 bot 最有效的手段之一。WAF 向客户端发送一段 JavaScript 代码,客户端必须在真实浏览器环境中执行。代码会收集 Canvas API 渲染结果、WebGL 指纹、屏幕分辨率等多维度特征,判断客户端是否是真实浏览器。通过挑战后,WAF 颁发加密 token,后续请求携带 token 即可。
无法执行 JavaScript 的客户端则拿不到 token。curl_cffi 没有 JS 引擎,过不了挑战;即使攻击者用 headless browser,挑战代码也会检测环境异常。JS Challenge 比 JA4 可靠,因为它验证的是实际的浏览器环境,不是可以伪造的 TLS 字段。
以 AWS WAF 为例:反 DDoS 托管规则组(AntiDDoS AMR)在检测到攻击时自动对浏览器流量发起 Challenge,无法通过的请求被拦截;高级 bot 管理方案(Targeted Bot Control)则利用 token 追踪 session 行为,做行为分析和置信度评估,能识别已经通过基础 TLS 指纹检测的高级 bot。
但 JS Challenge 有一个前提:客户端得能执行 JavaScript。Native App(iOS URLSession、Android OkHttp)、服务器到服务器的 API 调用、静态文件下载,这些流量都跑不了 JS。于是就有了一个架构层面的问题:流量类型分离。
如果浏览器流量和 API 流量共用同一个域名,差异化安全策略很难部署。正确做法是域名分离:
www.example.com → 浏览器流量 → 速率限制 + JS Challenge + 反 DDoS 规则组(默认配置)+ bot 管理方案api.example.com → API 流量 → 速率限制 + 反 DDoS 规则组(调整配置)cdn.example.com → 静态资源 → 速率限制 + 反 DDoS 规则组(调整配置)通过不同的 CDN 域名挂载不同的 WAF Web ACL,就可以对不同流量实施不同策略。这个架构决策影响整个安全策略的有效性,设计阶段没做好流量分离,后期 WAF 层面的精细化配置都会受限。
WAF 在网络层还能检测到比 JA4 更多的信号,组合使用效果更好:
:method → :authority → :scheme → :path 的顺序,与声称的浏览器类型不符是可检测的异常Sec-Fetch-Site、Sec-Fetch-Mode、Sec-Fetch-Dest 等 Fetch Metadata 头是可疑信号这些信号在 WAF 中主要由高级 Bot Control 的规则集自动处理,不需要手动配置。但了解它们有助于理解为什么多信号模型比单一 JA4 检测靠谱得多。
攻防从来不对称:防守方要守住所有边界,攻击方只需要找到一个缺口,或者更简单,更新一行 impersonate="chrome145"。
对 WAF 工程师来说,JA4 是个有用的诊断工具,也是多层防御中的一环。但一旦把它当成单独的封锁标准,你就同时招来两种后果:误封合法用户,以及迫使攻击者更新工具或调整 TLS 设置,而后者对他们来说只需要几秒钟。
有效的防御不是找到一个更难伪造的指纹,而是验证客户端的实际能力。JA4 在这个体系里有它的位置,但不是你以为的那个位置。
How I Took Down 30% of Production with One TLS Fingerprinting Rule: https://tinker.expert/blog/ja4-fingerprinting-network-security
[2]Fastly:Chrome TLS ClientHello 随机化分析: https://www.fastly.com/blog/a-first-look-at-chromes-tls-clienthello-permutation-in-the-wild
[3]uTLS: https://github.com/refraction-networking/utls
[4]curl_cffi: https://github.com/lexiforest/curl_cffi
[5]Mozilla Bug 1816878 - Firefox 扩展随机化默认启用(未解决): https://bugzilla.mozilla.org/show_bug.cgi?id=1816878
[6]curl-impersonate: https://github.com/lwthiker/curl-impersonate