2 คะแนน โดย GN⁺ 2025-03-31 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • สาเหตุที่เว็บไซต์ของ David Bushell แสดงผลพังมานานสำหรับผู้ใช้บางส่วน คือ CSS ที่ ส่วนขยายเบราว์เซอร์ Grammarly แอบฉีดเข้าไปในหน้าเว็บ
  • ใน Firefox ส่วนขยาย Grammarly จะแทรก สไตล์ชีต จากทรัพยากรภายในส่วนขยายเอง ทำให้หาได้ยากผ่าน StyleSheetList ของเว็บเพจ และยังหลบเลี่ยง Content Security Policy ได้ด้วย
  • ปัญหาการชนกันเกิดจาก Grammarly กำหนด --rem:16 แบบ global ไว้ที่ :root ขณะที่เว็บไซต์ก็ใช้ --rem ชื่อเดียวกันสำหรับคำนวณ ตัวพิมพ์แบบไหลลื่น
  • ฝั่งเว็บไซต์กำหนด --rem ไว้ใน cascade layer และตามกฎ CSS นั้นสไตล์ที่อยู่นอกเลเยอร์จะมีลำดับความสำคัญเหนือสไตล์ในเลเยอร์ ทำให้ค่าของ Grammarly สามารถไปทับการคำนวณได้
  • วิธีแก้ชั่วคราวคือใช้ mutation observer กับ !important พยุงไว้ แต่การรับมือสุดท้ายคือเปลี่ยนชื่อพร็อพเพอร์ตีเป็น --🤡 และถ้าส่วนขยายฉีดชื่อทั่วไปลงใน :root แบบ global ก็ชนกับเว็บเพจได้ง่าย

CSS ของ Grammarly ที่เข้ามาอยู่ในหน้าเว็บ

  • ตลอดหลายเดือนมีรายงานประปรายว่าเลย์เอาต์ของเว็บไซต์เพี้ยนและขนาดแปลกไป พร้อมมีการส่งภาพหน้าจอมาให้ดู
  • ผู้อ่านที่คุ้นเคยด้านเทคนิคชี้ว่า Grammarly browser extension คือสาเหตุหลัก และ David Bushell ก็ได้ติดตั้งบน Mullvad browser ที่อิง Firefox เพื่อยืนยันด้วยตนเอง
  • เมื่อทำการติดตั้ง ส่วนขยายจะขอสิทธิ์ดังนี้
    • เข้าถึงข้อมูลของทุกเว็บไซต์
    • แสดงการแจ้งเตือน
    • เข้าถึงแท็บของเบราว์เซอร์
  • Grammarly ฉีด สไตล์ชีต ที่โหลดจากทรัพยากรภายในส่วนขยายลงในเว็บเพจ
    • เว็บเพจไม่สามารถค้นหาสไตล์ชีตนี้ผ่าน StyleSheetList ได้
    • ยังหลบเลี่ยง Content Security Policy ได้ด้วย
    • สำหรับ Firefox มันทำงานคล้าย สไตล์ชีตล่องหน ที่ตัวเว็บไซต์เองตรวจจับได้ยาก
  • ส่วนขยายจะเพิ่ม custom element ชื่อ <grammarly-desktop-integration> ลงในเอกสาร <html> ของทุกเว็บไซต์ แม้ผู้ใช้จะไม่ได้โต้ตอบอะไรเลยก็ตาม

