Source: https://bscan.info/zh/blog/language-leak
Language: zh-Hans

浏览器发出的每一个请求都带着 Accept-Language 请求头，而 VPN 会把它原封不动地送达。在本页实时测一测你的语言与 IP 是否对得上。

# 语言泄露：VPN 改不了你的浏览器语言

发布于 2026年7月20日

把流量从阿姆斯特丹绕一圈，Google 就会用荷兰语向你问好。几乎每个用过 VPN 的人都碰到过这个恼人的现象，然后耸耸肩，把它归为 VPN 的怪毛病。这不是怪毛病。这正是本文要讲的那个泄露，只是方向反了过来。

那个请求自己就前后矛盾。数据包来自一个荷兰 IP 地址；请求头却写明了你真正阅读的语言：`Accept-Language: en-US,en;q=0.9`。在挑选界面语言这件事上，Google 相信的是 IP。而结账页面上的欺诈脚本会把同一个比对反过来跑：它检查语言和 IP 是否对得上，然后把答案记进你的风险评分。本页上的小工具此刻就在对你的浏览器跑这项检查。

**简而言之：**你的浏览器在发出的每一个请求里宣告你阅读的语言，装在一个 VPN 会一字不差照原样送达的请求头里。拿这份列表和 IP 所在国家比对，成本比时区检查还要低：读时区起码得跑一行 JavaScript，语言却本来就躺在请求头里。而只伪装一半（改了请求头没改 JavaScript，或者反过来）会把“不匹配”升级成“自相矛盾”，后者扣的分只会更多。

## Accept-Language：每个请求都带的请求头

无论你的浏览器向服务器要什么——页面、图片还是字体——它都会附上 `Accept-Language` 这个 HTTP 请求头。它存在的意义，是让服务器能挑一种你看得懂的语言；它的内容来自浏览器设置里的语言列表，而这份列表的初始值，早在浏览器安装时就由操作系统的区域设置定好了。`q=0.9` 这种记法给列表排出了优先级：这台浏览器最偏好美式英语，普通英语紧随其后。完整的语法规则见 [MDN 的 Accept-Language 页面](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language)。

这个请求头就在你发出的第一个请求里。不用等某个脚本加载，也不用等某个 cookie 落地。服务器读到它的同一瞬间，也读到了数据包上的源 IP 地址：

第一个请求就已经回答了两次

1 数据包从哪里来

源 IP `198.51.100.7` 地理位置数据库 荷兰阿姆斯特丹

2 请求内容写了什么

GET /checkout HTTP/1.1
Host: shop.example
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
Accept-Language: en-US,en;q=0.9  ◄ 浏览器自己的语言列表

1 IP：荷兰 vs 2 请求头：一位读英语的人

同一个请求里的两份位置声明，在任何一行 JavaScript 运行之前就已被放在一起比对。

Accept-Language 请求头搭载在浏览器发出的每一个 HTTP 请求上。服务器读到它时，源 IP 地址就在旁边，这场比对因此毫无成本。

