1 คะแนน โดย GN⁺ 2024-03-17 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Ladybird จัดการเว็บคอนเทนต์ปกติได้ในระดับหนึ่ง แต่เมื่อรัน Domato ซึ่งเป็น DOM fuzzer ของ Google Project Zero ก็เผย edge case ที่ซ่อนอยู่ในเอนจินเบราว์เซอร์ได้อย่างรวดเร็ว
  • พบและแก้ไขบั๊กจริง 5 รายการจาก อินพุตผิดปกติที่เกิดขึ้นได้จริง เช่น DOM ที่สร้างด้วย JavaScript เพื่อเลี่ยงกฎของ parser, document ที่ไม่มี window และการอ้างอิง SVG แบบวนลูป
  • ข้อสมมติโดยนัยภายใน implementation เช่น การสมมติว่า `` มี table ancestor, การสมมติว่า document ของ DOMParser มี window และความผิดพลาดในการไล่ sibling ของ Element.before() นำไปสู่ crash หรือ infinite loop
  • ปัญหาการเข้าถึง contentWindow ของ iframe ที่ถูกลบ ไม่ใช่ข้อบกพร่องเฉพาะของ Ladybird เท่านั้น แต่ยังเกี่ยวข้องกับสมมติฐานเรื่อง browsing context ในสเปก HTML จนนำไปสู่ issue ใน WHATWG HTML
  • Fuzzer อย่าง Domato เปิดเผยปัญหาด้านความปลอดภัยและเสถียรภาพที่ยากจะจับได้ด้วยการทดสอบเว็บเพจปกติเท่านั้น และโจทย์ถัดไปของ Ladybird คือทำให้เสถียรพอจะทนต่อการ fuzzing ต่อเนื่อง แล้วจึงรันแบบอัตโนมัติ

ทดสอบความอึดของ Ladybird ด้วย Domato

  • Ladybird จัดการเว็บคอนเทนต์ที่สร้างมาอย่างถูกต้องได้ในระดับหนึ่ง แต่ลองใช้เครื่องมือวิจัยความปลอดภัยส่งอินพุตแปลก ๆ เข้าไปเพื่อดูว่าจะเกิดปัญหาอะไรบ้าง
  • เครื่องมือที่ใช้คือ DOM fuzzer Domato ของ Google Project Zero
    • Domato สร้างเว็บเพจสุ่มที่ผสม HTML, CSS, JavaScript ซึ่งส่วนใหญ่ valid แต่มีความแปลก
    • โหลดเพจที่สร้างขึ้นใน debug build ของ Ladybird แล้วสังเกตพฤติกรรม
  • README ของ Domato ชูว่าพบ bugs จำนวนมากในเบราว์เซอร์หลัก ๆ จึงประเมินว่าน่าจะพบข้อบกพร่องที่มีนัยสำคัญใน Ladybird ได้เช่นกัน

Null pointer dereference เมื่อ อยู่ใน

  • ปัญหาแรกถูกพบภายในไม่ถึง 1 วินาที และสามารถย่อ output ขนาด 562KiB จาก Domato ให้เหลือรูปแบบด้านล่างได้

let mfrac = document.createElement("mfrac");
mfrac.appendChild(document.createElement("th"));
document.body.appendChild(mfrac);

  • ใน build ของ Ladybird ที่เปิด UBSAN การเรียก table_containing_cell ใน HTMLTableCellElement.cpp ทำให้เกิด null pointer dereference
  • สาเหตุคือ implementation ของ และ ใน Ladybird สมมติว่า DOM tree ด้านบนมี `` อยู่เสมอ
    • HTML parser ไม่อนุญาต markup อย่าง ``
    • เบราว์เซอร์ที่ทำตามสเปก เมื่อโหลด markup ด้านบน จะสร้าง `` หนึ่งตัวที่ด้านในว่างเปล่า
    • แต่ถ้าสร้าง node โดยตรงด้วย JavaScript DOM API จะเลี่ยงกฎบางส่วนของ parser และใส่ ไว้ใน ได้
  • โค้ดที่มีปัญหาถูกใช้เพื่อ implement พฤติกรรมเก่า ๆ ของ และ ที่นำ CSS border และ padding ไปใช้กับแต่ละ cell ไม่ใช่แค่ table box
  • การแก้ไขทำโดยลบสมมติฐานที่ว่า และ จะมี `` ancestor เสมอ
    • ใช้ first_ancestor_of_type() แทน table_containing_cell(*this)
    • ถ้าไม่มี table ancestor ให้ return ทันที
    • commit ที่แก้ไขอยู่ ที่นี่

