บทความเทคนิค

เล่นซ้ำ EMF และ WMF ที่ไม่น่าเชื่อถืออย่างปลอดภัยใน HotXLS

เวิร์กบุ๊ก Excel แบกภาพ EMF และ WMF ได้ และวิธีวาดตามธรรมเนียมคือส่งสตรีมไบต์ให้ metafile player ของระบบปฏิบัติการ นั่นเป็นการตัดสินใจที่ควรถูกจ้องเข้าไปตรง ๆ: metafile คือสตรีมคำสั่งแบบ serialised ของ API ด้านกราฟิก และการเล่นมันซ้ำหมายถึงการปล่อยให้ไฟล์ที่วิ่งเข้ามาทางอีเมลเป็นคนขับ graphic driver HotXLS เลือกเส้นทางตรงข้าม XLSDecodeVectorScene parse metafile เอง ตรวจ header, ขนาด record ทุกตัว, จำนวน record ที่ประกาศไว้ และตำแหน่งพอดีของ record ปิดไฟล์ ปฏิเสธ escape record ทั้งหมด แล้วคืน TXLSVectorScene ที่เป็นคำสั่งวาดระดับ primitive ให้ backend ทั้ง Canvas และ SVG เล่นซ้ำผ่านโค้ดของตัวเอง ไม่มีการ playback ผ่าน driver แทรกอยู่ในจุดใดเลย

HotXLS parse ไบต์ EMF และ WMF ของเวิร์กชีตที่ไม่น่าเชื่อถือด้วย XLSDecodeVectorScene เป็นรายการคำสั่ง TXLSVectorScene แทนการ playback ผ่าน GDI metafile
HotXLS parse metafile เองและคืนคำสั่ง primitive ให้ Canvas และ SVG เล่นซ้ำ ส่วนเส้นทางธรรมเนียมไปรันสตรีมไบต์บนชั้นกราฟิกของระบบ

การแลกเปลี่ยนนี้คือความครอบคลุมเพื่อการกักกัน whitelist คำสั่งที่หมุนรอบรูปสี่เหลี่ยมจะไม่แจกแจง metafile ทุกชิ้นที่นักออกแบบสร้างได้ จึงต้องให้ scene รายงานว่ามี drawing record กี่ตัวที่มันแทนไม่ได้ แล้วผู้เรียกเป็นคนตัดสินว่าจะจัดการอย่างไร สำหรับ process ฝั่ง server ที่ render เอกสารที่ตัวเองไม่ได้สร้าง การแลกแบบนี้คือทิศทางที่ถูก

ทำไมการ playback metafile จึงเหมาะกับอินพุตที่ไม่น่าเชื่อถือน้อยมาก

เพราะรูปแบบนี้ไม่ใช่ภาพ มันคือโปรแกรม สตรีม record ของ EMF จัดการสแตกสถานะของ device context, จัดสรรและเลือกออบเจกต์จากตาราง handle และแบก escape record ที่ payload ของมันถูกส่งตรงไปถึง device driver การเล่นซ้ำจึงใช้เส้นทางในชั้นกราฟิกของ platform ที่ถูกเขียนโดยสมมติว่า metafile มาจากแอปพลิเคชันที่ร่วมมือกันบนเครื่องเดียวกัน เมื่ออินพุตเป็นไฟล์แนบ spreadsheet สมมติฐานนั้นหมดไป และความระมัดระวังมากแค่ไหนในไลบรารี spreadsheet ก็ไม่ช่วย เพราะไลบรารีไม่ใช่ component ที่ทำหน้าที่ parse อยู่

นี่คือการเหตุผลชุดเดียวกับที่คุมชั้นคอนเทนเนอร์ เวิร์กบุ๊กคือ archive ZIP และ HotXLS ตรวจ central directory ของมัน แทนที่จะเชื่อ offset ที่ประกาศไว้ ตามที่อธิบายไว้ในบทความการตรวจสอบ ZIP end-of-central-directory payload metafile เป็นชั้นถัดไปของปัญหาชุดเดียวกัน

ตัว decoder เช็กอะไรก่อนจะวาดสักอย่าง

การตรวจสอบเป็นเชิงโครงสร้างและเกิดขึ้นก่อน เพราะ parser ที่เริ่มวาดแล้วค่อยตรวจระหว่างทางคือ parser ที่ลงมือกับข้อมูลที่ยังไม่ได้พิสูจน์แล้ว header ต้องตรงแบบเข้มงวด ไม่ใช่ตรงแบบใกล้เคียง record ทุกตัวต้องประกาศขนาดที่อยู่ในบัฟเฟอร์ที่เหลือ และใหญ่พอสำหรับ field คงที่ของตัวเอง จำนวน record ที่ header ประกาศต้องตรงกับ record ที่มีอยู่จริง record ปิดไฟล์ต้องนั่งอยู่ตรงจุดที่สตรีมจบพอดี ไม่ใช่แค่แถว ๆ นั่น ซึ่งปิดเทคนิคขยะท้ายไฟล์ที่ซ่อน payload ที่สองไว้หลังภาพที่ดูดี

