2 ポイント 投稿者 GN⁺ 2025-03-31 | 1件のコメント | WhatsAppで共有
  • David Bushellのサイトが一部ユーザーには長い間壊れて見えていた原因は、Grammarlyブラウザー拡張機能がページにこっそり注入していたCSSだった
  • Firefoxでは、Grammarly拡張機能はローカルの拡張機能アセットのスタイルシートを挿入し、WebページのStyleSheetListでは見つけにくく、Content Security Policyも回避する
  • 衝突は、Grammarlyが:rootにグローバル定義した--rem:16と、サイトの流動タイポグラフィ計算用の--remが同じ名前を使っていたことから発生した
  • サイト側の--remcascade layer内にあり、レイヤー外のスタイルが優先されるCSSルールのため、Grammarlyの値が計算を上書きできてしまった
  • 一時的にはmutation observerと!importantでしのいだが、最終的な対応はプロパティ名を--🤡に変えることだった。拡張機能がグローバルな:rootにありふれた名前を注入すると、Webページと簡単に衝突し得る

ページ内に入り込んだGrammarly CSS

  • 数か月にわたり、サイトのレイアウトがずれ、サイズがおかしいという散発的な報告があり、スクリーンショットも送られてきていた
  • 技術に詳しい読者たちはGrammarly browser extensionを主な原因として指摘し、David BushellはFirefoxベースのMullvad browserに直接インストールして確認した
  • 拡張機能のインストール時の権限には以下が含まれる
    • すべてのWebサイトデータへのアクセス
    • 通知の表示
    • ブラウザータブへのアクセス
  • GrammarlyはWebページに、ローカルの拡張機能アセットから読み込まれるスタイルシートを注入する
    • このスタイルシートは、WebページがStyleSheetListで見つけることができない
    • Content Security Policyも回避する
    • Firefoxでは、Webサイト自身が検知しにくいステルススタイルシートのように動作する
  • 拡張機能は、ユーザーが操作しなくても、すべてのWebサイトの<html>文書に<grammarly-desktop-integration>カスタム要素を追加する

--remという名前ひとつがレイアウトを壊した過程

  • Grammarlyのスタイルシートの末尾には、次のCSSが含まれている
:host,
:root {
  --rem:16
}
  • 同じスタイルシートの別の箇所では、--remを使ってフォントサイズと行の高さを計算している
.kE2Bj {
  font-size:calc(0.86px*(var(--rem) - 2));
  line-height:calc(1.2868px*(var(--rem) - 2));
}
  • サイトもまた、独自の流動タイポグラフィ実験のために--remカスタムプロパティを使っていた
@layer base {
  :root {
    --rem: 0.0625rem;
    --fluid: calc((100vi - (400 * var(--rem))) / (1920 - 400));
    --font-size-h1: clamp(
      calc(31 * var(--rem)),
      calc((31 * var(--rem)) + (80 - 31) * var(--fluid)),
      calc(80 * var(--rem))
    );
  }
}
  • サイトの--remcascade layer内に定義されており、レイヤー外のスタイルはCSS specificityに関係なく、レイヤー内のスタイルより優先される
    • ソース順も影響するため、Grammarlyの--remが勝った可能性がある
    • その結果、サイトの計算式が壊れ、レイアウト問題が発生した
  • 当初はmutation observerで追加されたWebコンポーネントを検知した後、!importantスタイルを加えて対処していた
  • 正確な原因を把握した後、サイトのカスタムプロパティ名を--🤡に変更した
    • この名前はCSSで有効なカスタムプロパティ名である
    • --remはGrammarlyがグローバルに使うため、衝突リスクのある名前になった
  • Grammarlyはランダムなクラス名を作っている一方で、--remという一般的なカスタムプロパティ名を:rootにグローバル適用しており、拡張機能を実際に使わなくても、すべてのWebページにコードを注入する
  • Grammarlyサポートチームには連絡したが、まだ問題を理解する技術担当者にはたどり着けていない

