💡 Pełny działający przykład dostępny na GitHubie:
load-untrusted-documents-safely-python
Stara metoda była bolesna
Napisałeś trzy linijki, aby wyrenderować miniaturę przesłanego dokumentu. Wyglądały tak i wydawały się w porządku:
with signature.Signature(upload_path) as sign:
save_page_preview(sign, thumbnail_path)
Co te linijki robiły, przed wersją GroupDocs.Signature 26.9, to pobierały każdy adres, na który wskazywał dokument. Plik Word może zawierać obraz, którego nie posiada – plik przechowuje URL, a każde otwarcie go pobiera ten URL. Na komputerze stacjonarnym jest to funkcja. Na serwerze przyjmującym pliki oznacza to, że osoba, która wysłała Ci plik, decyduje, które adresy Twoja infrastruktura ma żądać.
Atak nosi nazwę server‑side request forgery i ma trzy warianty, które warto wymienić. Adres wewnętrzny, niedostępny z Internetu, jest dostępny z Twojego serwera, więc spreparowany dokument może zmusić Twoją usługę do pobrania http://169.254.169.254/ lub punktu końcowego administratora na localhost. Ścieżka UNC może spowodować, że host Windows uwierzytelni się na zewnątrz, przekazując poświadczenia do serwera kontrolowanego przez atakującego. A link do hosta, który po prostu nigdy nie odpowiada, blokuje wątek ładowania, aż nastąpi timeout – tani sposób na wyczerpanie puli pracowników przy dokumentach, które wyglądają na nieszkodliwe.
W niczym nie ma błędu w bibliotece dokumentów. Podążanie za linkiem to zachowanie określone przez format. Kłopotliwe było to, że domyślnie było to włączone, a w kodzie nikt tego nie zaznaczył w przeglądzie.
Jest lepszy sposób
Bezpieczne ładowanie dokumentów to zachowanie GroupDocs.Signature dla Pythona, które odmawia wykonywania tych żądań. Od wersji 26.9, LoadOptions.skip_external_resources domyślnie ma wartość True, więc te same trzy linijki nie pobierają nic i renderują placeholder zamiast powiązanego obrazu.
Zmiana jest domyślną wartością, a nie nową funkcją – właściwość istniała już wcześniej. To, co 26.9 zmieniło, to kierunek, w którym wskazuje, gdy Twój kod nic nie określa, a to jedyne ustawienie, którego używa większość usług.
Nowy sposób: trzy tryby ładowania
Krok 1 – Zachowaj domyślne ustawienie dla wszystkiego, co nie jest zaufane
Brak w ogóle LoadOptions:
with signature.Signature(source_path) as sign:
return save_page_preview(sign, preview_path)
Nic nie jest żądane. Podgląd jest mniejszy niż w przeciwnym wypadku, a ta różnica w rozmiarze jest najwygodniejszym dowodem, że żadne żądanie nie opuściło maszyny.
Krok 2 – Dodaj do białej listy host, który naprawdę posiadasz
Wiele dokumentów odwołuje się do legalnych zasobów: CDN firmy, wewnętrzny serwer obrazów, sklep szablonów. Zezwól na to i nic więcej:
load_options = LoadOptions()
load_options.whitelisted_resources = [trusted_address]
with signature.Signature(source_path, load_options) as sign:
return save_page_preview(sign, preview_path)
Reguła dopasowania wymaga uwagi. Jest to test podciągu (case‑insensitive) względem adresu zasobu, co sprawia, że krótki fragment jest niebezpieczny: github pasuje zarówno do github.attacker.example/payload.png, jak i do zamierzonego hosta. Użyj schematu, hosta i ścieżki – w tym przykładzie do białej listy dodano raw.githubusercontent.com/groupdocs-signature/.
Krok 3 – Zezwól na wszystko, świadomie
Zachowanie sprzed wersji 26.9, nadal dostępne:
load_options = LoadOptions()
load_options.skip_external_resources = False
Rozsądne dla dokumentów generowanych przez własną aplikację. Jedna pułapka: przestarzała właściwość load_external_resources ma odwrotną polaryzację, więc skip_external_resources = False zastępuje load_external_resources = True. Skopiowanie wartości ze starej właściwości odwróci Twoją postawę bezpieczeństwa bez żadnego komunikatu o błędzie.
Obok siebie: przed i po
Ten sam dokument, ta sama ścieżka kodu, trzy polityki ładowania. Są to rozmiary plików zapisanych w katalogu Result/ przykładu, więc można je zweryfikować, a nie przyjmować na wiarę:
| Tryb ładowania | Rozmiar podglądu | Żądania wychodzące |
|---|---|---|
| domyślny (26.9 i później) | 16 435 bajtów | brak |
| host na białej liście | 51 738 bajtów | jedno, do dozwolonego adresu |
| wszystkie zasoby (domyślne przed 26.9) | 51 738 bajtów | po jednym dla każdego powiązanego zasobu |
Powiązany obraz ma 35 303 bajty tej różnicy. Nie ufałem temu ustawieniu, dopóki te dwie liczby nie znalazły się obok siebie, i sugeruję to samo: odczytanie właściwości zwraca to, co skonfigurowałeś, a nie to, co proces wykonał.
Co liczy się jako zasób zewnętrzny?
Węższe niż się powszechnie myśli, dlatego aktualizacja zazwyczaj nie wywołuje problemów. Powiązane obrazy zamiast osadzonych, pola INCLUDEPICTURE, powiązane obrazy w prezentacjach i arkuszach kalkulacyjnych oraz obrazy i arkusze stylów, do których odwołuje się SVG. Zawartość osadzona pozostaje nietknięta, ponieważ już znajduje się w pliku i nie wymaga żądania, aby ją wyrenderować.
To rozróżnienie stanowi całą granicę bezpieczeństwa. Dokument może zmusić Twój serwer do wyjścia na zewnątrz tylko wtedy, gdy przechowuje adres zamiast bajtów, więc pytanie dla dowolnego korpusu brzmi po prostu: ile jego plików odwołuje się, a nie osadza? Jeśli żaden nie odwołuje, nowy domyślny nie kosztuje nic i możesz zaktualizować bez dalszej lektury.
Przykład z życia: przesłanie, które zostaje podpisane
Przypadek, dla którego istnieje zmiana domyślna. Dokument przychodzi z zewnątrz i musisz na niego nałożyć podpis:
with signature.Signature(source_path) as sign:
options = QrCodeSignOptions("Approved by GroupDocs.Signature")
options.encode_type = QrCodeTypes.QR
options.left = 400
options.top = 50
options.width = 120
options.height = 120
result = sign.sign(output_path, options)
Żadne zewnętrzne zasoby nie są żądane podczas ładowania, podpisywania ani zapisywania dokumentu. Podpisany wynik zachowuje swój link, więc użytkownik otwierający go później w Wordzie nadal widzi obraz rozwiązywany na własnym komputerze. Pomijanie jest polityką po stronie serwera, a nie edycją dokumentu – co dokładnie czyni ją bezpieczną do zastosowania w plikach obsługiwanych w imieniu kogoś innego.
Co jeszcze się zmienia po aktualizacji?
Dla większości usług nic widocznego, co warto wyraźnie zaznaczyć, ponieważ domyślne ustawienie bezpieczeństwa, które zmieniło zachowanie wszędzie, nie przetrwałoby przeglądu aktualizacji. Podpisywanie, weryfikacja i wyszukiwanie pozostają nietknięte. Jedynym wyjątkiem jest podgląd, który wcześniej wyświetlał powiązany obraz, a teraz pokazuje placeholder – zmiana spełnia swoją rolę. Dodaj host do białej listy, jeśli jest Twój; zaakceptuj go, jeśli nie.
Warto wymienić osobno: SVG. SVG może odwoływać się do obrazów i arkuszy stylów przez URL; te odwołania są zasobami zewnętrznymi według tej samej reguły, a SVG jest zarówno powszechnym formatem przesyłanym, jak i częstym wektorem SSRF. Usługa przyjmująca awatary SVG i renderująca je po stronie serwera jest dokładnie tym typem systemu, który ta zmiana chroni.
Jedna szczegółowość Pythona: jak zapisywany jest podgląd
PreviewOptions przyjmuje dwie fabryki strumieni zamiast ścieżki, a zwykłe wywołania Pythona są wszystkim, czego potrzebuje:
def create_page_stream(page_data):
return open(preview_path, "wb")
def release_page_stream(page_data, page_stream):
page_stream.close()
preview_options = PreviewOptions(create_page_stream, release_page_stream)
preview_options.preview_format = PreviewOptions.PreviewFormats.PNG
sign.generate_preview(preview_options)
Jedna tworzy strumień na stronę, druga go zwalnia. Przykładowy dokument ma jedną stronę, więc zapisany zostaje jeden plik; przy wejściu wielostronicowym uwzględnij numer strony w nazwie lub każdy kolejny nadpisuje poprzedni.
Wnioski
Domyślne ustawienie zostało odwrócone, tak aby ryzykowne zachowanie wymagało wyraźnej decyzji, a bezpieczne – niczego. Zachowaj domyślne dla nieufnych danych, wąsko białą listę tam, gdzie używasz własnych hostów, i pamiętaj, że podpisywanie nigdy nie wymagało sieci.
Jeśli potrzebujesz mocniejszej kontroli niż rozmiar pliku, skieruj dokument testowy na host, który kontrolujesz, i obserwuj jego logi dostępu podczas generowania podglądu. Rozmiar mówi, czy bajty dotarły; logi dostępu mówią, czy w ogóle wykonano żądanie, i te dwie informacje różnią się dokładnie w sytuacji, która ma znaczenie – host na białej liście, który jest niedostępny, wygląda tak samo jak zablokowany w samym wyniku.
Uruchomienie przykładu na jednym z własnych dokumentów zajmuje minutę i w trzech rozmiarach plików dokładnie pokaże, co Twoja usługa pobierała w imieniu nadawcy.