เหนือโครงสร้าง ตัว decoder เป็น fail-closed ในเชิงความหมาย escape record ถูกปฏิเสธ ไม่ใช่ถูกข้าม record ที่เปลี่ยนสถานะที่ตัว decoder ไม่ได้จำลองไว้ทำให้การ decode ล้ม แทนที่จะถูกมองข้าม เพราะการมองข้ามการเปลี่ยนสถานะหมายถึงคำสั่งวาดทุกคำสั่งถัดไปถูกรันในสถานะที่ไฟล์ไม่ได้ขอ และผลลัพธ์คือภาพที่ผิดแบบที่ไม่มีใครทำนายได้ drawing record นอกชุดคำสั่งที่รองรับเป็นอีกเรื่อง: พวกมันถูกนับแล้วข้าม เพราะรูปทรงที่หายไปเป็นช่องว่างที่มองเห็นและรายงานได้ ไม่ใช่ความเสียหายแบบเงียบ ๆ

XLSDecodeVectorScene เช็ก header, ขนาด record, จำนวนรวมและตำแหน่ง EOF ล่วงหน้า แล้วปฏิเสธ escape record และนับ draw record ที่ไม่รองรับ
การเช็กเชิงโครงสร้างรันล่วงหน้า และความหมายแบบ fail-closed ปฏิเสธ escape record ส่วน drawing record ที่ไม่รองรับถูกแค่นับแล้วข้าม

งบประมาณเป็นส่วนหนึ่งของสัญญากับรูปแบบไฟล์

รูปแบบเวกเตอร์มีรุ่นของตัวเองเรื่องระเบิดการคลายการบีบอัด record ไม่กี่กิโลไบต์ประกาศ polyline ที่มีจุดเป็นร้อยล้านได้ หรือภาพที่มิติที่ประกาศไว้คูณกันกลายเป็นเทระไบต์ ขอบเขตจึงต้องเป็นค่าคงที่แบบชัดเจน ไม่ใช่ค่าเท่าไรที่เครื่องบังเอิญรอดได้

// จาก lxVectorScene: งบของการ decode ที่ระบุไว้ชัด ไม่ใช่แฝงไว้
XL_VECTOR_MAX_RECORDS           = 1000000;
XL_VECTOR_MAX_HANDLES           = 4096;
XL_VECTOR_MAX_DC_DEPTH          = 32;
XL_VECTOR_MAX_COMMANDS          = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS      = 2000000;
XL_VECTOR_MAX_TEXT_CHARS        = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS  = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE        = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS      = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES       = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD             = 1000000000;

มีสองข้อที่สมควรได้บันทึกเพิ่ม เพดานความลึก device context ที่ 32 มีอยู่เพราะ record SaveDC และ RestoreDC ซ้อนกันได้ และสตรีมที่ไม่สมดุลสามารถ push ไปได้ไม่มีที่สิ้นสุด 32 กลมกล่อมพอสำหรับ metafile จริงและบังคับใช้แทบไม่มีต้นทุน เพดานพิกัดมีอยู่เพราะพิกัดไหลเข้าสู่ transform ค่าใกล้ขีดของช่วงจำนวนเต็มให้ผลที่ถูกแปลงเป็นได้ทั้งอนันต์และ wrap จากจุดนั้นการคำนวณ bounding box ทุกตัวปลายน้ำกลายเป็นเรื่องไร้ความหมาย การ clamp พิกัดตอน parse ให้เหตุผลที่ตามง่ายกว่าการไปป้องกันผู้ใช้เรขาคณิตทุกราย

ค่าคงที่งบ decode ใน lxVectorScene ของ HotXLS สำหรับ record, handle, ความลึก DC, คำสั่ง, จุด, ข้อความ, ขนาดภาพและการ clamp พิกัด
ทุกขีดจำกัดเป็นค่าคงที่ที่มีชื่อซึ่งถูกบังคับระหว่าง parse เพดานความลึก DC กับการ clamp พิกัดสมควรได้รับความสนใจมากที่สุด

การใช้ scene

