12k
All articles

User-Agent文字列は今でも信頼できるのか?

User-Agent文字列の信頼できる項目とブラウザーが固定する値を解説。ChromiumのClient Hintsや機能検出で不安定な解析を置き換える方法も紹介します。

OpenReplay Team
OpenReplay Team
User-Agent文字列は今でも信頼できるのか?

部分的には信頼できます。User-Agent(UA)文字列から今でも正確に読み取れるのは、ブラウザファミリー、メジャーバージョン、モバイルかデスクトップか、OSファミリーです。一方、OSバージョン、デバイスモデル、CPUアーキテクチャ、そして(Chromium系ブラウザでは)ブラウザのマイナーバージョンは当てになりません。

明らかにAndroid 10ではないスマートフォンの Android 10; K や、MシリーズのMacBookの Mac OS X 10_15_7 がログに残っていても、不具合ではありません。これらの値はプレースホルダーであり、ブラウザは意図的に送信しています。

本記事では、現行のChromeのUA文字列を分解し、各エンジンでどのフィールドが固定(フリーズ)されているかを示します。続いて、ChromiumでUA文字列に代わる仕組みと、UA文字列のパースが今でも妥当な選択となるケースを解説します。

重要なポイント

  • 主要ブラウザはすべて、今でもUser-Agent文字列を Mozilla/5.0 で始めています。これは互換性のためのトークンで、送信元のブラウザについては何も示しません。
  • Chromeは実際のOSリリースにかかわらず、Windows NT 10.0; Win64; x64、Macintosh; Intel Mac OS X 10_15_7、X11; Linux x86_64、Linux; Android 10; K のいずれかを報告します。マイナー、ビルド、パッチ番号は常に 0.0.0 です。
  • Safari 26以降、iOS、iPadOS、visionOS上のSafariは固定のOSバージョンを報告します。Mac版Safariは2017年からmacOSのバージョンを固定しています。
  • User-Agent Client HintsはChromium系ブラウザにしかありません。FirefoxとSafariは Sec-CH-UA-* ヘッダーを一切送信しません。
  • どのクライアントも任意のUser-Agentを送信できるため、このヘッダーで除外できるのは自ら名乗るボットだけです。

ChromeのUA文字列は実際に何を示しているのか?

現行のChromeデスクトップ版のUA文字列で、リリースごとに変わるのはメジャーバージョンだけです。それ以外はすべて、固定の互換性トークンか固定されたプラットフォーム値です。以下はWindows上のChrome 154の例です。

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
セグメント表している内容実際は?
Mozilla/5.0Mozilla互換ブラウザ意味なし。すべてのブラウザが送信する
(Windows NT 10.0; Win64; x64)Windows 10、64ビットx86固定値。Windows 11やARMマシンも同じ値を送信する
AppleWebKit/537.36 (KHTML, like Gecko)KHTMLから派生し、Geckoに類似したWebKitエンジン互換性のための名残。Chromeのエンジンは Blink
Chrome/154.0.0.0Chrome 154.0.0.0メジャーバージョンは実際の値。0.0.0 はプレースホルダー
Safari/537.36SafariSafariではない。「Safari」を判定するコードが引き続きマッチするよう残されている

MDNのFirefoxリファレンスは、Mozilla/5.0 をMozilla互換を名乗る汎用トークンと説明しています。エンジンを問わず、ほぼすべてのブラウザがこれを送信します。MDNのUser-Agentリファレンスでも、Blinkベースのブラウザは KHTML, like Gecko と Safari を互換性トークンとしてのみ含めていると説明されています。ChromeのUA文字列に含まれる AppleWebKit/537.36 (KHTML, like Gecko) と Safari/537.36 は、ChromeのエンジンもSafariも表していません。古いUA判定(スニッフィング)コードが引き続きマッチするよう残された固定トークンです。ご自身のUA文字列で同じ内訳を確認するには、OpenReplay user agent parserに貼り付けてください。

どのUAフィールドが固定され、どれが今も有効なのか?

3つのエンジンはいずれも、UA文字列の高エントロピーな部分、つまりOSバージョン、デバイスモデル、CPUアーキテクチャを固定しています。Chromiumはさらにブラウザのマイナーバージョンもゼロにしています。低エントロピーな部分は、今も実際のブラウザを反映しています。ChromeのUser-Agent Reductionプランにより、Chrome 101(2022年)からマイナー、ビルド、パッチ番号が 0.0.0 に固定され始めました。完全なUA文字列を維持できるdeprecation trialが2023年9月23日に終了してからは、すべてのページ読み込みで削減版の文字列が送信されています。MDNのUA reductionガイドには、Androidの Android 10; K を含む固定プラットフォーム値の一覧があります。

