💡 Plně funkční příklad je k dispozici na GitHubu:
load-untrusted-documents-safely-python
Starý způsob byl bolestivý
Napsali jste tři řádky pro vykreslení miniatury nahraného dokumentu. Vypadaly takto a vypadaly v pořádku:
with signature.Signature(upload_path) as sign:
save_page_preview(sign, thumbnail_path)
Co tyto řádky dělaly před verzí GroupDocs.Signature 26.9, bylo načítání každé adresy, na kterou dokument odkazoval. Wordový soubor může obsahovat obrázek, který v sobě neobsahuje – soubor ukládá URL a cokoliv, co jej otevře, tuto URL stáhne. Na desktopu je to funkce. Na serveru, který přijímá nahrané soubory, to znamená, že osoba, která vám soubor poslala, rozhoduje, na jaké adresy vaše infrastruktura požádá.
Útok má název server‑side request forgery a existují tři podoby, které stojí za pojmenování. Interní adresa nedostupná z internetu je z vašeho serveru dosažitelná, takže vytvořený dokument může přimět vaši službu načíst http://169.254.169.254/ nebo administrativní endpoint na localhostu. UNC cesta může přimět Windows hosta autentizovat se ven, čímž předá přihlašovací údaje útočníkovi. A odkaz na hosta, který nikdy neodpoví, drží načítací vlákno, dokud nevyprší časový limit, což je levný způsob, jak vyčerpat pracovní pool pomocí dokumentů, které vypadají neškodně.
Nic z toho není chyba v knihovně dokumentů. Následování odkazu je to, co formát požaduje. Nepříjemná část byla, že vyhovění bylo výchozí, v kódu to nikdo v revizi neoznačil.
Existuje lepší způsob
Bezpečné načítání dokumentů je chování GroupDocs.Signature pro Python, které odmítá provádět tyto požadavky. Od verze 26.9 je výchozí hodnota LoadOptions.skip_external_resources nastavena na True, takže stejné tři řádky nyní nic nevyžádají a vykreslí zástupný prvek tam, kde by byl odkazovaný obrázek.
Změna je výchozí nastavení, nikoli nová funkce – vlastnost už existovala. Co verze 26.9 změnila, je směr, kterým se chová, když váš kód nic neřekne, což je jediné nastavení, které většina služeb vůbec používá.
Nový způsob: tři režimy načítání
Krok 1 – Použijte výchozí nastavení pro vše, co není důvěryhodné
Žádné LoadOptions vůbec:
with signature.Signature(source_path) as sign:
return save_page_preview(sign, preview_path)
Není požádáno nic. Náhled je menší, než by byl jinak, a tato velikostní rozdíl je nejpohodlnější důkaz, že žádný požadavek neopustil stroj.
Krok 2 – Přidejte do whitelistu hostitele, kterého skutečně vlastníte
Mnoho dokumentů odkazuje na něco legitimního: CDN společnosti, interní server s obrázky, úložiště šablon. Povolit to a nic jiného:
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)
Pravidlo shody si zaslouží pozornost. Jedná se o test podřetězce bez rozlišení velikosti písmen vůči adrese zdroje, což dělá krátký fragment nebezpečným: github odpovídá github.attacker.example/payload.png stejně snadno jako hostiteli, který jste zamýšleli. Použijte schéma, hostitele a cestu – tento příklad přidává do whitelistu raw.githubusercontent.com/groupdocs-signature/.
Krok 3 – Povolit vše, úmyslně
Chování před verzí 26.9, stále k dispozici:
load_options = LoadOptions()
load_options.skip_external_resources = False
Rozumné pro dokumenty, které vaše vlastní aplikace vytvořila. Jedna past: zastaralá vlastnost load_external_resources má opačnou polaritu, takže skip_external_resources = False nahrazuje load_external_resources = True. Zkopírujete-li hodnotu ze staré vlastnosti, obrátíte svou bezpečnostní politiku bez jakékoli chyby, která by vás na to upozornila.
Vedle‑vedle: před a po
Stejný dokument, stejná cesta kódu, tři zásady načítání. Jedná se o velikosti souborů uložených ve složce Result/ ukázky, takže je lze ověřit místo aby se na ně spoléhalo:
| Režim načítání | Velikost náhledu | Odchozí požadavky |
|---|---|---|
| výchozí (26.9 a novější) | 16 435 bajtů | žádné |
| hostitel ve whitelistu | 51 738 bajtů | jeden, na povolenou adresu |
| všechny zdroje (výchozí před 26.9) | 51 738 bajtů | jeden na každý odkazovaný zdroj |
Odkazovaný obrázek tvoří 35 303 bajtů tohoto rozdílu. Nastavení jsem nedůvěřoval, dokud jsem neviděl tyto dvě čísla vedle sebe, a doporučil bych to samé: přečtení vlastnosti vám řekne, co jste nakonfigurovali, ne co proces udělal.
Co se považuje za externí zdroj?
Úžeji, než lidé očekávají, což je důvod, proč je aktualizace obvykle neškodná. Odkazované obrázky místo vložených, pole INCLUDEPICTURE, odkazované obrázky v prezentacích a tabulkách a obrázky a stylové listy, na které odkazuje SVG. Vložený obsah zůstává nedotčený, protože už je součástí souboru a není potřeba žádný požadavek k jeho vykreslení.
Toto rozlišení je celá bezpečnostní hranice. Dokument může přimět váš server komunikovat ven jen tehdy, když místo bajtů uloží adresu, takže otázka pro jakýkoli korpus je prostě kolik jeho souborů odkazuje místo aby vkládalo. Pokud žádný neodkazuje, nové výchozí nastavení vás nic nestojí a můžete aktualizovat bez dalšího čtení.
Praktický příklad: nahrání, které se podepisuje
Případ, pro který existuje výchozí změna. Dokument přijde zvenčí a potřebujete na něj přidat 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)
Žádný externí zdroj není požádán během načítání, podepisování ani ukládání dokumentu. Podepsaný výstup si zachová svůj odkaz, takže uživatel, který jej později otevře ve Wordu, stále vidí obrázek načtený na svém vlastním počítači. Přeskakování je server‑side politika, ne úprava dokumentu – což je přesně to, co ho činí bezpečným pro soubory, které zpracováváte jménem někoho jiného.
Co se ještě mění při aktualizaci?
Pro většinu služeb se nic neviditelného nemění, což stojí za jasné vyjádření, protože bezpečnostní výchozí nastavení, které by změnilo chování všude, by neprošlo revizí aktualizace. Podepisování, ověřování a vyhledávání zůstávají nedotčeny. Výjimkou je náhled, který dříve zobrazoval odkazovaný obrázek a nyní zobrazuje zástupný prvek – změna plní svůj úkol. Přidejte hostitele do whitelistu, pokud je váš, jinak jej odmítněte.
Zvlášť stojí za zmínku: SVG. SVG může odkazovat na obrázky a stylové listy pomocí URL, tyto odkazy jsou externími zdroji podle stejného pravidla a SVG je jak běžný formát pro nahrávání, tak běžný vektor SSRF. Služba, která přijímá SVG avatary a vykresluje je na serveru, je přesně typ systému, který tato změna chrání.
Jeden detail v Pythonu: jak se náhled zapisuje
PreviewOptions přijímá dvě tovární funkce proudu místo cesty a obyčejné Pythonové volatelné objekty jsou vše, co potřebuje:
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 funkce vytvoří proud pro každou stránku, druhá jej uvolní. Ukázkový dokument má jedinou stránku, takže je zapsán jeden soubor; u vícestránkového vstupu zahrňte číslo stránky do názvu nebo každá stránka přepíše předchozí.
Závěr
Výchozí nastavení se otočilo tak, že riskantní chování vyžaduje explicitní rozhodnutí a bezpečné chování nevyžaduje nic. Používejte výchozí nastavení pro nedůvěryhodný vstup, úzce omezte whitelist na vlastní hostitele a pamatujte, že podepisování nikdy nepotřebovalo síť.
Pokud chcete silnější kontrolu než velikost souboru, nasměrujte testovací dokument na hostitele, který ovládáte, a sledujte jeho přístupový log během běhu náhledu. Velikost vám řekne, zda data dorazila; přístupový log vám řekne, zda byl vůbec proveden požadavek, a ty se liší právě v tom případě, který je podstatný – hostitel ve whitelistu, který je nedostupný, vypadá z výstupu stejně jako blokovaný.
Spuštění ukázky proti jednomu z vašich vlastních dokumentů zabere minutu a řekne vám ve třech velikostech souborů přesně, co vaše služba stahovala jménem toho, kdo vám soubor poslal.