การกำหนด event handler `` ใน document ที่ไม่มี window

  • ปัญหาที่สองก็ถูกพบภายในไม่ถึง 1 วินาทีเช่นกัน และ output ขนาด 472KiB จาก Domato ถูกย่อเหลือโค้ดต่อไปนี้

var parser = new DOMParser();
var doc = parser.parseFromString("", "text/html");
var body = doc.createElement("body");
body.onblur = null;

  • Ladybird หยุดทำงานจากความล้มเหลวในการตรวจสอบ GCPtr
  • ประเด็นสำคัญคือ พฤติกรรมพิเศษ ของ property event handler onfoo บน ``
    • เพื่อความเข้ากันได้กับเว็บคอนเทนต์เก่า การกำหนด document.body.onfoo ต้องถูกส่งต่อไปยัง window.onfoo
    • แต่ document ที่สร้างด้วย DOMParser ไม่มี object window
  • โมเดล object ภายในของ Ladybird ถูกวางโครงสร้างผิด โดยถือว่า document ทุกตัวมี window เสมอ
  • หลังแก้ไข Document::window() จะ return ค่า nullable และหลายจุดจัดการกรณี null
    • เมื่อกำหนด document.body.onblur ใน document ที่ไม่มี window จะไม่เกิดอะไรขึ้นเหมือนเบราว์เซอร์อื่น

การอ้างอิงแบบวนลูปของ SVG ``

  • ปัญหาที่สามคือ infinite recursion ที่เกิดเมื่อ SVG gradient อ้างอิงตัวเอง

  • SVG ต้องรองรับทั้ง inline SVG ใน HTML และรูปแบบภาพภายนอก และ gradient สามารถอ้างอิง gradient อื่นเพื่อสืบทอดสีได้
  • implementation ของ Ladybird ไม่ได้พิจารณากรณีที่ gradient อ้างอิงตัวเอง จึงไล่ตาม chain การอ้างอิงแล้ววนลูปต่อเนื่อง
  • หากป้องกันเฉพาะกรณีอ้างอิงตัวเองแบบตรง ๆ จะยังจัดการ circular reference หลายขั้นไม่ได้

  • วิธีจัดการที่ถูกต้องคือ track gradient ทั้งหมดที่เคยเยี่ยมชม และเมื่อพบ gradient ที่เคยเยี่ยมชมแล้วอีกครั้ง ให้หยุดการไล่ chain
  • Firefox แสดงข้อความบ่นใน developer console สำหรับ gradient ประเภทนี้

การเข้าถึง property window ของ iframe ที่ถูกลบ และ bug ในสเปก HTML

  • ปัญหาที่สี่เกิดขึ้นเมื่อเรียก getSelection() จาก contentWindow ที่เก็บไว้ก่อนหน้า หลังจากลบ iframe แล้ว

window.onload = function() {
let iframe = document.querySelector("iframe")
let iframeWindow = iframe.contentWindow;
iframe.remove();
iframeWindow.getSelection();
}

  • Ladybird แสดง binding runtime error ใน WindowProxy.cpp จาก null pointer reference ไปยัง BrowsingContext
  • เมื่อ iframe ถูกลบออกจาก DOM แล้ว content document ของมันจะถูกแยกออกจาก browsing context ของตัวเอง
  • เมื่อดึงหรือตั้งค่า property ของ object window จะมีการรันอัลกอริทึมในสเปก HTML ชื่อ "check if an access between two browsing contexts should be reported"
    • อัลกอริทึมนี้ตรวจ browsing context ของ window ที่เข้าถึงและ window เป้าหมายที่ถูกเข้าถึง
    • สเปกสมมติผิดว่า ณ เวลาที่มีการเข้าถึง property ทั้งสอง window จะมี browsing context ที่เชื่อมต่ออยู่เสมอ
  • มีการเปิด issue ต่อสเปก HTML และใน Ladybird มีการเพิ่ม null check ไปก่อน
  • หากพบ bug ในสเปกระหว่างทำงานกับ Ladybird ก็สามารถช่วยปรับปรุงสเปกให้ทุกคนได้ผ่าน bug report หรือข้อเสนอแก้ไข

Infinite loop ใน Element.before()

  • ปัญหาที่ห้าคือหน้าโหลดไม่จบและใช้ CPU 100%