Chrome / EdgeFirefoxSafari
ブラウザファミリー、メジャーバージョン実際の値実際の値実際の値(Version/)
モバイル/デスクトップ、OSファミリー実際の値実際の値実際の値
OSバージョン固定上限あり(macOS 10.15、Android 10)固定(macOS、iOSは26以降)
デバイスモデルAndroidでは K送信しない送信しない
CPUアーキテクチャ固定固定Macは常に「Intel」
マイナーバージョン0.0.0公開しない実際の値(Version/)
UA Client Hintsありなしなし

EdgeはChromeと同じ固定トークンを使い、Edg/ を追加します。FirefoxはFirefox 87以降、Big Sur以降のすべてのmacOSリリースを10.15として報告し、Apple Silicon MacをIntelと表示します。Firefox for Android 122以降は、実際のバージョンにかかわらずAndroid 10を報告します。WebKitのSafari 26.0リリース記事によると、Mac版Safariは2017年から同じ Intel Mac OS X 10_15_7 を送信しています。Safari 26(2025年9月)以降は、iOS、iPadOS、visionOS上のSafariも、実際のバージョンではなくiOS 18の固定バージョンを報告します。Safari 26.0は 18_6 を送信していました。Safari 26.2からは、WebKitがこの値をiOS 18の最終リリースに固定したため、現在は 18_7 が送信されます。Version/ トークンはリリースごとに更新され続けます。

ChromiumのUA削減の対象は、Windows、macOS、Linux、ChromeOS、Android上のChromeです。Android WebViewとChrome for iOSは対象外です。実務上は次のようになります。

  • Windows 10とWindows 11は区別できません。
  • Apple Silicon Macは、3つのエンジンすべてで「Intel」と報告されます。
  • Android 10; K を含むChrome for Androidの文字列は、Android 10のデバイスを意味しません。Android上のすべてのChromeが、このバージョンとプレースホルダーのモデル名 K を送信します。

なぜブラウザ検出より機能検出が優れているのか?

ブラウザ名からは、そのブラウザで何ができるかを確実には判断できません。機能検出は機能そのものを直接確認する方法で、その利点はUA文字列が固定される前から変わりません。UAによる判定は、文字列が実態と異なる場合、新しいブラウザがそのAPIを実装した場合、古いバージョンにそのAPIがない場合に破綻します。Baselineを使えば、ある機能をいつから安心して使えるかをブラウザ横断で把握できます。まだ使えない機能については、ランタイムチェックで補います。

// Brittle: guesses capability from a name
if (/Chrome\/\d+/.test(navigator.userAgent)) enableShareButton();

// Direct: asks the browser
if ('share' in navigator) enableShareButton();

User-Agent Client Hintsはどのように機能するのか?

User-Agent Client Hintsは、UA文字列から失われた詳細情報の代わりにChromium系ブラウザが送信する、一連の Sec-CH-UA-* リクエストヘッダーです。ChromeとEdgeは Sec-CH-UA、Sec-CH-UA-Mobile、Sec-CH-UA-Platform をデフォルトで送信します。FirefoxとSafariはどれも送信しません。MDNではSec-CH-UA-Platform は低エントロピーヒントに分類されています。そのため、パーミッションポリシーでブロックされていない限り、ブラウザはサーバーから要求されなくてもこのヘッダーを送信します。それ以外のヒントは、すべてサーバーから要求する必要があります。

ヒントを要求するには、サーバーがAccept-CHにヒントを列挙します。以降、ブラウザはそのオリジンへのセキュアなリクエストにヒントを含めます。

ただし、Accept-CH は最初のリクエストには効きません。最初のリクエストから高エントロピーヒントを受け取るには、サーバーは Accept-CH に加えてCritical-CHにもそのヒントを指定します。するとブラウザは最初のレスポンスをレンダリングせず、ヒントを付けてリクエストを再送信します。また、キャッシュがバージョンごとに別々に保存されるよう、サーバーはそのヒントを Vary にも追加してください。

import https from 'node:https';
import { readFileSync } from 'node:fs';

https.createServer(
  { key: readFileSync('key.pem'), cert: readFileSync('cert.pem') },
  (req, res) => {
    const platform = req.headers['sec-ch-ua-platform'];           // default hint
    const version = req.headers['sec-ch-ua-platform-version'];    // opt-in only

    res.setHeader('Accept-CH', 'Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch');
    res.setHeader('Critical-CH', 'Sec-CH-UA-Platform-Version');
    res.setHeader('Vary', 'Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch');
    res.setHeader('Content-Type', 'text/plain');
    res.end(`platform=${platform ?? 'n/a'} version=${version ?? 'n/a'}\n`);
  }
).listen(8443);

Nodeはヘッダー名を小文字に変換します。値は "Windows" のように、引用符付きの構造化フィールド(Structured Field)文字列として届きます。ブラウザ側では、navigator.userAgentData.getHighEntropyValues()を使えば、ヘッダーを設定しなくても同じデータを取得できます。MDNでは uaFullVersion は非推奨とされ、代わりに fullVersionList が推奨されています。