ชื่อ --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));
}
  • ตัวเว็บไซต์เองก็ใช้ custom property ชื่อ --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))
    );
  }
}
  • --rem ของเว็บไซต์ถูกกำหนดไว้ใน cascade layer และสไตล์ที่อยู่นอกเลเยอร์จะมีลำดับความสำคัญเหนือสไตล์ในเลเยอร์ โดยไม่ขึ้นกับ CSS specificity
    • ลำดับของซอร์สก็มีผลด้วย ดังนั้น --rem ของ Grammarly จึงอาจเป็นฝ่ายชนะ
    • ผลคือสูตรคำนวณของเว็บไซต์พัง และเกิดปัญหาเลย์เอาต์
  • ในช่วงแรกมีการรับมือโดยใช้ mutation observer ตรวจจับเว็บคอมโพเนนต์ที่ถูกเพิ่มเข้ามา แล้วเติมสไตล์ !important เข้าไป
  • หลังระบุสาเหตุที่แท้จริงได้แล้ว จึงเปลี่ยนชื่อ custom property ของเว็บไซต์เป็น --🤡
    • ชื่อนี้เป็นชื่อ custom property ที่ใช้ได้ถูกต้องใน CSS
    • ส่วน --rem กลายเป็นชื่อที่เสี่ยงชนกัน เพราะ Grammarly ใช้แบบ global
  • Grammarly สุ่มสร้างชื่อคลาส แต่กลับใช้ชื่อ custom property ทั่วไปอย่าง --rem แบบ global กับ :root และยังฉีดโค้ดลงทุกเว็บเพจแม้ผู้ใช้จะไม่ได้ใช้งานส่วนขยายจริง ๆ
  • มีการติดต่อทีมซัพพอร์ตของ Grammarly แล้ว แต่ตอนนี้ยังไม่สามารถติดต่อถึงผู้รับผิดชอบด้านเทคนิคที่เข้าใจปัญหาได้