JavaScript 通过 `navigator.language` 拿到同样的信息，通过 `navigator.languages` 拿到完整列表，全程没有任何权限弹窗。注意它和时钟的区别：脚本得先跑起来，才能 [问出你的时区](https://bscan.info/zh/blog/timezone-leak)；而你的语言在页面还没有任何脚本可跑之前就已经送到了。

## 为什么 VPN 改不了你的浏览器语言

在这件事上，VPN 恰恰无能为力。隧道负责搬运你的数据包，不负责翻译它们。浏览器写下什么就送达什么，包括那个写着“我的用户读英语，不读荷兰语”的请求头。至于改写它，根本无从谈起：在任何 HTTPS 网站上，这个请求头都封在你的浏览器与网站之间的 TLS 会话里传输，VPN 打不开这层加密。

## 不匹配能证明什么，又不能证明什么

语言是个软信号，比时区还软。对操作系统而言，英语是最接近“出厂设置”的语言，所以阿姆斯特丹 IP 配上 `en-US`，单看不会让任何人皱眉。比利时有三种官方语言，瑞士有四种。外派人员和远程工作者读的语言和邻居不一样，天经地义。任何一套认真的检测系统都清楚这些，所以语言不匹配只会轻轻推一下评分，仅此而已。

但这个信号有一个尖锐的方向。荷兰出口 IP 背后一台偏好 `pt-BR` 的浏览器，可没有英语那种模糊空间：它说明键盘前的这个人读巴西葡萄牙语，而荷兰的 IP 地址要多少有多少，把葡萄牙语排在第一位的家庭却寥寥无几。再叠上一个数据中心 IP，和一只仍设在 `America/Sao_Paulo` 的时钟，那些声称来自阿姆斯特丹的数据包就彻底输掉了这场争辩：

评分脚本从这对组合里读出什么

1.  `nl-NL, nl` + 阿姆斯特丹 IP
    
    一致 两者讲的是同一个故事。没什么可扣分的。
    
2.  `en-US, en` + 阿姆斯特丹 IP
    
    含糊 英语浏览器遍地都是。单独看是个弱信号，但当其他信号叠上来时，它也会被计入。
    
3.  `pt-BR, pt` + 阿姆斯特丹 IP
    
    不匹配 浏览器指向巴西，数据包却声称在荷兰。这下你变得有意思了。
    
4.  请求头： `nl-NL` + JavaScript： `pt-BR`
    
    检测到伪装 浏览器和自己吵了起来，而除了蓄意篡改，几乎没有什么能造成这种局面。
    

评分系统要的不是铁证，而是方向。梯子每往下一级，风险就加一分，而最底下那一级是专门留给做了一半的伪装的。

还有第二重更安静的代价。你那份列表的确切构成——哪些语言、按什么顺序、配什么 `q` 值——本身就是指纹材料。[EFF 的 Panopticlick 研究](https://coveryourtracks.eff.org/static/browser-uniqueness.pdf) 测得 Accept 系列请求头约携带六比特的身份信息，Accept-Language 指纹识别正是靠这一点成立的：`en-US,en;q=0.9` 能藏进人群；`nl-BE,fr-BE;q=0.9,en;q=0.8,de;q=0.7` 几乎就是一枚签名。作为对比，[你的字体列表](https://bscan.info/zh/blog/font-fingerprinting)泄露的比特数是它的两倍多——但字体得靠脚本去测量，而这个请求头在每一次请求里都主动送上门。

浏览器厂商对此心知肚明。正因为如此，Safari 只发送你排第一的那一种语言；Chrome 的工程师提出了 [把这个请求头裁剪到只剩一种语言的提案](https://github.com/explainers-by-googlers/reduce-accept-language)；Brave 则会 [按站点刻意模糊自己的回答](https://brave.com/privacy-updates/17-language-fingerprinting/)。不过这些对不匹配问题都无济于事：裁剪之后幸存下来的那一种语言，仍然是你的语言。

## 为什么一开 VPN，Google 就说错语言

回到那个说荷兰语的 Google。既然浏览器在每个请求里都客客气气地报上了自己的语言，为什么你一连上 VPN，网站就对它视而不见？因为它们更信任 [IP 地理定位](https://bscan.info/zh/blog/ip-geolocation-accuracy)，而且远在你用上 VPN 之前就已如此。Google 在 [2017 年的一则公告](https://blog.google/products/search/making-search-results-more-local-and-relevant/) 里把这话挑明了：从那时起，它不再用 google.nl 这类域名来区分国家版本，而是检测到什么位置，就端出哪一版。请求头陈述的是偏好；IP 被当作事实。在 Google 首页上让你恼火的这套优先次序，正是你的请求头和 IP 不一致时欺诈系统套用的那一套：IP 获胜，你的请求头则从“偏好”被降级成“证据”。

如果你真正想解决的只是这个烦恼，那答案在账号设置里，而不在 VPN 设置里：在 Google 和多数大型网站上，登录后的语言偏好通常能压过 IP 猜测。但这个泄露本身没有任何设置页能修，因为泄露的就是请求头本身。

## 又见半吊子伪装的陷阱

读过时区那篇文章的人已经见识过这个陷阱。它在这里长得一模一样，还更容易踩：语言存在于两个地方——网络上的 `Accept-Language` 请求头，和 JavaScript 引擎里的 `navigator.languages`。在正常的浏览器里，它们是同一项设置的两个侧面，永远一致。而造假的工具往往顾了一头，忘了另一头。

伪装扩展大多只覆盖 JavaScript 这一侧，因为那是扩展伸手就够得着的地方；你的请求头则继续向每一台服务器坦白真相。改写请求头的代理正好相反，任由 `navigator.languages` 向每个来问的脚本报出你的真实列表。无论哪一种，浏览器都开始自相矛盾，而这种矛盾在检测系统眼里比原本的不匹配值钱得多：普通 VPN 用户成天制造不匹配，能制造出自相矛盾的却只有蓄意篡改。此外，被覆盖过的 `navigator.languages` getter，在脚本查看它的源码时不会再报出 `[native code]`，这正是 [我们的机器人检测文章](https://bscan.info/zh/blog/bot-detection) 花了一整节来讲的破绽。

唯一大规模行得通的伪装是最无聊的那一种：在默认英文配置下，Tor Browser 对所有人都回答 `en-US,en;q=0.5`，靠着让你和其他每一个 Tor 用户长得一模一样来藏起你的语言。这笔交易——用“一眼就能看出是 Tor”换来保护——正是那些半吊子扩展假装能提供的东西的诚实版本。

## 到底该怎么做

1.  **先跑一遍检查。** 本页上的小工具此刻就在拿你浏览器的语言和 IP 所在国家做比对，用的正是欺诈脚本那套“这个国家该有哪些语言”的预期。请开着 VPN 测，因为你的风险评分就是基于这套配置算出来的；然后接着做一次 [完整浏览器扫描](https://bscan.info/zh)，把相邻的那几项检查一并跑完：时区、WebRTC、指纹。
2.  **让你的语言跟着 VPN 走。** 干净的修法和时区那篇的建议如出一辙：把声明变成真的。把出口国家的语言加进浏览器的语言列表——Chrome 在 `chrome://settings/languages`，Firefox 在“首选语言”里——愿意的话尽管把母语留在旁边：多数系统跑的检查是“有没有一种语言对得上”，而不是“是不是每种都对得上”。只是务必在浏览器自己的设置里改，别用扩展，这样请求头和 JavaScript 才会一起变。
3.  **如果你运营着多个身份，这就不是设置页面能干的活了。** 每个配置文件的语言、时区和 IP 都得讲同一个故事，而且每一次会话都得如此；逐个身份手改设置的做法，一碰上真实的工作量就会散架。[Incogniton](https://www.incogniton.com/?utm_source=bscanInfo&utm_medium=Website&utm_campaign=LanguageLeakBlog) 会按每个配置文件的代理来设定它的语言，请求头和 JavaScript 一步到位，让浏览器永远不会和自己的数据包吵起来。

你的浏览器刚刚已经把你读什么语言告诉了你访问的上一台服务器。上面的小工具会告诉你，你的 IP 有没有为这句话背书。

[运行完整浏览器扫描](https://bscan.info/zh)

## 继续阅读

[

![时区泄露：VPN 换掉的是你的 IP，不是你的时钟](https://bscan.info/timezoneCover.svg)

时区泄露：VPN 换掉的是你的 IP，不是你的时钟

](https://bscan.info/zh/blog/timezone-leak)[

![你会被识别为机器人吗？测试你的浏览器是否泄露自动化特征](https://bscan.info/botCover.svg)

你会被识别为机器人吗？测试你的浏览器是否泄露自动化特征

](https://bscan.info/zh/blog/bot-detection)[

![一个浏览器，五种操作系统声明：为什么平台伪造总是露馅](https://bscan.info/platformCover.svg)

一个浏览器，五种操作系统声明：为什么平台伪造总是露馅

](https://bscan.info/zh/blog/platform-spoofing)

---
This is the page's server-rendered documentation. Interactive browser tests, file processing and permission prompts require the HTML page; Markdown does not contain live measurements or process files.