ตัว decoder ส่งออบเจกต์ที่คุณเป็นเจ้าของคืนมา พร้อมจำนวนคำสั่ง ขนาดในเชิงปกติ และจำนวน drawing record ที่มันเลือกไม่แทน

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data ถือ payload ของภาพดิบที่หยิบมาจากเวิร์กบุ๊ก
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // ถูกปฏิเสธ: header, ขอบเขต, จำนวนรวม, ตำแหน่ง EOF หรืองบประมาณ
    LogReject('metafile rejected: ' + Error);
    Exit;
  end;
  try
    if Scene.SkippedDrawRecords > 0 then
      LogWarning(Format('%d drawing records outside the safe subset',
        [Scene.SkippedDrawRecords]));
    for I := 0 to Scene.Count - 1 do
      case Scene.Commands[I].Kind of
        xlsvcRectangle: DrawRect(Scene.Commands[I]);
        xlsvcEllipse:   DrawEllipse(Scene.Commands[I]);
        xlsvcPolyline,
        xlsvcPolygon,
        xlsvcBezier:    DrawPath(Scene.Commands[I]);
        xlsvcText:      DrawText(Scene.Commands[I]);
        xlsvcImage:     DrawImage(Scene.Commands[I]);
      end;
  finally
    Scene.Free;
  end;
end;

record คำสั่งแบกทุกอย่างที่ backend ต้องใช้ และไม่มีอะไรที่ต้องการ device: การมีอยู่ของปากกา สี ความกว้างและสไตล์ การมีอยู่ของแปรงกับสี เรขาคณิต และสำหรับข้อความคือสตริง ชื่อ font, ขนาด, สไตล์และการจัดวาง นั่นคือสิ่งที่ทำให้ scene เดียวกันใช้ได้ทั้งกับ renderer canvas บนหน้าจอและตัวเขียน SVG และเป็นเหตุผลที่เส้นทางเวกเตอร์ไม่แตกต่างระหว่าง preview กับ export การ render เนื้อหาเวิร์กชีตบนหน้าจอโดยทั่วไปอธิบายไว้ในบทความการ render กริด VCL แบบกำหนดเอง

การปฏิเสธภาพไม่ได้ทำร้ายเวิร์กบุ๊ก

คุณสมบัติสำคัญของการออกแบบนี้คือการ decode ที่ถูกปฏิเสธกระทบเฉพาะการ render payload ต้นฉบับยังอยู่ในโมเดล เวิร์กบุ๊กที่ถูกเปิดแล้วบันทึกใหม่จึงพาภาพ metafile ของมันออกไปครบทุกไบต์ ไม่ว่าตัว decoder ที่ปลอดภัยจะวาดมันได้หรือไม่ เส้นทาง raster แบบมีขอบเขตที่มีอยู่เดิมยังพร้อมใช้เป็น fallback อีกด้วย พูดง่าย ๆ คือ parser ที่เข้มงวด gate สิ่งที่ถูกรัน ไม่ใช่สิ่งที่ถูกเก็บรักษา ซึ่งเป็นความต่างที่ทำให้การเปลี่ยนแปลงเพื่อความปลอดภัยส่งมอบได้โดยไม่กลายเป็นการเปลี่ยนแปลงที่ทำข้อมูลหาย

การจัดการวัตถุวาดภาพโดยรวม รวมถึงส่วนของโมเดลออบเจกต์ที่รอดผ่านการวนกลับโดยไม่ถูกแตะ อธิบายไว้ในบทความ chart, ภาพ และวัตถุวาดภาพ

สถานะที่เรื่องนี้ทิ้งไว้กับการ deploy ฝั่ง server

ถ้าคุณ render เวิร์กบุ๊กที่ผู้ใช้อัปโหลดในบริการ จุดยืนเชิงปฏิบัติตอนนี้ป้องกันตัวได้: ภาพ metafile ถูก parse โดยโค้ดที่คุณ audit ได้ ถูกพันธนาการด้วยค่าคงที่ที่คุณอ่านได้ และไม่เคยถูกส่งให้ graphic driver ข้อสังเกตที่ตรงไปตรงมาคือเรื่องความครอบคลุม metafile ซับซ้อนจากเครื่องมือวาดภาพจะโดนตัวนับ record ที่ถูกข้าม คำตอบคือยกตัวนับนั้นขึ้นมาให้เห็น ไม่ใช่ขยาย whitelist อย่างเงียบ ๆ ภาพที่ render ได้บางส่วนและบอกเช่นนั้นคือบทสนทนาฝ่ายสนับสนุน ภาพที่ render ผิดและไม่พูดอะไรคือบั๊กรายงานจากลูกค้า

HotXLS จัดการ XLS, XLSX, ODS และ CSV แบบ native ใน Delphi และ C++Builder โดยไม่ต้องติดตั้ง Excel และปรัชญา parse แบบมีขอบเขตเดียวกันวิ่งทั่วชั้นคอนเทนเนอร์ สูตร และชั้นวัตถุวาดภาพของมัน รายละเอียดรูปแบบไฟล์และความปลอดภัยระบุไว้บนหน้าผลิตภัณฑ์HotXLS Delphi spreadsheet component