1 ความคิดเห็น

 
GN⁺ 2025-03-31
ความคิดเห็นจาก Hacker News
  • กรณีที่เคยเจอจากปัญหาส่วนขยายจะแตกต่างออกไปเล็กน้อย เรากำลังเผยแพร่ส่วนขยายที่ช่วยให้ สลับพร็อกซีเซิร์ฟเวอร์ ได้ง่ายสำหรับการทดสอบตำแหน่งทางภูมิศาสตร์
    เมื่อไม่กี่เดือนก่อนเราเดโมให้ลูกค้าดูได้แย่ที่สุดครั้งหนึ่ง เพราะผลิตภัณฑ์ดูเหมือนไม่ทำงานเลย หลังจากดีบักอยู่นาน ก็พบว่าการอัปเดตล่าสุดของ ส่วนขยาย 1Password ทำให้ส่วนขยายของเราพัง 1Password subscribe อีเวนต์การยืนยันตัวตนไว้แต่ไม่ return ทำให้หมดเวลา และ subscriber ของเราจึงไม่ถูกเรียก ส่วนขยายของเราสั่งเบราว์เซอร์ให้เปลี่ยนพร็อกซีเซิร์ฟเวอร์แล้วเตรียมพร้อมให้ข้อมูลรับรอง แต่ไม่มี request เข้ามา ทีมซัพพอร์ตของ 1Password ดีกว่า Grammarly แต่การโน้มน้าว PM ที่ไม่รู้ว่าเป็นใครผ่านทีมซัพพอร์ตให้จัดลำดับความสำคัญนั้นยาก
    ต่อมาพบว่าส่วนขยายบางตัวที่จำเป็นสำหรับเว็บไซต์รัฐบาลรัสเซียก็มีปัญหาเดียวกัน

    • สถานการณ์คล้ายกัน 1Password ยังทำให้ฟังก์ชันการเปิด Chrome side panel UI จาก content script ของส่วนขยายอื่นพังอยู่ มันทำให้ trust flag ที่บอกว่าอีเวนต์มาจากการโต้ตอบของผู้ใช้เสีย
      จากมุมมองของคนที่อยู่ฝั่งส่วนขยายมานานกว่า 10 ปี สุดท้าย Google มีความรับผิดชอบมากทีเดียว นอกเหนือจากประเด็นการเมืองเรื่องการเปลี่ยนแปลงตัวบล็อกโฆษณาแล้ว Manifest v3 ยังแย่กว่าที่คาดไว้มากในหลายด้าน
      โดยรวมรู้สึกว่าคุณภาพของโค้ดเบส Chromium ลดลงจากเมื่อก่อนมาก
  • ถ้าจะ inject สคริปต์หรือสไตล์ลงในหน้าเว็บที่ไม่รู้จัก อย่างน้อยก็ควรแยก namespace ของตัวแปร ให้ชัดเจน

    • เรื่องนี้น่าหงุดหงิดมาก เพราะราว 5–6 เดือนก่อน ในการสัมภาษณ์งาน ผมเล่าเรื่องสตาร์ทอัพ Instagram/branding ที่เคยทำงานเป็น CTO และนักพัฒนาหลักเมื่อปี 2014 ตอนนั้นผมอธิบายว่าเราได้สร้าง build system เพื่อให้ CSS class และ JavaScript object มีการ แยก namespace อย่างถูกต้อง ทำให้ไม่มีโอกาสชนกัน และจัดการได้ว่าต้องโหลดสคริปต์ใดอย่างแม่นยำตามวิดเจ็ตที่มีอยู่บนไซต์ลูกค้า third-party
      แต่ผู้สัมภาษณ์กลับพูดเหมือนปัด ๆ ว่าเรื่องแบบนั้นเครื่องมือสมัยนี้ทำให้หมดแล้ว และทุกคนก็ทำกันอยู่ ผมก็จำเป็นต้องเห็นด้วยอยู่บ้าง เพราะตอนนี้ไม่ได้ทำงานส่วนนั้นแล้วจึงไม่รู้จริง ๆ แต่กลายเป็นว่าไม่ได้มีทุกคนทำแบบนั้นเลย
    • การแยก namespace ไม่ได้ดีต่อคนอื่นเท่านั้น แต่ยังสะดวกกับตัวเองด้วย ที่ทำงานก่อนหน้านี้ผมทำ browser automation ที่ผู้ใช้มองไม่เห็น แม้ไม่ใช่ส่วนขยาย แต่การแยก namespace ก็ยังมีประโยชน์
      เราแยกได้ชัดเจนว่าอะไรคือสิ่งที่เราแทรกเข้าไป และอะไรคือของเดิม พร้อมทั้งหลีกเลี่ยงการชนกันที่อาจเกิดขึ้นได้
    • ผมออกจากสาย frontend มาสักพักแล้ว ทุกวันนี้การ แยก namespace ของ CSS ปกติจัดการกันอย่างไร?
    • วิธีที่ดีกว่านั้นคือใช้ Shadow DOM
  • เวลาหน้าจอถูกแชร์หรืออัดวิดีโอ แล้วเห็นผู้บุกรุกสีเขียวตัวนั้นติดตั้งเป็นค่าเริ่มต้นอยู่บนทุกเว็บไซต์ มันน่ากลัว ไม่ใช่แค่เรื่องรบกวนสายตา แต่ยังมีเรื่องความเป็นส่วนตัวและ attack vector ที่ชัดเจนตามมาด้วย
    Chrome สามารถเปิดส่วนขยายเฉพาะเวลาจำเป็นได้ แต่ไม่เข้าใจว่าทำไมไม่มีใครทำแบบนั้น และก็สงสัยว่าทำไมค่าเริ่มต้นของทุกเบราว์เซอร์ถึงไม่เป็นแบบนั้น

    • รู้สึกว่าโชคดีพอสมควรที่มีเพื่อนร่วมงานที่ใส่ใจเรื่องพวกนี้ เคยหยุดการประชุมเมื่อเห็นชัดว่าผู้เข้าร่วมบางคนติดตั้งส่วนขยายบางตัวหรือ ผู้ช่วย AI หลายประเภท
      เพื่อนร่วมงานบางคนไม่สบายใจกับความเป็นไปได้ที่ข้อมูลจะถูกส่งต่อไปยังบุคคลที่สาม จึงหยุดการประชุมจนกว่าจะปิดส่วนขยาย
  • ผมเป็นวิศวกรของ Grammarly Extension ก่อนอื่นต้องขออภัยจริง ๆ ที่ส่วนขยายของเราทำลาย ประสบการณ์ผู้ใช้ ของ dbushell.com และทำให้ผู้เขียนต้องเสียเวลาและความพยายามในการหาสาเหตุ
    นี่ไม่ใช่สิ่งที่ตั้งใจ และเราก็ใช้หลายเทคนิคเพื่อไม่ให้เรื่องแบบนี้เกิดขึ้น แต่ยังไม่เพียงพอ และบทความก็ชี้ให้เห็นชัดเจนว่ายังมีพื้นที่ให้ปรับปรุง
    ในฐานะการแก้ไขแบบเร่งด่วน เราได้เพิ่มข้อยกเว้นชั่วคราวให้ dbushell.com แล้ว ขณะเดียวกันก็กำลังทำการเปลี่ยนแปลงเพื่อรับประกัน การแยกสไตล์ อย่างเหมาะสม ปัญหาแบบนี้ไม่ควรเกิดขึ้นเลย

  • มีปัญหาคล้ายกันที่ Google Translate ทำให้เว็บแอปของผมพัง ผู้ใช้ใช้ Google Translate แล้วบ่นว่าแอปของผมเสีย แต่จริง ๆ แล้ว Google เป็นฝ่ายเปลี่ยนสถานะของแอปใน meta layer ที่สูงกว่า เป็นแนวปฏิบัติที่แย่มาก
    กำลังพยายามตรวจจับ 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 มีหลายอย่างที่ทำได้เพื่อลดการแทรกแซง แต่ผมคิดว่ายากที่จะกำจัดให้หมดโดยไม่มี browser API ใหม่
  • ส่งต่อให้ทีมวิศวกรรมแล้ว

    • ค่อนข้างน่าหงุดหงิดที่การแก้แบบบรรทัดเดียวแบบนี้ถูกทิ้งไว้ในนรกของ backlog นาน ๆ ผมอยากได้บริษัทที่นักพัฒนาพูดว่า “แก้ตอนนี้เลยเร็วกว่าการเขียน ticket งั้นทำเลยแล้วกัน”
      ที่ที่ผมทำงานอยู่ คนก็ไม่ทำแบบนั้นจนผมแทบคลั่ง แม้แต่ director ฝ่ายวิศวกรรมยังเพิ่มงานที่ใช้เวลาน้อยกว่าการจัดการ ticket เข้าเป็น ticket ของตัวเอง แต่ก็ถือเป็นสัญญาณที่ดีที่ได้ยินบ่อย ๆ ว่า “ผมไม่ได้สร้าง ticket เพื่อส่งข้อความ แต่ส่งข้อความหาคนนั้นโดยตรงตามวิธีของคุณแล้ว”
  • ในบริษัทมี ข้อผิดพลาดใน Sentry จำนวนมากที่เกิดจากส่วนขยายเบราว์เซอร์ทำอะไรแปลก ๆ
    Google Translate ของ Chrome ก็ขึ้นชื่อว่าไปทำให้ไซต์ที่ใช้ React พัง
    สุดท้ายกลายเป็นงานคัดแยกน่าเบื่อที่ต้องค่อย ๆ ตั้งให้มองข้ามปัญหาจากส่วนขยายใหม่ ๆ ทีละรายการ เราใช้การกรองฝั่งไคลเอนต์เพื่อลดปริมาณที่เก็บ โดยรวมแล้วมี noise มากกว่าแบ็กเอนด์ จึงต้องตั้ง threshold สูงกว่ามาก

    • มันไม่ใช่แค่ noise ธรรมดา จริง ๆ แล้วผู้ใช้เจอ crash หรือปัญหาอื่นเพราะเรื่องนี้ มีบทความที่เขียนละเอียดเกี่ยวกับการรบกวนของส่วนขยาย Google Translate ต่อ React และเว็บแอปอื่น ๆ: https://martijnhols.nl/blog/everything-about-google-translat...
      ไม่แปลกที่ฝั่งฟรอนต์เอนด์จะมีข้อผิดพลาดมากกว่ามาก เพราะต้องรองรับ ความหลากหลายของไคลเอนต์ มากกว่าแบ็กเอนด์ทั่วไปอย่างมาก การสร้างเว็บแอปขนาดใหญ่ที่ทำงานได้ดีกับทุกคนอาจเป็นเรื่องยากมาก
    • หมายถึงข้อผิดพลาด “Object captured as exception” ใช่ไหม? ถ้าเป็นข้อผิดพลาดนั้นที่ Sentry ไม่ให้คำแนะนำอะไรเลย เราก็แค่กรองทิ้งฝั่งไคลเอนต์
  • สงสัยว่าถ้าจะฉีดตัวแปรสักตัวที่ทำให้เว็บพังได้หนักที่สุด ควรเป็นอะไร นึกออกแบบนี้:
    --primary-color: transparent

    • --serif: "Comic Sans MS"
  • ควรรับมือกับส่วนขยายเบราว์เซอร์ที่เป็นปฏิปักษ์อย่างไร?

    • นี่เป็นคำร้องเรียนที่ผมชอบที่สุดบนเว็บไซต์ชุมชนที่ผมดูแล “รูปในหน้าโฆษณาไม่ขึ้น” ใช้ ad blocker อยู่ไหม? “ใช่” แล้วคิดว่า ad blocker มันทำอะไรล่ะ...
    • อาจกำหนดสถานะที่ถูกต้องของ DOM ในหน้าไว้ แล้วหลังโหลดหน้าเสร็จสักไม่กี่วินาที ก็สแกนหาองค์ประกอบและสไตล์ CSS ที่ “เป็นปฏิปักษ์” แล้วลบออกได้
      ตอนคิดเรื่องนี้ ผมลองเปิดหน้าไหนก็ได้ของ The Guardian ใน DevTools แล้วพบว่ามีใครบางคนแทรกสคริปต์และ iframe ที่ชี้ไปยัง twitter.com ไว้
    • ในกรณีนี้ ผมว่าคำว่า ‘เป็นปฏิปักษ์’ ค่อนข้างแรงไปหน่อย แค่ ‘ขาดความสามารถ’ ก็น่าจะพอแล้ว แต่ออกเสียงยาวกว่าหน่อย
      ผมไม่ได้ชอบ Grammarly หรือโมเดลเทคโนโลยีของมัน แต่การโยนความมุ่งร้ายให้กับเรื่องที่อธิบายได้ด้วยความโง่เขลานั้นไม่ยุติธรรม
      ผมไม่ได้ทำงานฟรอนต์เอนด์มานานแล้ว แต่ทั้งส่วนขยาย Grammarly และโค้ดของตัวเองไม่ควรใช้ ชื่อแอตทริบิวต์ที่แยก namespace กันหรือ?
    • มาถึงจุดนี้ ผมไม่ติดตั้งส่วนขยายเบราว์เซอร์เลย
    • ก็เอาออกไม่ได้เหรอ?
  • ผมคิดว่าน่าจะใช้สิ่งนี้เพื่อ hijack ปลั๊กอินนั้นได้ อย่างน้อยก็น่าจะฉีดข้อความเข้าไปได้ และอาจถึงขั้น render ฟอร์มล็อกอินสวย ๆ เพื่อใช้ประโยชน์จากความเชื่อใจที่ผู้ใช้มีต่อส่วนขยายได้ด้วย
    การฉีดองค์ประกอบเข้าไปในเอกสารที่คนอื่นควบคุมอยู่นั้นปลอดภัยจริงหรือ?

    • ไม่เข้าใจว่ามันจะทำงานอย่างไร พวกเขาฉีด CSS เข้าไปในหน้าของคุณ แต่เว็บไซต์ไม่สามารถฉีดอะไรเข้าไปใน UI ของส่วนขยายได้
      สิ่งที่ทำได้ก็แค่เลียนแบบ UI ของส่วนขยายภายในเว็บไซต์เอง ซึ่งไม่จำเป็นต้องฉีดอะไรเลย แค่ลอกดีไซน์มาก็พอ