2P by GN⁺ | ★ favorite | 댓글 1개
  • David Bushell의 사이트가 일부 사용자에게 오래 깨져 보인 원인은 Grammarly 브라우저 확장이 페이지에 몰래 주입한 CSS였음
  • Firefox 기준 Grammarly 확장은 로컬 확장 자산의 스타일시트를 삽입하며, 웹페이지의 StyleSheetList로 찾기 어렵고 Content Security Policy도 우회함
  • 충돌은 Grammarly가 :root에 전역 정의한 --rem:16과 사이트의 유동 타이포그래피 계산용 --rem이 같은 이름을 쓴 데서 발생함
  • 사이트 쪽 --remcascade layer 안에 있었고, 레이어 밖 스타일이 우선하는 CSS 규칙 때문에 Grammarly 값이 계산을 덮어쓸 수 있었음
  • 임시로는 mutation observer와 !important로 버텼지만, 최종 대응은 속성명을 --🤡로 바꾸는 것이었고, 확장이 전역 :root에 평범한 이름을 주입하면 웹페이지와 쉽게 충돌할 수 있음

페이지 안으로 들어온 Grammarly CSS

  • 몇 달 동안 사이트 레이아웃이 어긋나고 크기가 이상하다는 산발적인 제보가 있었고, 스크린샷도 함께 전달됨
  • 기술에 익숙한 독자들이 Grammarly browser extension을 주요 원인으로 지목했고, David Bushell은 Firefox 기반 Mullvad browser에 직접 설치해 확인함
  • 확장 설치 시 권한은 다음을 포함함
    • 모든 웹사이트 데이터 접근
    • 알림 표시
    • 브라우저 탭 접근
  • Grammarly는 웹페이지에 로컬 확장 자산에서 로드되는 스타일시트를 주입함
    • 이 스타일시트는 웹페이지가 StyleSheetList로 찾을 수 없음
    • Content Security Policy도 우회함
    • Firefox 기준 웹사이트 자체가 감지하기 어려운 스텔스 스타일시트처럼 동작함
  • 확장은 사용자가 상호작용하지 않아도 모든 웹사이트의 <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로 추가된 웹 컴포넌트를 감지한 뒤 !important 스타일을 더해 대응함
  • 정확한 원인을 파악한 뒤에는 사이트의 커스텀 속성명을 --🤡로 바꿈
    • 이 이름은 CSS에서 유효한 커스텀 속성명임
    • --rem은 Grammarly가 전역으로 쓰기 때문에 충돌 위험이 있는 이름이 됨
  • Grammarly는 무작위 클래스명을 만들면서도 --rem이라는 일반적인 커스텀 속성명을 :root에 전역 적용했고, 확장을 실제로 사용하지 않아도 모든 웹페이지에 코드를 주입함
  • Grammarly 지원팀에는 연락했지만, 아직 문제를 이해하는 기술 담당자에게 닿지 못한 상태임

댓글과 토론

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 도우미를 설치한 게 뻔히 보이면 회의를 멈춘 적도 있음
      일부 동료는 정보가 제3자에게 넘어갈 가능성을 불편해해서, 확장 프로그램을 끌 때까지 회의를 중단함
  • Grammarly Extension 엔지니어임. 먼저 우리 확장 프로그램이 dbushell.com의 사용자 경험을 망가뜨리고 작성자가 원인을 찾는 데 시간과 노력을 쓰게 만든 점은 정말 죄송함
    의도한 일은 아니었고, 이런 일이 생기지 않도록 여러 기법을 쓰고 있음. 하지만 충분하지 않았고, 글에서 개선할 여지가 분명히 드러남
    빠른 수정으로 dbushell.com에 임시 예외를 추가했음. 동시에 적절한 스타일 격리를 보장하는 변경을 작업 중이며, 이런 문제는 절대 발생해서는 안 됨

  • Google Translate가 내 웹 앱을 망가뜨리는 비슷한 문제가 있음. 사용자는 Google Translate를 쓰면서 내 앱이 깨졌다고 불평하지만, 실제로는 Google이 더 높은 메타 계층에서 앱 상태를 바꾼 것임. 정말 나쁜 관행임
    Google Translate를 감지해서 경고를 표시하려고 하는 중

    • 이틀 전 사례와 관련 있을 수도 있음: 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 및 다른 웹 앱에 끼치는 간섭에 대해 자세히 쓴 글이 있음: https://martijnhols.nl/blog/everything-about-google-translat...
      프론트엔드에 오류가 훨씬 많은 건 놀랍지 않음. 일반적인 백엔드보다 훨씬 더 많은 클라이언트 변형을 지원해야 하기 때문임. 모두에게 잘 동작하는 큰 웹 앱을 만드는 건 매우 어려울 수 있음
    • “Object captured as exception” 오류를 말하는 건가? Sentry가 아무런 가이드를 주지 않는 그 오류라면, 우리는 그냥 클라이언트 측에서 필터링해 버림
  • 웹을 가장 크게 망가뜨릴 수 있는 변수 하나를 주입한다면 뭘까 궁금함. 이런 게 떠오름:
    --primary-color: transparent

    • --serif: "Comic Sans MS"
  • 적대적인 브라우저 확장 프로그램은 어떻게 대응해야 함?

    • 내가 운영하는 커뮤니티 웹사이트에서 가장 좋아하는 불만임. “광고 페이지에서 사진이 안 보여요.” 광고 차단기를 쓰고 있나요? “네.” 광고 차단기가 뭘 한다고 생각하나요...
    • 페이지 DOM의 유효한 상태를 정의해 두고, 페이지 로딩이 끝난 몇 초 뒤 “적대적인” 요소와 CSS 스타일을 스캔해서 삭제할 수도 있을 것 같음
      이 생각을 하면서 The Guardian의 아무 페이지나 DevTools로 열어봤더니, 누군가 twitter.com을 가리키는 스크립트와 iframe을 삽입해 놨음
    • 이 경우에는 ‘적대적’이라는 표현은 좀 과하다고 봄. ‘역량 부족’이면 충분함. 다만 음절은 더 길어짐
      Grammarly나 그 기술 모델을 좋아하진 않지만, 어리석음으로 충분히 설명되는 일에 악의를 부여하는 건 공정하지 않음
      프론트엔드 작업을 한 지 오래됐는데, Grammarly 확장 프로그램과 자기 코드 양쪽 모두 이름공간이 분리된 속성명을 써야 하지 않나?
    • 이 시점에서는 브라우저 확장 프로그램을 아예 설치하지 않음
    • 제거하면 되지 않나?
  • 이걸 이용해서 저 플러그인을 하이재킹할 수 있지 않을까 싶음. 최소한 텍스트를 주입할 수는 있을 것 같고, 아마 사용자가 확장 프로그램에 갖는 신뢰를 악용해 예쁜 로그인 폼도 렌더링할 수 있을 것임
    남이 제어하는 문서에 요소를 주입하는 게 정말 안전한가?

    • 어떻게 동작한다는 건지 모르겠음. 그들은 네 페이지에 CSS를 주입하지만, 웹사이트가 확장 프로그램 UI에 뭔가를 주입할 수는 없음
      할 수 있는 건 웹사이트 안에서 확장 프로그램 UI를 흉내 내는 정도인데, 그건 굳이 주입이 필요 없음. 그냥 디자인을 베끼면 됨