Artykuł techniczny

Bezpieczne odtwarzanie niezaufanych EMF i WMF w HotXLS

Skoroszyt Excel może nieść obrazki EMF i WMF, a konwencjonalny sposób ich narysowania to oddanie strumienia bajtów systemowemu odtwarzaczowi metafilów. To decyzja warta spojrzenia wprost: metafil to serializowany strumień poleceń dla API graficznego, a odtworzenie go znaczy pozwolenie plikowi, który przyszedł pocztą, na prowadzenie sterownika graficznego. HotXLS wybiera drugą drogę. XLSDecodeVectorScene parsuje metafil samodzielnie, waliduje nagłówek, rozmiar każdego rekordu, zadeklarowaną liczbę rekordów i dokładne umiejscowienie rekordu końca pliku, kategorycznie odrzuca rekordy escape i zwraca TXLSVectorScene prymitywnych poleceń rysowania, które backendy Canvas i SVG odtwarzają własnym kodem. Żadne odtwarzanie przez sterownik nie bierze udziału w żadnym momencie

HotXLS parsuje niezaufane bajty EMF i WMF arkusza przez XLSDecodeVectorScene do listy poleceń TXLSVectorScene zamiast odtwarzania metafila przez GDI
HotXLS parsuje metafil samodzielnie i zwraca prymitywne polecenia do odtworzenia przez Canvas i SVG; konwencjonalna droga wykonuje strumień bajtów na stosie graficznym

Wymiana to pokrycie za zawieranie. Biała lista poleceń zorientowana na prostokąty nie odtworzy każdego metafila, jaki potrafi stworzyć projektant, więc scena zgłasza, ile rekordów rysowania nie potrafiła przedstawić, a wywołujący decyduje, co z tym zrobić. Dla procesu serwerowego renderującego dokumenty, których nie stworzył, ta wymiana jest właściwie ustawiona

Czemu odtwarzanie metafila słabo pasuje do niezaufanego wejścia?

Bo format nie jest obrazkiem, jest programem. Strumień rekordów EMF manipuluje stosem stanu kontekstu urządzenia, alokuje i wybiera obiekty z tablicy uchwytów i może nieść rekordy escape, których ładunek jest przekazywany do sterownika urządzenia. Odtwarzanie go ćwiczy ścieżki w platformowym stosie graficznym napisane przy założeniu, że metafil pochodzi od współpracującej aplikacji na tej samej maszynie. Gdy wejściem jest załącznik arkusza kalkulacyjnego, to założenie znika, i żadna staranność wewnątrz biblioteki arkusza nie pomoże, bo to nie biblioteka jest komponentem robiącym parsowanie

To samo rozumowanie rządzi warstwą kontenera. Skoroszyt to archiwum ZIP, a HotXLS waliduje jego katalog centralny, zamiast ufać zadeklarowanym offsetom, co opisuje artykuł o walidacji końcowego katalogu ZIP. Ładunki metafilów to następna warstwa tego samego problemu

Co dekoder sprawdza, zanim cokolwiek narysuje

Walidacja jest strukturalna i dzieje się z góry, bo parser, który zaczyna rysować i waliduje w biegu, już zadziałał na danych, których nie zweryfikował. Nagłówek musi się zgadzać ściśle, a nie wiarygodnie. Każdy rekord musi zadeklarować rozmiar mieszczący się w pozostałym buforze i wystarczająco duży na własne pola stałe. Liczba rekordów zadeklarowana w nagłówku musi się zgadzać z rekordami faktycznie obecnymi. Rekord końca pliku musi siedzieć dokładnie tam, gdzie strumień się kończy, nie jedynie gdzieś blisko, co zamyka trik śmieci końcowych ukrywających drugi ładunek za poprawnym obrazkiem

Poza strukturą dekoder jest fail-closed na semantyce. Rekordy escape są odrzucane, nie pomijane. Rekord zmieniający stan, którego dekoder nie modeluje, powoduje zawodzenie dekodowania zamiast być zignorowanym, bo ignorowanie zmiany stanu znaczy, że każde kolejne polecenie rysowania jest wykonywane w stanie, o który plik nie prosił, a wynikiem jest obrazek zły w sposób, którego nikt nie przewidzi. Rekordy rysowania poza wspieranym zestawem poleceń to inna sprawa: te są liczone i pomijane, bo brakujący kształt to widoczna, zaraportowalna luka, a nie ciche uszkodzenie

XLSDecodeVectorScene sprawdza z góry nagłówek, rozmiary rekordów, sumy i umiejscowienie EOF, potem odmawia rekordów escape i liczy niewspierane rekordy rysowania
Kontrole strukturalne biegną z góry, a semantyka fail-closed odmawia rekordów escape, podczas gdy niewspierane rekordy rysowania są tylko liczone i pomijane