async function describeClient() {
  const uaData = navigator.userAgentData;
  if (!uaData) return { source: 'ua', ua: navigator.userAgent }; // Firefox, Safari
  const high = await uaData.getHighEntropyValues([
    'platformVersion', 'architecture', 'model', 'fullVersionList',
  ]);
  return { source: 'ua-ch', brands: uaData.brands, mobile: uaData.mobile, ...high };
}

UA文字列のパースが今でも妥当なのはどんな場合か?

有効なフィールドだけが必要で、判定を誤ってもコストが小さい場合は、パースは今でも妥当です。

アナリティクスの分類。 「Chrome 154、デスクトップ、Windows」のような分類であれば、UAのパースで正確に集計できます。しかし「Windows 10」や「Android 10」という分類は正確ではありません。新しいリリースが気づかないうちに混ざり込むため、OSファミリーとしてのみ扱ってください。判定は Chrome/ より先に Edg/ を、Safari/ より先に Chrome/ をチェックします。各ブラウザの文字列には、この順序で後に来るトークンも含まれているためです。

ボットのフィルタリング。 RFC 9110はUser-Agentをクライアントが提供するヘッダーと定義しており、その内容を検証する仕組みはありません。どのクライアントも任意の値を送信できます。このヘッダーで捕捉できるのは自ら名乗るクローラーだけで、名乗らないボットには効果がありません。

サポートチケット。 ユーザーからバグ報告を受けたときに必要なのは、通常は正確なビルドではなく、おおよその利用環境です。セッションリプレイツールは各セッションとともにUAを記録するので、バグがmacOS上のFirefoxでは再現し、Chromeでは再現しないといったことは十分に確認できます。機能チェックの結果も併せて記録しておけば、さらに原因を絞り込めます。

まとめ

UA文字列で信頼できるのは、ブラウザファミリー、メジャーバージョン、モバイルかデスクトップか、OSファミリーです。それ以外の値はすべてプレースホルダーとして扱ってください。対応としては、まずUA文字列からOSバージョン、デバイスモデル、マイナーバージョンを読み取っている分岐がないか、コードを監査しましょう。UAベースの機能判定は機能検出に置き換え、プラットフォームの詳細が本当に必要な処理はClient Hintsに移行します。その際、Client Hintsを送信しないFirefoxとSafari向けのフォールバックも用意してください。

よくある質問

UAがWindows NT 10.0の場合、Windows 11とWindows 10はどう見分ければよいですか?

platformVersionのクライアントヒントを要求します。navigator.userAgentData.getHighEntropyValues(['platformVersion']) を使うか、Accept-CH: Sec-CH-UA-Platform-Version を送信してください。Microsoftのドキュメントでは、1.0.0から10.0.0までの値がWindows 10、13.0.0以上がWindows 11とされており、同社のサンプルコードもメジャーバージョンが13以上であればWindows 11として扱っています。FirefoxはClient Hintsを送信しないため、両者を区別できません。

Accept-CHで要求したヒントを、ブラウザはどのくらいの期間送信し続けますか?

Chromeは各オリジンのAccept-CH設定をディスクに保存します。Chrome 103以降、この設定に固定の有効期限はありません。ユーザーがそのオリジンのCookieまたはサイトデータを消去するまで保持され、セッションCookieを消去した場合にも併せて消去されます。サーバー側では、Accept-CHを再送信してリストを置き換える、空のAccept-CHを送信してすべてのヒントを停止する、Clear-Site-Data: 'clientHints' を送信する、のいずれかの方法で変更できます。

Sec-CH-UAヘッダーにNot A;Brandのようなブランドが含まれているのはなぜですか?

これはGREASEと呼ばれる、意図的に追加された偽のエントリです。Chromiumはバージョン番号の低い架空のブランドを追加し、その区切り文字やリスト内の位置を毎回変化させます。これにより、サーバーは固定の文字列や固定のブランドリストとの照合に頼れず、ヘッダーを正しくパースせざるを得なくなります。Sec-CH-UAは構造化フィールドのリストとしてパースし、Chromium、Google Chrome、Microsoft Edgeなど必要なブランドを探して、認識できないエントリは無視してください。

UAパーサーがiPhone上のChromeをSafariと判定するのはなぜですか?

Chrome for iOSは、Version/ の代わりに CriOS/ トークンを含むMobile SafariのUA文字列を送信するため、文字列に Chrome/ トークンが含まれません。Firefox for iOSも同様に FxiOS/ を使います。Chrome/ と Firefox/ だけを探すパーサーではSafariと判定されてしまうため、先に CriOS/ と FxiOS/ をチェックしてください。なお、ChromiumのUA削減はChrome for iOSには適用されません。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.