two.before(one);

  • สาเหตุคือความผิดพลาดใน logic ของ implementation before() ที่ต้องหา sibling ก่อนหน้าตัวแรกของ `` ที่ไม่อยู่ใน arguments
  • loop เดิมดึง node->previous_sibling() ใหม่ทุกครั้ง
while (auto previous_sibling = node->previous_sibling()) {
    // check if previous_sibling is one of the arguments
}
  • ที่ถูกต้องคือควรไล่ไปตาม chain ของ sibling โดยเลื่อนต่อเนื่องด้วย previous_sibling->previous_sibling()
for (auto sibling = node->previous_sibling(); sibling; sibling = sibling->previous_sibling()) {
    // check if previous_sibling is one of the arguments
}

ผลการ fuzzing และขั้นตอนถัดไป

  • ใน session นี้พบ bug จริง 5 รายการ หนึ่งในนั้นคือ bug ในสเปก HTML และทั้งหมดได้รับการแก้ไขแล้ว
  • เผยให้เห็นว่าเมื่อเจออินพุตที่แปลกและคาดไม่ถึง Ladybird ล้มได้เร็วมาก
  • Fuzzer อย่าง Domato เป็นทรัพยากรที่มีประโยชน์สำหรับผู้ที่ต้องการทำให้ซอฟต์แวร์แข็งแกร่งขึ้น
  • ขั้นตอนถัดไปคือทำให้ Ladybird เสถียรถึงระดับที่ทนต่ออินพุต fuzzing แบบต่อเนื่องได้
  • เมื่อเสถียรเพียงพอแล้ว มีแผนจะรันแบบอัตโนมัติบนคลาวด์ที่ไหนสักแห่งเพื่อค้นหาปัญหาเพิ่มเติม

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

 
GN⁺ 2024-03-17
ความคิดเห็นจาก Hacker News
  • แสดงให้เห็นได้ดีว่าทำไม การมี implementation อิสระหลายตัว ของสเปกจึงมีคุณค่า
    แค่บทความนี้บทความเดียวก็พบช่องโหว่หนึ่งจุดในสเปกแล้ว และดูเหมือนอาจมีมากกว่านี้ หรือจะมีเพิ่มอีกในอนาคต

    • ใช่ เราเคยค้นพบและ รายงาน ปัญหามากมายทั่วทั้งสเปก HTML, CSS และ JS แล้ว
      การมี implementation อิสระหลายตัวสำคัญต่อสุขภาพระยะยาวของเว็บแพลตฟอร์ม ดังนั้นเราก็กำลังพยายามทำบทบาทนั้นอยู่
    • สงสัยว่าทำไม fuzzer ตัวนั้นถึงไม่พบ bug ใน browser ยอดนิยม
    • ข้อสรุปนั้นดูเหมือนเป็นการ กระโดดสรุป ไปหน่อย
      เช่น ถ้าผมทวีตว่า “มะเขือม่วงเป็นผักที่ผมชอบที่สุด” แล้วมีคนมาแก้ให้ทันทีว่า “จริง ๆ เป็นผลไม้” จากนั้นเอาไปบอกว่า “นี่พิสูจน์คุณค่าของ Twitter แล้ว” ความรู้สึกก็คล้าย ๆ กัน
      ไม่ได้หมายความว่างานนี้เองหรือการมี implementation หลายตัวของสเปกไม่มีคุณค่า แต่ผมคิดว่าตัวอย่างเฉพาะนี้ยังไม่เพียงพอให้สรุปนัยนั้นได้
  • ชอบที่โปรเจกต์นี้แสดงให้เห็นอยู่เรื่อย ๆ ว่า ทีมเล็ก ๆ ก็สร้างสิ่งน่าทึ่งได้
    คิดว่าการทำเรื่องแบบนี้ในบริษัทที่มีผู้มีส่วนได้ส่วนเสียจำนวนมากคงยากกว่านี้มาก

    • โปรเจกต์นี้เจ๋ง แต่ก็สงสัยว่าแนวทางที่เริ่มจาก “ทำให้จัดการคอนเทนต์เว็บปกติได้พอประมาณ” แล้วค่อยย้อนกลับไปแก้ให้ตรงตามสเปก พฤติกรรม browser โดยพฤตินัย และประเด็นความปลอดภัยที่อาจเกิดขึ้น จะนำไปสู่ production browser จริง ๆ ได้หรือไม่
      ถ้าเป็นโปรเจกต์งานอดิเรก ก็ย้อนกลับไปสร้างใหม่ได้เสมอ แต่ก็อดรู้สึกไม่ได้ว่าบางอย่างในนี้ควรถูกใส่ไว้ในสถาปัตยกรรมตั้งแต่แรกหรือเปล่า
  • implement SVG แล้วเหรอ? กำลังติดตามอย่างสนใจ เพราะคืบหน้าเร็วกว่าที่คิดไว้มาก

    • implement สเปก SVG ไปค่อนข้างมากแล้ว แต่ยังมีหลายส่วนที่ขาดอยู่
      โดยเฉพาะ animation เป็นส่วนใหญ่ที่ยังขาดไป
  • สำหรับ issue #3 การใส่ ขีดจำกัดความลึกสูงสุด ให้ gradient ที่ชี้ไปยัง gradient อื่นก็ดูเป็นความคิดที่ดี
    อาจเป็น defense-in-depth เผื่อ logic “เคยเห็น reference นี้มาก่อนหรือยัง” มีข้อผิดพลาดหรือข้อจำกัด
    ผมไม่ค่อยรู้เรื่อง SVG gradient แต่ถึงอาจมีเหตุผลชอบธรรมที่ reference chain ต้องยาวต่อกันเป็น 1000 ชั้น ในสภาพแวดล้อมจริงถ้าเห็นแบบนั้นก็น่าจะเป็นการโจมตีหรือ input จาก fuzzer มากกว่า

    • ฝั่งป้องกัน malware เราเห็นการ ใช้งานโครงสร้างในทางที่ผิด แบบนี้ตลอด และไม่เคยเห็นกรณีถูกต้องตามกฎหมายที่ลึกเกิน 5 ชั้นเลย
  • ผมกำลังเขียนคอมเมนต์นี้บน Ladybird
    ตอนนี้ Hacker News ทำงานบน Ladybird แล้ว
    ผมใช้ Ladybird ตอนเข้าเว็บอย่าง Hacker News หรือ OSnews วันละไม่กี่นาที
    มันช้าและพังง่าย แต่ก็ใช้งานได้ เมื่อคิดว่าโปรเจกต์นี้ยังใหม่มากและเขียนทุกอย่างขึ้นมาตั้งแต่ศูนย์จริง ๆ แค่นั้นก็น่าทึ่งแล้ว
    รอคอยอย่างมากที่จะได้เห็น Ladybird เติบโตขึ้น

  • น่าสนใจ แต่รำคาญที่ developer แทบทุกคนจบแบบ “เจอแล้ว! commit แก้แล้ว จบ!” อย่างที่เห็นใน issue #1
    ไม่ควรทำแบบนั้น ต้องเข้าใจให้ชัดว่าอะไรผิดพลาดกันแน่ เช่น ถ้าปัญหาคือสมมติฐานว่า “ต้องมี parent อยู่เสมอ” ก็ต้องค้นหาความผิดพลาดประเภทเดียวกันทั่วทั้ง codebase
    ต้องใช้ความคิดสร้างสรรค์หาว่าเรื่องเดียวกันนี้อาจเกิดขึ้นที่ไหนอีก ไม่มีทางมีอยู่แค่จุดเดียว
    ที่ software สมัยใหม่เป็นฝันร้ายเต็มไปด้วย bug และเชื่อถือได้ยาก ส่วนใหญ่เป็นเพราะข้อจำกัดแบบทุนนิยม แต่ถึงอย่างนั้นเราก็ยังทำได้ดีกว่านี้

  • สงสัยว่าปีนี้ Web Engines Hackfest จะมี Ladybird ไปปรากฏตัวหรือไม่

  • ขอพูดต่างเรื่องนิดหน่อย แต่สงสัยว่า วิดีโอแฮ็กกิ้ง บน YouTube เป็นอย่างไรบ้าง
    เมื่อก่อนผมเคยรอวิดีโอใหม่ ๆ แต่เหมือนไม่ได้เห็นมาพักหนึ่งแล้ว

    • พูดตรง ๆ คือหลังจากอัปโหลดวิดีโอไปมากกว่า 1000 รายการ ก็เริ่มเหนื่อยนิดหน่อย
      วิดีโออัปเดตรายเดือนยังลงอยู่ แต่หลังจากวิดีโอแฮ็กกิ้งล่าสุดก็ผ่านมาหลายเดือนแล้ว
      ถึงอย่างนั้นก็ยังทำงานกับ Ladybird ทุกวัน และด้วยการสนับสนุนอย่างใจกว้างจาก Shopify และที่อื่น ๆ เมื่อปีที่แล้ว ตอนนี้ยังดูแล วิศวกร full-time สองคน อยู่ด้วย