Upgrade to Pro — share decks privately, control downloads, hide ads and more …

沈黙すべきか、叫ぶべきか - 独自ブラウザの GPC 実装 - / gpc-war-story

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

沈黙すべきか、叫ぶべきか - 独自ブラウザの GPC 実装 - / gpc-war-story

2026/05/16 大LT2026 春 in Aizu

Avatar for marcy731

marcy731

May 15, 2026

More Decks by marcy731

Other Decks in Programming

Transcript

  1. 長谷川 将司 (marcy731) STORES 株式会社 / テクノロジー部門 モバイル開発本部 モバイルPOSグループ マネージャー

    個人開発: iOS プライバシー特化ブラウザ Umbric LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 — marcy731 / STORES 1
  2. 過去登壇 LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 自己紹介 長谷川

    将司 (@marcy731) ☐ STORES 株式会社 ☐ テクノロジー部門 / モバイル開発本部 / モバイルPOSグループ ・ マネージャー (iOS エンジニア) ・ 個人開発: Umbric — iOS プライバシー特化ブラウザ ☐ SwiftUI + WKWebView + iOS 26 (Liquid Glass) ・ 「痕跡を残さない」 「見せない」がコンセプト ・ 今日の話の舞台 ・ marcy731 / STORES 2
  3. LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 今日の話 Global Privacy

    Control (GPC) という機械可読シグナルを WKWebView で出す話 ☐ 実装は JavaScript 10 行 ☐ でも設計判断は分岐点だらけ ☐ WKWebView で GPC を出すのは、表面的に見えるほど簡単じゃなかった ☐ marcy731 / STORES 3
  4. Umbric は 2 つの軸でプライバシーを守るブラウザ: しかし、これは全部端末側で完結するプライバシー LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの

    GPC 実装 Umbric の世界観 — 「残さない」と「見せない」 ☐ 痕跡を残さない ☐ Cookie / 履歴 / Web Storage は端末に永続化しない ・ WKWebsiteDataStore.nonPersistent() を全タブで使用 ・ SwiftData の履歴はパニックボタンで全削除可能 ・ ☐ 見せない ☐ バックグラウンド復帰時にカモフラージュ画面で隠す ・ Face ID / パスコード でアプリロック ・ 加速度センサで「覗き見されそうな瞬間」を検知 ・ marcy731 / STORES 5
  5. たとえば: でも、その間サイト側は → 「端末側で消す」と「サーバ側で売られない」は、別レイヤ LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC

    実装 端末で消しても、サーバ側のデータは残る 履歴を消した ☐ Cookie を消した ☐ 広告 ID と紐づけてユーザを識別 ☐ サードパーティトラッカーに**「あなたのデータ」を売却** ☐ 別端末 / 別ブラウザで同じあなたを再認識 ☐ marcy731 / STORES 6
  6. 手段 概要 コスト Cookie 同意バナーで毎回 "Reject All" 表示されたバナー全部に手動で拒否 高 (1

    日 50 サイト = 50 回) GPC (Global Privacy Control) ブラウザがブラウザ単位で機械可読に「売らないで」を全サ イトに送り続ける 低 (一度設定すれば 終わり) Umbric は当然 GPC を採用したい → でも実装してみたら、思ったより設計判断が多かったというのが今日の話 LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 サーバ側で「売らせない」手段は 2 つしかない marcy731 / STORES 7
  7. globalprivacycontrol.org で定義された 「私はトラッキング・販売・共有されたくありません」 を表明する機械可読シグナル W3C で 2 つの伝達手段が定義されている 1. HTTP

    ヘッダ: Sec-GPC: 1 2. JS API: navigator.globalPrivacyControl === true → サイト側はこのシグナルを見て販売・共有を止める義務を負う LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 GPC (Global Privacy Control) とは HTTP ヘッダ: ブラウザが全リクエストに自動で付与 ☐ JS API: サイト側 JS から navigator.globalPrivacyControl で参照できる ☐ marcy731 / STORES 9
  8. LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 CCPA とは California

    Consumer Privacy Act ☐ カリフォルニア州が 2018 年に制定 / 2020 年に施行した消費者プライバシー保護法 ☐ 通称「カリフォルニア州版 GDPR」 ☐ ユーザに以下の権利を与える: ☐ ☐ 知る権利 (自分のどんなデータが収集されているか) ・ ☐ 削除する権利 (収集されたデータを消させる) ・ ☐ オプトアウトする権利 ← GPC が関わるのはここ ・ 2020 年に CPRA (California Privacy Rights Act) で強化、2023 年施行 ☐ marcy731 / STORES 10
  9. → GPC を無視 = CCPA 違反 = 罰金対象 LT —

    沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 なぜ GPC は CCPA で「効く」のか CCPA 規則 §999.315(c) で、事業者はユーザのオプトアウト意思表示の手段を提供する義務がある ☐ 2021 年、カリフォルニア州 AG (Attorney General = 州司法長官) が: GPC は CCPA 下で有効な opt-out 意思表示として扱う と公式声明を出した ☐ marcy731 / STORES 11
  10. 項目 内容 年 2022 年 対象 化粧品大手 Sephora 違反 GPC

    シグナルを無視してサードパーティに顧客データ販売 罰金 $1.2M + 是正命令 出典 California AG プレスリリース → 大手企業でも実際に罰金を食らった → GPC は飾りじゃない DNT (Do Not Track, 2009-2019) は法的拘束力ゼロで誰も尊重しなかった GPC はその二の舞ではない LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 執行実例 — Sephora 事件 marcy731 / STORES 12
  11. ブラウザ 既定 備考 Brave ON 全 navigation で Sec-GPC: 1

    + JS API Firefox ON (Private モード) 一般モードは opt-in DuckDuckGo ON 全 navigation Safari (WebKit) 意図的に未実装 WebKit ポジション paper で明示 → Safari だけが**「やらない」と明示**している なぜ?という伏線を、メタ視点のところで回収します LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 各ブラウザの実装状況 marcy731 / STORES 13
  12. HTTP ヘッダを 1 行足せばいいだけ GET / HTTP/1.1 Host: example.com Sec-GPC:

    1 ← これを追加 URLRequest を直接扱うネイティブコードなら、 setValue(_:forHTTPHeaderField:) でたった 1 行 → WKWebView 以外では、本当にそれで終わる LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 最短経路 — のはずだった marcy731 / STORES 15
  13. WKWebView は WebKit プロセスが独自にネットワーク処理を担当 アプリ側から HTTP ヘッダに直接介入する API が存在しない 一応の迂回路

    WKNavigationDelegate.webView(_:decidePolicyFor:) → navigationAction を cancel → URLRequest をコピーして Sec-GPC ヘッダ追加 → webView.load(modifiedRequest) で再読込 この迂回路に伴うコスト → Sec-GPC 1 個のためには割に合わない LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ところが WKWebView では ☐ navigation を 2 回発火: cookie / POST / 課金 webhook 二重打ちリスク ☐ ☐ subresource には届かない: fetch / iframe / image に乗らない ☐ marcy731 / STORES 16
  14. W3C 仕様は HTTP ヘッダと JS API の両方を定義している: navigator.globalPrivacyControl === true

    幸い、GPC を尊重するサイトの多くはこの JS API も見る (同意管理プラットフォーム = CMP は基本的に JS で動くため) 戦略 WKUserScript で navigator.globalPrivacyControl を true に固定する。 ヘッダは諦めて、JS 層だけで叫ぶ → Umbric Issue #691 で議論し、この中間案を採用 LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 じゃあ JS で出す (中間案) marcy731 / STORES 17
  15. static let globalPrivacyControlSource: String = """ (function () { try

    { Object.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: function () { return true; }, configurable: false, enumerable: true }); } catch (e) {} })(); """ 10 行未満。シンプル。 ここから、この 10 行に詰まっている設計判断 5 つを解いていきます LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 コード本体 (UserScripts.swift ) marcy731 / STORES 19
  16. // instance に定義 Object.defineProperty(navigator, 'globalPrivacyControl', ...) // prototype に定義 Object.defineProperty(Navigator.prototype,

    'globalPrivacyControl', ...) なぜ prototype か GPC を最も見てほしいのは 3rd party の広告 SDK = iframe で動くやつら → prototype 経由は契約(テストでも Navigator.prototype を含むことを assert) LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ポイント ① prototype vs instance ☐ iframe は独自の navigator インスタンスを持つ ☐ ☐ main window の navigator に直接定義しても iframe からは見えない ☐ ☐ Navigator.prototype に置くと、全 frame の navigator が継承で見る ☐ marcy731 / STORES 20
  17. Object.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: function () { return true; },

    configurable: false, // ← これ enumerable: true }); なぜ false か configurable: true だと、ページ側 JS がこう書ける: // 悪意ある page JS Object.defineProperty(navigator, 'globalPrivacyControl', { get: () => false // ← 上書きで偽装 }); → ユーザの「売らないで」が改ざんされる configurable: false でロック → page JS が再 defineProperty しようとすると TypeError で throw ユーザの意思は書き換え不可な契約として固定される LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ポイント ② configurable: false marcy731 / STORES 21
  18. WKUserScript を仕込む側のオプション: let script = WKUserScript( source: UserScripts.globalPrivacyControlSource, injectionTime: .atDocumentStart,

    forMainFrameOnly: false // ← デフォルト true なので明示 ) なぜ false か **「主犯が居るのは iframe」**だから、frame 全部に撒く必要がある LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ポイント ③ forMainFrameOnly: false ☐ 広告 SDK / トラッカーは iframe に居る ☐ ☐ forMainFrameOnly: true (既定)だと main frame にしか注入されない ☐ ☐ → 肝心の追跡側に届かない ☐ marcy731 / STORES 22
  19. injectionTime: .atDocumentStart // ← page JS より先 なぜ start か

    atDocumentEnd だと: 1. document 読み込み開始 2. page JS が走り、navigator を sniff して結果を保存 3. ← ここで GPC 注入(手遅れ) ページ JS が navigator.globalPrivacyControl を読むタイミングはどこでもありうる → DOM が触れる最初のタイミングで確定する必要がある WKContentWorld のドキュメントにも書いてあるが、順序が全て LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ポイント ④ atDocumentStart marcy731 / STORES 23
  20. (function () { try { Object.defineProperty(Navigator.prototype, 'globalPrivacyControl', { ... });

    } catch (e) {} // ← 黙って飲み込む })(); なぜ try-catch か Umbric は atDocumentStart に複数のスクリプトを同居させている (GPC / 広告 DOM ブロック / Canvas spoof / Cookie バナー自動却下 …) GPC が throw → 同じ world の後続スクリプトが全滅 (silent failure) → try-catch で GPC だけ失敗させて、他は生かす LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ポイント ⑤ try / catch でフェイルクローズド marcy731 / STORES 24
  21. @Suite("GlobalPrivacyControlSignal") struct GlobalPrivacyControlSignalTests { @Test("Navigator.prototype に defineProperty している") func definesPropertyOnNavigatorPrototype()

    { #expect(Self.source.contains("Navigator.prototype")) #expect(Self.source.contains("Object.defineProperty")) } @Test("configurable: false でページ JS から再定義できない") func notConfigurable() { #expect(Self.source.contains("configurable: false")) } @Test("try/catch で fail-closed") func wrappedInTryCatch() { #expect(Self.source.contains("try")) #expect(Self.source.contains("catch")) } } JS の実行検証はシミュレータでしかできない LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 テストでピン留め marcy731 / STORES 25
  22. サイトが Cookie を使わずにユーザを識別する技術 ブラウザから取れる情報を JS で全部集める: → これらの組合せが珍しい = 個別

    ID として機能 Chrome 142 / macOS 26 / 2560x1440 / WebGL "Apple M3 Max" ≒ ほぼあなた 1 人 ※ Canvas: 絵を描くための HTML5 API。同じ命令で描いても GPU やフォント差でピクセル値がずれる ※ WebGL: GPU を使う 3D 描画 API。GPU ベンダー / モデル名や描画結果に個体差が出る ※ AudioContext: 音を生成・解析する Web Audio API。出力波形にハードウェア個体差が出る Cookie バナーで Reject しても、こちらはサイレントに走り続ける LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 まず: フィンガープリンティングとは何か User-Agent / OS / 言語 / タイムゾーン ☐ 画面サイズ / インストール済みフォント ☐ GPU renderer (WebGL) / hardwareConcurrency ☐ Canvas / AudioContext で描いた結果のピクセル差 ☐ marcy731 / STORES 27
  23. 各情報は「何 bit のエントロピーを持つか」で評価される 世界人口 ≒ 80 億人 = 約 33

    bit で個人を一意特定できる User-Agent: 約 10 bit 言語設定: 約 6 bit 画面サイズ: 約 5 bit インストール フォント: 約 14 bit ← これだけでほぼ特定可能 Canvas hash: 約 10 bit タイムゾーン: 約 4 bit 合計 = 約 49 bit >> 33 bit → 「fingerprint bit を 1 つでも減らす」のがプライバシーブラウザの戦い LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 エントロピー (fingerprint bit) の話 marcy731 / STORES 28
  24. 各ブラウザの GPC 送出パターン: ブラウザ群 Sec-GPC: 1 を送る? Brave / Firefox-Private

    / DuckDuckGo 送る Safari / Chrome / Edge 送らない 世界の Safari + Chrome シェア = 約 80% GPC を送るブラウザのシェア = 残り 約 20% ユーザ A が Sec-GPC を送る → A は Brave/Firefox-Private/DuckDuckGo のいずれか確定 → ブラウザ識別の bit が +1〜+3 bit 増える → 「GPC を送ること自体」が新しい fingerprint bit になる LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 なぜ「GPC を送る」と識別されるのか marcy731 / STORES 29
  25. WebKit プロジェクト公式ポジション (2020): User-agent extension signals for opting out [...]

    increase the fingerprinting surface 要約: 「全員が同じシグナル出さない限り、出してる人を識別できてしまう」 attack surface 最小化の観点では筋が通っている判断 LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 Safari の判断 — 「沈黙」 ☐ ユーザの権利を行使する手段としては GPC は正しい ☐ ☐ でも少数派が GPC を送る = その人達が識別される ☐ ☐ Apple は「フィンガープリント表面の最小化」を最優先 ☐ ☐ → 全員沈黙が最適解 ☐ marcy731 / STORES 30
  26. → 「全員が出す」均一化で、fingerprint bit を 1 つに抑える戦略 LT — 沈黙すべきか、叫ぶべきか —

    独自ブラウザの GPC 実装 Brave / Firefox はどう正当化してるか Brave: 「全 Brave ユーザが必ず GPC を出す」 → 漏れるのは「Brave 使ってる」事実だけ → Brave 自体が他で識別されてるので追加情報量ゼロ ☐ Firefox Private: 「全 Private モードユーザが出す」 → 漏れるのは「Firefox Private モード」だけ ☐ marcy731 / STORES 31
  27. 方式 fingerprint bit CCPA 有効性 何もしない (Safari) ±0 HTTP ヘッダで送る

    (Brave) +1〜+3 (Sec-GPC ヘッダで識別) JS だけで宣言 (Umbric) ±0 (既存の保護機能と同じ bit) △ (JS を見るサイトのみ) なぜ ±0 か → 既存の fingerprint surface 内に「収まる」場所だけで叫ぶ LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 Umbric の選択 — 「JS だけ叫ぶ」中間案 Umbric は既に Canvas / WebGL / AudioContext / navigator.languages を spoof している ☐ JS API を hook してる時点で「Umbric だ」と既に識別される ☐ そこに globalPrivacyControl を追加しても新たな識別軸にならない ☐ 一方、HTTP ヘッダで送ると別の bit が新規に増える ☐ marcy731 / STORES 32
  28. 戦略 fingerprint 増加 CCPA 行使 採用例 沈黙 0 × Safari

    / Chrome / Edge 全方位主張 高 (+1〜+3 bit) ◎ Brave / Firefox 既存 surface 内のみ 低 (±0) △ Umbric **「正しいことをやろうとすると逆に識別される」**というジレンマを、 設計判断で割り切る話 LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 3 つの戦略を 1 枚で marcy731 / STORES 33
  29. GPC を「半分送って半分送らない」は 逆効果 → fingerprint 対策の鉄則は「ランダム化」ではなく 「均一化」 (Tor / Brave:

    全ユーザが同じ generic な値を返す) LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 「ランダムに送ればよくない?」と思った人へ ☐ CCPA 拘束力が中途半端 — 送らなかった時はサイトが「同意」とみなして販売継続 ☐ ☐ ランダム挙動自体が新しい fingerprint bit — そんな挙動するブラウザはほぼ存在しない ☐ ☐ 観察回数が多いと確率分布で露見 — 「50% で送る」が統計的に検出される ☐ marcy731 / STORES 34
  30. 実は Umbric は Canvas / WebGL / AudioContext でランダムノイズで撹乱している: //

    fingerprint_protection.js data[i] = data[i] + (Math.random() < 0.5 ? -1 : 1) // ±1 ノイズ 属性タイプ 推奨戦略 例 連続値・観察独立 ランダムノイズ Canvas / WebGL / AudioContext 離散 boolean・法的意味あり 送る/送らないを設計判断 GPC 識別性の高い属性 均一化 (全員同じ値) UA / 画面サイズ → GPC は boolean × 観察可能 × 法的意味固定 → ランダム化は両方の悪いとこ取り LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ランダム化が効く属性、効かない属性 marcy731 / STORES 35
  31. LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 まとめ ☐ GPC

    は CCPA で legal binding がある「売らないで」シグナル ☐ ☐ WKWebView では HTTP ヘッダで送るのが現実的に詰む ☐ ☐ JS API だけで叫ぶ中間案を Umbric は採用 (Issue #691) ☐ ☐ 10 行の JS に 5 つの設計判断: ☐ prototype / configurable: false / forMainFrameOnly: false / atDocumentStart / try-catch ・ ☐ **「正しい主張は、新しい識別軸になりうる」**というジレンマがある ☐ ☐ Safari の沈黙 / Brave の全実装 / Umbric の中間案、全部設計判断 ☐ marcy731 / STORES 37
  32. LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 3 つの学び ☐

    WKWebView でできないことは JS で逃げる ☐ HTTP ヘッダ注入は詰む。でも W3C 仕様には JS API も書いてある ・ 「仕様の中で逃げ道を探す」が WebKit 制約とのつきあい方 ・ ☐ シンプルなコードほど、判断密度が高い ☐ 10 行の JS の各要素が「なぜそうするか」を持っている ・ ☐ 「正しい」と「安全」は同じじゃない ☐ CCPA で legal binding がある GPC ですら、送ること自体が識別軸 ・ プライバシー設計は「何を守るか」と「何を主張するか」を両方問われる ・ marcy731 / STORES 38
  33. Q A Brave iOS 版は GPC 出してる? 出してます (WebKit 制約は同じ。Sec-GPC

    ではなく JS のみ) Sec-GPC ヘッダ実装は本当に無理? URLProtocol を立てて全リクエストを横取り、または NSURLSession の delegate で追加。WKWebView では実用にな らない Apple は将来 GPC API を出す? 出さない可能性が高い (WebKit ポジションが揺らいでいない) iframe で navigator.globalPrivacyControl が継 承されない場合は? sandbox 属性で navigator が再生成される場合あり。実検証が 必要 同じ手法で他の Web API も hook できる? 可能。Umbric は Canvas / WebGL / AudioContext / languages で同じ手法を使用 LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 想定 Q&A (先回り) marcy731 / STORES 39
  34. GPC を真面目に出したい方、 WKWebView の HTTP ヘッダ追加で困った方、 このスライドが供養になれば 長谷川 将司 (marcy731)

    STORES / モバイルPOSグループ マネージャー GitHub: github.com/marcy731 / X: @marcy731 LT — 沈黙すべきか、叫ぶべきか — 独自ブラウザの GPC 実装 ありがとうございました marcy731 / STORES 40