BreakHub
راهنماهااثر انگشت مرورگر

زوال User-Agent و ظهور Client Hints

رشتهٔ User-Agent سی سال بار سازگاری را با خود حمل می‌کند و Chrome آن را به یک قالب تقریباً خالی منجمد کرده است. جزئیات به Client Hints منتقل شده. اینجا می‌بینید هر کدام چه چیزی را فاش می‌کند و سایت‌ها چطور مرورگری را که UA آن ویرایش شده می‌گیرند.

7 دقیقه مطالعه · انتشار ۱۹ شهریور ۱۴۰۵

هر Chrome‌ای را باز کنید و navigator.userAgent را چاپ کنید. رشته‌ای می‌گیرید که با خودش در تناقض است: با Mozilla/5.0 شروع می‌شود، در میانه به AppleWebKit و «KHTML, like Gecko» اشاره می‌کند و با Safari/537.36 تمام می‌شود. یک Chrome که هم‌زمان ادعا می‌کند خویشاوند Netscape، Safari و Firefox است. این باگ نیست. فسیلی است که از دهه‌ها ترفند سازگاری روی هم انباشته شده، و فهمیدن اینکه چطور به این شکل درآمده، چرا حالا منجمد می‌شود و سایت‌ها به‌جایش از چه استفاده می‌کنند، پیش‌زمینه‌ای است که برای خواندن بخش مرورگر گزارش اثر انگشت ما لازم دارید.

رشته‌ای که هیچ‌کس به آن اعتماد ندارد

ایدهٔ اولیهٔ User-Agent متواضعانه بود: به سرور بگوید چه نرم‌افزاری درخواست می‌دهد تا چیز مناسبی تحویل دهد. در دههٔ نود کار خراب شد، وقتی سایت‌ها صفحات «پیشرفته»شان را به وجود «Mozilla» در UA مشروط کردند. Internet Explorer بلافاصله خودش را Mozilla نامید و هر مرورگر بعدی همین ترفند را کپی کرد. وقتی Chrome عرضه شد، می‌خواست هر سایتی که دنبال «Safari» یا «Gecko» می‌گشت همچنان کار کند، پس همهٔ آن نشانه‌ها را هم در رشته چپاند. نتیجه رشته‌ای است که تقریباً هر نشانه‌اش دروغ است و تنها بخش‌های صادقانه‌اش Chrome/1xx در انتها و بخش پلتفرم داخل پرانتز است.

بدتر اینکه UA نسخهٔ سیستم‌عامل، معماری CPU و در Android، مدل دقیق دستگاه را هم حمل می‌کرد. تقریباً هیچ سایتی به این‌ها نیاز ندارد، اما هر ردیابی آن‌ها را رایگان می‌گیرد.

کاهش UA در Chrome

تقریباً بین ۲۰۲۲ و ۲۰۲۳، Chrome رشتهٔ UA را مرحله‌به‌مرحله کوچک کرد. UA یک Chrome امروزی تقریباً این شکلی است: Windows همیشه Windows NT 10.0; Win64; x64 گزارش می‌دهد، چه Windows 10 داشته باشید چه 11؛ macOS حتی روی Apple Silicon به Intel Mac OS X 10_15_7 سنجاق شده؛ Android روی Android 10 منجمد شده و مدل دستگاه با یک حرف K جایگزین شده؛ و نسخهٔ Chrome فقط عدد اصلی‌اش را نگه می‌دارد، پس چیزی مثل Chrome/128.0.0.0 می‌بینید.

بنابراین UA یک Chrome امروزی سه چیز به شما می‌گوید و نه خیلی بیشتر: پلتفرم تقریبی، موبایل بودن یا نبودن، و نسخهٔ اصلی Chrome. هدف همین بود. UA را به آنتروپی پایین برسانند و جزئیات را به سازوکاری منتقل کنند که باید درخواست شود.

Client Hints: چه چیزی به‌طور پیش‌فرض و چه چیزی با درخواست ارسال می‌شود

Client Hints (UA-CH) همان سازوکار است و اطلاعات مرورگر را به دو سطح تقسیم می‌کند.

