技術文章

在 HotXLS 中安全重播不受信任的 EMF 與 WMF

Excel 活頁簿可以攜帶 EMF 與 WMF 圖片,而傳統的畫法是把位元組串流交給作業系統的中繼檔播放器。這個決策值得直視:中繼檔是給圖形 API 用的序列化命令串流,重播一份中繼檔,等於讓一份透過電子郵件抵達的檔案驅動圖形驅動程式。HotXLS 走另一條路。XLSDecodeVectorScene 自己解析中繼檔,驗證標頭、每個記錄大小、宣告的記錄總數與檔案結尾記錄的精確位置,斷然拒絕 escape 記錄,回傳一個由基本繪圖命令組成的 TXLSVectorScene,Canvas 與 SVG 後端用自己的程式碼重播它。任何環節都不涉及驅動程式播放

HotXLS 以 XLSDecodeVectorScene 把不受信任的 EMF 與 WMF 工作表位元組解析成 TXLSVectorScene 命令清單,而不是 GDI 中繼檔播放
HotXLS 自己解析中繼檔,回傳供 Canvas 與 SVG 重播的基本命令;傳統路線則在圖形堆疊上執行位元組串流

這是一筆以涵蓋換圍堵的交易。面向矩形的命令白名單無法重現設計師能造出的每份中繼檔,所以場景會回報它無法表現的繪圖記錄數量,由呼叫端決定怎麼處理。對一個渲染不是自己產出文件的伺服器行程來說,這筆交易的方向是對的

為什麼中繼檔播放不適合不受信任的輸入?

因為這個格式不是圖片,是程式。EMF 記錄串流操縱裝置內容狀態堆疊,從 handle 表配置並選取物件,還能攜帶其負載被直通裝置驅動程式的 escape 記錄。重播它,會演練平台圖形堆疊中那些以「中繼檔來自同一台機器上合作的應用程式」為前提撰寫的路徑。當輸入是試算表附件時,這個前提不存在,而試算表程式庫內部再小心也沒用,因為做解析的元件不是它

這與治理容器層的推理相同。活頁簿是一個 ZIP 壓縮檔,HotXLS 驗證它的中央目錄而不是信任宣告的偏移,如ZIP 中央目錄結尾驗證文章所述。中繼檔負載是同一問題的下一層

解碼器在畫任何東西之前檢查什麼

驗證是結構性的,而且事先完成,因為一個邊畫邊驗證的剖析器,已經對未驗證的資料採取了行動。標頭必須嚴格相符而不是貌似相符。每個記錄必須宣告一個放得進剩餘緩衝區、又大到足以容納自身固定欄位的大小。標頭宣告的記錄數必須與實際在場的記錄相符。檔案結尾記錄必須正好落在串流結束處,而不是只在附近,這封死了把第二份負載藏在有效圖片後面的尾部垃圾技倆

在結構之外,解碼器在語意上 fail-closed。escape 記錄被拒絕,不是被跳過。解碼器未建模的改變狀態記錄會讓解碼失敗,而不是被忽略,因為忽略一個狀態改變,意味著後續每個繪圖命令都在一個檔案沒有要求的狀態下執行,結果是一幅以無人能預測的方式出錯的圖。支援命令集之外的繪圖記錄則是另一回事:那些被計數並跳過,因為缺一個形狀是可見、可回報的缺口,而不是無聲的損壞

XLSDecodeVectorScene 事先檢查標頭、記錄大小、總數與 EOF 位置,然後拒絕 escape 記錄並計數不支援的繪圖記錄
結構檢查事先執行,fail-closed 語意拒絕 escape 記錄,而不支援的繪圖記錄只被計數並跳過

預算是格式契約的一部分

向量格式有自己版本的解壓縮炸彈。幾 KB 的記錄可以宣告數億個點的折線,或宣告尺寸相乘後以 TB 計的影像。因此界限必須是明確的常數,而不是這台機器碰巧撐得過的值

// 來自 lxVectorScene:解碼預算,明文陳述而非隱含
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;

其中兩個值得註記。裝置內容深度上限 32 的存在,是因為 SaveDCRestoreDC 記錄會巢狀,不平衡的串流可以無限推入;對真實中繼檔來說 32 已經寬裕,執行起來也便宜。座標上限的存在,是因為座標會餵進轉換,接近整數範圍極限的值會產出無限大或回繞的轉換結果,之後下游每個包圍盒計算都是胡話。在剖析時鉗制座標,比在幾何的每個消費端設防容易推理得多

HotXLS lxVectorScene 的解碼預算常數:記錄、handle、DC 深度、命令、點數、文字、影像尺寸與座標鉗制
每個上限都是在剖析期間執行的具名常數;DC 深度上限與座標鉗制最值得關注

使用場景

解碼器交回一個您擁有的物件、一個命令數、一個名義尺寸,以及它選擇不表現的繪圖記錄數

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data 持有從活頁簿取出的原始圖片負載
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // 被拒:標頭、界限、總數、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;

命令記錄攜帶後端需要的一切,不帶任何需要裝置的東西:畫筆的存在、色彩、寬度與樣式;筆刷的存在與色彩;幾何;文字則有字串、字型名稱、大小、樣式與對齊。這正是同一個場景能同時供螢幕畫布渲染器與 SVG 寫出器使用的原因,也是向量路徑在預覽與匯出之間不分岔的原因。工作表內容的螢幕渲染一般性描述於自訂 VCL 網格渲染文章

拒絕一張圖片不會傷害活頁簿

這個設計的一個重要性質是:被拒的解碼只影響渲染。原始負載留在模型裡,所以一份被打開再另存的活頁簿,無論安全解碼器畫不畫得出來,都逐位元組帶出它的中繼檔圖片。既有的有界限點陣路徑也仍可作為回退。換句話說,嚴格剖析器把關的是什麼被執行,而不是什麼被保留,這個區別讓一個出於安全動機的改動出貨時,不會變成造成資料損失的改動

繪圖物件處理的一般情況,包括物件模型中在來回轉換後原樣保留的部分,在圖表、影像與繪圖文章中涵蓋

這讓伺服器部署處於什麼位置

如果您在服務裡渲染使用者上傳的活頁簿,現在的實務立場站得住腳:中繼檔圖片由您可以稽核的程式碼解析,由您讀得懂的常數設界,永遠不交給圖形驅動程式。誠實的警告是涵蓋。繪圖工具產出的複雜中繼檔會撞上跳過記錄計數器,對此的正確回應是把計數器浮上來,而不是悄悄擴大白名單。一幅部分渲染且明說的圖片,是一場支援對話;一幅渲染錯誤卻不吭聲的圖片,是一張來自客戶的錯誤報告

HotXLS 在 Delphi 與 C++Builder 中原生處理 XLS、XLSX、ODS 與 CSV,無需安裝 Excel,同樣的有界限解析哲學貫穿它的容器、公式與繪圖層。格式與安全細節列於 HotXLS Delphi spreadsheet component 產品頁