1件のコメント

 
GN⁺ 2025-03-31
Hacker News の意見
  • 拡張機能の問題で経験した事例は少し違います。地理位置情報テスト用にプロキシサーバーの切り替えを簡単にする拡張機能を配布しています。
    数か月前に最悪の顧客デモをしたのですが、製品がまったく動いていないように見えました。しばらくデバッグした末、最近の 1Password 拡張機能のアップデートが私たちの拡張機能を壊していたことを発見しました。1Password が認証イベントを購読したものの返さなかったためタイムアウトし、その結果、私たちの購読者が呼び出されませんでした。私たちの拡張機能はブラウザにプロキシサーバーの変更を指示したあと、認証情報を提供する準備をしていましたが、リクエストが来なかったのです。1Password のサポートチームは Grammarly よりは良かったものの、サポート経由で正体の分からない PM に優先順位を説得するのは難しいです。
    その後、ロシア政府のウェブサイトに必要なある拡張機能にも同じ問題があることが分かりました。

    • 似た状況です。1Password は今でも、他の拡張機能のコンテンツスクリプトから Chrome のサイドパネル UI を開く機能を壊します。イベントがユーザー操作から来たことを示す信頼フラグを壊してしまいます。
      10年以上拡張機能の分野に関わってきた立場からすると、結局 Google の責任が大きいです。広告ブロッカー変更という政治的問題とは別に、Manifest v3 は多くの面で期待を大きく下回っています。
      全体として、Chromium のコードベースの品質は以前よりかなり低下したように感じます。
  • 未知のページにスクリプトやスタイルを注入するなら、少なくとも変数の名前空間は分離すべきです。

    • これが本当に腹立たしいのは、5〜6か月前くらいの面接で、2014年に CTO 兼リード開発者として働いていた Instagram/ブランディング系スタートアップの話をしたことです。そのとき、CSS クラスと JavaScript オブジェクトがきちんと名前空間分離されるようにビルドシステムを作り、衝突の可能性がないようにし、サードパーティの顧客サイトにどのウィジェットがあるかに応じて正確にどのスクリプトを読み込むべきかを管理していた、と説明しました。
      ところが面接官は、そういうことは今どきのツールが全部やってくれるし、誰でもやっている、という感じで軽くあしらいました。その言葉にはある程度同意せざるを得ませんでした。今はその仕事をしていないので、実際のところは分からないからです。ところが、ふたを開けてみると、みんながそうしているわけでもありませんでした。
    • 名前空間分離は他人のためだけでなく、自分自身にとっても便利です。以前の職場でユーザーには見えないブラウザ自動化を作っていましたが、拡張機能ですらなかったものの、それでも名前空間分離は役に立ちました。
      自分たちが挿入したものと元からあったものを明確に区別でき、潜在的な衝突も避けられました。
    • フロントエンド分野を離れてしばらく経つのですが、最近の CSS の名前空間分離は普通どのように処理しているのでしょうか?
    • もっと良いのは Shadow DOM を使うことです。
  • 画面共有や録画で、あの緑色の侵入者がすべてのウェブサイトにデフォルトで入り込んでいるのを見ると怖くなります。単に見た目が邪魔というだけでなく、プライバシー問題と明らかな攻撃ベクトルがついて回ります。
    Chrome では必要なときだけ拡張機能を有効にできるのに、なぜ誰もそうしないのか分かりません。なぜすべてのブラウザのデフォルトがそうなっていないのかも疑問です。

    • こういうことを気にする同僚がいて、かなり運がいいと感じます。会議参加者の一部が特定の拡張機能や各種 AI アシスタントをインストールしているのが明らかに見えると、会議を止めたこともあります。
      一部の同僚は情報が第三者に渡る可能性を不快に思っているため、拡張機能をオフにするまで会議を中断します。
  • Grammarly Extension のエンジニアです。まず、私たちの拡張機能が dbushell.com のユーザー体験を壊し、作者に原因究明のための時間と労力を使わせてしまったことを本当に申し訳なく思っています。
    意図したことではなく、このようなことが起きないよう複数の手法を使っています。しかし十分ではなく、記事から改善の余地が明確に示されています。
    迅速な修正として dbushell.com に一時的な例外を追加しました。同時に、適切なスタイル分離を保証する変更に取り組んでおり、このような問題は絶対に起きるべきではありません。

  • Google Translate が私の Web アプリを壊すという似た問題があります。ユーザーは Google Translate を使いながら私のアプリが壊れたと文句を言いますが、実際には Google がより高いメタレイヤーでアプリの状態を変えているのです。本当に悪い慣行です。
    Google Translate を検出して警告を表示しようとしているところです。

    • 2日前の事例と関係があるかもしれません: https://www.pewresearch.org/decoded/2025/03/21/how-a-glitch-... / https://news.ycombinator.com/item?id=43441880
    • Google Translate の干渉は厄介ですが、現在のブラウザツールでは、実際には別の挙動にするのは難しいと思います。
      たとえば「[ここをクリック]すると、さらに詳しい情報を見られます」のような文を翻訳しなければならないことがあります。別の言語にすると、リンクを文末に移して「さらに詳しい情報を見るには[ここをクリック]」のようにする必要があるかもしれません。これを行うには DOM 要素の再配置が必要で、それがインタラクティブなアプリと衝突する可能性があります。
      Google Translate チームが干渉を減らすためにできることは多いですが、新しいブラウザ API なしに完全に取り除くのは難しいと思います。
  • エンジニアリングチームに伝えました。

    • こういう一行で済む修正がバックログ地獄に長く放置されるのは、かなり気に障ります。開発者が「チケットを書くより今直すほうが早いから、そのままやろう」と言う会社がいいです。
      私の職場でも人々がそうしないので、気が狂いそうになります。エンジニアリングディレクターでさえ、すぐ処理するより時間がかからないようなことを自分のチケットとして追加します。それでも「メッセージを送るためのチケットを作らず、あなたのやり方どおりその人に直接メッセージしました」とよく言われるのは良い兆候です。
  • 会社では、ブラウザ拡張機能が妙なことをして発生する Sentry エラーが多い。
    Chrome の Google Translate も、React ベースのサイトを壊すことで悪名高い。
    結局、新しい拡張機能の問題を一つずつ無視扱いにする、退屈な分類作業になる。収集量を減らすためにクライアント側フィルタリングを使っている。全体的にバックエンドよりノイズが多いので、はるかに高いしきい値を設ける必要がある。

    • 単なるノイズだけではない。実際にユーザーはそのせいでクラッシュや他の問題に遭遇している。Google Translate 拡張機能が React や他の Web アプリに及ぼす干渉について詳しく書いた記事がある: https://martijnhols.nl/blog/everything-about-google-translat...
      フロントエンドにエラーがはるかに多いのは驚くことではない。一般的なバックエンドよりも、はるかに多くの クライアント側の差異をサポートしなければならないからだ。誰にでもうまく動く大規模な Web アプリを作るのは、非常に難しいことがある。
    • “Object captured as exception” エラーのことを言っている? Sentry が何のガイドも出してくれないあのエラーなら、うちでは単にクライアント側でフィルタリングしている。
  • Web を最も大きく壊せる変数を一つ注入するとしたら何だろう、と思う。こういうのが浮かぶ:
    --primary-color: transparent

    • --serif: "Comic Sans MS"
  • 敵対的なブラウザ拡張機能にはどう対応すべきか?

    • 自分が運営しているコミュニティ Web サイトでいちばん気に入っている苦情だ。「広告ページで写真が見えません」。広告ブロッカーを使っていますか?「はい」。広告ブロッカーは何をするものだと思っていますか……
    • ページ DOM の有効な状態を定義しておき、ページの読み込みが終わって数秒後に「敵対的な」要素と CSS スタイルをスキャンして削除することもできそうだ。
      そう考えながら The Guardian の適当なページを DevTools で開いてみたら、誰かが twitter.com を指すスクリプトと iframe を挿入していた。
    • この場合は「敵対的」という表現は少し大げさだと思う。「能力不足」で十分だ。ただし音節は少し長くなる。
      Grammarly やその技術モデルが好きなわけではないが、愚かさで十分説明できることに悪意を見いだすのは公平ではない。
      フロントエンド作業をしてから長いが、Grammarly 拡張機能と自分のコードの双方が 名前空間で分離された属性名を使うべきではないのだろうか?
    • この時点では、ブラウザ拡張機能はそもそもインストールしない。
    • 削除すればいいのでは?
  • これを利用してあのプラグインをハイジャックできるのではないかと思う。少なくともテキストを注入することはできそうだし、おそらくユーザーが拡張機能に寄せる信頼を悪用して、見栄えのいいログインフォームもレンダリングできるだろう。
    他人が制御するドキュメントに要素を注入するのは本当に安全なのか?

    • どう動くという話なのか分からない。彼らはあなたのページに CSS を注入するが、Web サイトが拡張機能の UI に何かを注入することはできない。
      できるのは Web サイト内で拡張機能の UI をまねる程度だが、それには注入など必要ない。単にデザインをコピーすればいい。