سه hint کم‌آنتروپی با هر درخواست می‌رود: Sec-CH-UA (برندها و نسخه‌های اصلی، مثلاً "Chromium";v="128", "Google Chrome";v="128"Sec-CH-UA-Mobile (?0 یا ?1) و Sec-CH-UA-Platform ("Windows"، "macOS"، "Android" و مانند آن). این‌ها روی هم به‌عمد تقریباً به اندازهٔ UA کاهش‌یافته لو می‌دهند.

hintهای پرآنتروپی مگر با درخواست ارسال نمی‌شوند: Sec-CH-UA-Platform-Version (که Windows 10 را از 11 تشخیص می‌دهد)، Sec-CH-UA-Arch، Sec-CH-UA-Bitness، Sec-CH-UA-Model (مدل واقعی دستگاه)، Sec-CH-UA-Full-Version-List (شماره نسخه‌های کامل) و Sec-CH-UA-WoW64. سرور با پاسخ دادن با هدر Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model آن‌ها را به دست می‌آورد و پس از آن مرورگر آن‌ها را به درخواست‌های بعدی ضمیمه می‌کند. اسکریپت صفحه هم می‌تواند navigator.userAgentData.getHighEntropyValues(['platformVersion', 'model']) را صدا بزند و آن‌ها را در یک Promise بگیرد.

یک سوءتفاهم را همین‌جا باید روشن کرد. «نیاز به مجوز» به معنای پنجره‌ای نیست که رویش کلیک کنید. اعلانی است: سایت می‌گوید این مقادیر را می‌خواهد و مرورگر بدون پرسیدن از شما تحویل می‌دهد. چیزی که به دست می‌آورید این است که به‌طور پیش‌فرض غایب‌اند. یک لاگ درخواست غیرفعال هیچ‌کدام را ندارد؛ هر که بخواهد باید صریحاً بپرسد، و پرسیدن صریح قابل ممیزی و محدود کردن با سیاست است. همچنین توجه کنید که در زمان نوشتن این متن، نه Firefox و نه Safari هیچ‌کدام UA-CH را پیاده نکرده‌اند و هیچ‌کدام UA خود را به اندازهٔ Chrome منجمد نکرده‌اند. «Client Hints جایگزین UA می‌شود» فعلاً بیشتر داستان Chromium است.

آن مورد عجیب «Not A(Brand»

به Sec-CH-UA نگاه کنید و در میان برندها چیزی می‌بینید که شبیه خرابی داده است، مثل "Not A(Brand";v="99" یا "Not/A)Brand";v="8". این GREASE است، نویز عمدی. انگیزه‌اش عملی است. اگر توسعه‌دهنده‌ای بنویسد if (brands[0] === "Chromium")، هر برند جدید یا تغییر ترتیب سایتش را خراب می‌کند، دقیقاً همان‌طور که جستجوی «Mozilla» دهه‌ها پیش کرد. پس Chrome یک برند ساختگی را در موقعیتی تصادفی با نسخه‌ای بی‌معنا تزریق می‌کند و همه را از روز اول مجبور می‌کند تجزیه‌گر تحمل‌پذیر بنویسند.

تشخیص موتور: ادعای UA کمتر از رفتار مرورگر اهمیت دارد

تغییر UA پیش‌پاافتاده است. یک افزونه، یک پروکسی یا صفحهٔ تنظیمات هر مرورگر ضدشناسایی با یک کلیک این کار را می‌کند. بنابراین سیستم‌های ریسک جدی به آنچه UA می‌گوید اهمیت نمی‌دهند و به آنچه مرورگر انجام می‌دهد نگاه می‌کنند. این تشخیص موتور است.

چند خانواده پروب معمول است:

  • window.chrome فقط در مرورگرهای مبتنی بر Chromium وجود دارد. Firefox قدیمی InstallTrigger را داشت (در نسخه‌های اخیر حذف شده، پس آن پروب دارد کم‌رنگ می‌شود). Safari اشیای مخصوص WebKit مثل ApplePayError و safari.pushNotification را دارد.
  • پشتیبانی از ویژگی‌های CSS. true بودن CSS.supports('-moz-appearance', 'none') تقریباً همیشه یعنی Gecko؛ ویژگی‌های -webkit- هم در Blink و هم در WebKit وجود دارند، اما مجموعهٔ دقیقشان فرق دارد.
  • اینکه اصلاً navigator.userAgentData وجود دارد یا نه. مرورگری که ادعای Chrome 128 دارد اما این شیء را ندارد، یا Firefox‌ای است که UA کروم پوشیده یا افزونه‌ای آن را حذف کرده است.
  • قالب stack خطا. V8، SpiderMonkey و JavaScriptCore متن Error().stack به‌وضوح متفاوتی تولید می‌کنند و هیچ ویرایش UA آن را عوض نمی‌کند.

وقتی موتور معلوم شد، گام بعدی تخمین نسخهٔ واقعی است. روش در اصل همان جدول Chrome Platform Status است که برعکس اجرا می‌شود: هر انتشار Chromium چند API مستند اضافه می‌کند. یک انتشار Array.prototype.toSorted را معرفی کرد، دیگری یک API زمان‌بندی اضافه کرد، دیگری یک selector جدید CSS. پروب کردن این قابلیت‌ها یکی‌یکی نسخهٔ واقعی Chromium را در محدودهٔ باریکی محصور می‌کند. اگر UA ادعای Chrome 128 دارد اما APIهایی که در Chrome 115 عرضه شده‌اند وجود ندارند، UA ویرایش شده است.

صفحهٔ User-Agent ما کار ساده‌تری هم می‌کند: UA هدر درخواست HTTP را کنار navigator.userAgent از JavaScript می‌گذارد. در حالت عادی این دو یکسان‌اند. اگر فرق داشته باشند، در اصل دو توضیح وجود دارد: یک پروکسی در میانه هدرها را بازنویسی می‌کند یا افزونه‌ای سمت JS را دستکاری کرده است. این سنگین‌ترین کسر در امتیاز اصالت ماست، چون تقریباً هیچ مثبت کاذبی ندارد.

ناسازگاری چه هزینه‌ای برای شما دارد

ویرایش UA بدون ویرایش موتور پیامدهای بیشتری از آنچه مردم انتظار دارند به همراه دارد و بیشترشان به ضرر شماست.

موتورهای ریسک ناهمخوانی «UA ادعایی در برابر قابلیت‌های اندازه‌گیری‌شده» را سیگنال قوی یک محیط جعلی می‌دانند، که در بهترین حالت یعنی CAPTCHA بیشتر و در بدترین حالت یعنی حساب علامت‌خورده. منطق سازگاری شاخهٔ اشتباه را می‌گیرد: ادعای نسخهٔ جدیدتر کنید و سایت syntax مدرنی می‌فرستد که موتور قدیمی‌تر شما نمی‌تواند تجزیه کند و صفحهٔ خالی می‌بینید؛ ادعای نسخهٔ قدیمی‌تر کنید و تجربهٔ تنزل‌یافته می‌گیرید. جنبهٔ امنیتی هم دارد. یادآورهای به‌روزرسانی و قابلیت‌های امنیتی وابسته به نسخه از روی نسخهٔ UA کار می‌کنند، پس گزارش نادرست آن، این یادآورها را بی‌سروصدا خاموش می‌کند.

خلاصه: ویرایش UA آسان‌ترین لباس مبدل برای پوشیدن و آسان‌ترین برای لو رفتن است. یا کل محیط را سازگار کنید یا به حال خودش بگذارید.

پرسش‌های متداول

Client Hints برای حریم خصوصی بدتر از UA است؟ hintهای پرآنتروپی واقعاً بیشتر از UA کاهش‌یافته لو می‌دهند، مثل نسخهٔ واقعی Windows و مدل دستگاه شما. اما این‌ها قبلاً در UA رایگان داده می‌شدند؛ حالا دست‌کم باید درخواست شوند، و فقط مرورگرهای مبتنی بر Chromium آن‌ها را ارائه می‌کنند. تغییر از «برای همه قابل مشاهده» به «در دسترس با درخواست» است. این پیشرفت است، اما آن را با یک قابلیت حریم خصوصی اشتباه نگیرید.

Firefox و Safari هم UA خود را منجمد کرده‌اند؟ نه به اندازهٔ Chrome. هر دو طی سال‌ها برخی جزئیات را حذف کرده‌اند، اما هیچ‌کدام UA-CH را پیاده نکرده‌اند، پس رشتهٔ UA همچنان شناسهٔ اصلی آن‌هاست.

افزونهٔ تغییر UA می‌تواند سایت را قانع کند که مرورگر دیگری دارم؟ فقط سایت‌هایی را که چیزی جز UA نمی‌خوانند. پروب‌های قابلیت آن را می‌بینند، و وقتی گیر افتادید، «UA با موتور نمی‌خواند» سیگنال بدتری از UA اصلی است. اگر هدف ردیابی کمتر است، یک مرورگر پیش‌فرض که شبیه بقیه به نظر می‌رسد از هر لباس مبدلی بهتر است.