Budżety są częścią kontraktu formatu

Formaty wektorowe mają własną wersję bomby dekompresyjnej. Kilka kilobajtów rekordów może zadeklarować polilinie ze sto milionami punktów albo obrazek, którego zadeklarowane wymiary mnożą się do terabajtów. Granice muszą być więc jawnymi stałymi, a nie tym, co maszyna akurat przeżyje

// Z lxVectorScene: budżet dekodowania, wypowiedziany, a nie dorozumiany
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;

Dwie z nich zasługują na uwagę. Sufit głębokości kontekstu urządzenia równy 32 istnieje, bo rekordy SaveDC i RestoreDC się zagnieżdżają, a niezbalansowany strumień może pchać w nieskończoność; 32 jest hojne dla prawdziwych metafilów i tanie w egzekwowaniu. Sufit współrzędnych istnieje, bo współrzędne zasilają transformację, a wartość blisko granic zakresu liczb całkowitych daje przekształcony wynik nieskończony albo zawinięty, po czym każde obliczenie ramki ograniczającej dalej w dół to nonsens. Przycinanie współrzędnych w chwili parsowania jest znacznie łatwiejsze do przemyślenia niż bronienie każdego konsumenta geometrii

Stałe budżetu dekodowania w HotXLS lxVectorScene dla rekordów, uchwytów, głębokości DC, poleceń, punktów, tekstu, rozmiaru obrazka i przycinania współrzędnych
Każdy limit to nazwana stała egzekwowana podczas parsowania; sufit głębokości DC i przycinanie współrzędnych zasługują na najwięcej uwagi

Używanie sceny

Dekoder oddaje obiekt, którego właścicielem jesteś, liczbę poleceń, rozmiar nominalny i liczbę rekordów rysowania, których nie zdecydował się przedstawić

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data trzyma surowy ładunek obrazka pobrany ze skoroszytu
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // Odmowa: nagłówek, granice, sumy, umiejscowienie EOF albo budżet
    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;

Rekord polecenia niesie wszystko, czego backend potrzebuje, i nic, co wymaga urządzenia: obecność, kolor, szerokość i styl pióra; obecność i kolor pędzla; geometrię; a dla tekstu łańcuch, nazwę fontu, rozmiar, style i wyrównanie. To sprawia, że ta sama scena jest używalna i przez renderer canvasu na ekranie, i przez pisarza SVG, i dlatego ścieżka wektorowa nie rozjeżdża się między podglądem a eksportem. Renderowanie ekranowe zawartości arkusza ogólnie jest omówione w artykule o renderowaniu własnej siatki VCL

Odrzucenie obrazka nie uszkadza skoroszytu

Ważną własnością tego projektu jest to, że odmówione dekodowanie wpływa tylko na renderowanie. Oryginalny ładunek zostaje w modelu, więc skoroszyt otwarty i zapisany ponownie wynosi swoje obrazki metafilów bajt w bajt, niezależnie od tego, czy bezpieczny dekoder umiał je narysować. Istniejąca ograniczona ścieżka rastrowa też pozostaje dostępna jako fallback. Inaczej mówiąc, surowy parser bramkuje to, co jest wykonywane, a nie to, co jest zachowywane, i to rozróżnienie pozwala zmianie motywowanej bezpieczeństwem wyjść, nie zmieniając się w zmianę gubiącą dane

Obsługa obiektów rysunkowych ogólnie, łącznie z częściami modelu obiektów, które przechodzą podróże w obie strony nietknięte, jest omówiona w artykule o wykresach, obrazkach i rysunkach

Gdzie to zostawia wdrożenie serwerowe

Jeśli renderujesz wgrywane przez użytkowników skoroszyty w usłudze, praktyczna pozycja jest teraz obronywalna: obrazki metafilów są parsowane przez kod, który możesz audytować, ograniczone stałymi, które możesz przeczytać, i nigdy oddawane sterownikowi graficznemu. Uczciwe zastrzeżenie to pokrycie. Złożone metafile wyprodukowane przez narzędzia do rysowania trafią w licznik pominiętych rekordów, a odpowiedzią jest wyniesienie licznika na widok, a nie po cichu poszerzanie białej listy. Obrazek renderowany częściowo i mówiący o tym to rozmowa z pomocą techniczną; obrazek renderowany źle i nic nie mówiący to zgłoszenie błędu od klienta

HotXLS obsługuje XLS, XLSX, ODS i CSV natywnie w Delphi i C++Builder bez zainstalowanego Excela, a ta sama filozofia ograniczonego parsowania przechodzi przez jego warstwy kontenera, formuł i rysunków. Szczegóły formatu i bezpieczeństwa są wypisane na stronie produktu HotXLS Delphi spreadsheet component