SSTI: wstrzyknięcie do szablonu po stronie serwera
Czym różni się SSTI od XSS, dlaczego sandbox silnika szablonów nie jest granicą bezpieczeństwa oraz jak testować, naprawiać i wykrywać tę klasę błędów.
- AUTOR
- Karol Rapacz / CEO Breachroad · OSCP · PNPT
- PUBLIKACJA
- 25 lipca 2026
- CZAS CZYTANIA
- 17 min czytania
- TEMAT
- Pentest i AppSec
Silnik szablonów wygląda niewinnie, bo kojarzy się z układem strony. W rzeczywistości większość popularnych silników to pełnoprawne języki wyrażeń z dostępem do obiektów aplikacji, atrybutów, metod i — pośrednio — do środowiska uruchomieniowego. Kiedy dane od użytkownika trafiają do szablonu jako kod, a nie jako wartość, powstaje SSTI: server-side template injection. Skutki sięgają od odczytu konfiguracji po wykonanie poleceń na serwerze.
Różnica wobec XSS jest zasadnicza i warto ją mieć z tyłu głowy przez cały czas. XSS wykonuje się w przeglądarce ofiary i ogranicza go model bezpieczeństwa przeglądarki. SSTI wykonuje się w procesie serwera, z jego uprawnieniami, jego zmiennymi środowiskowymi i jego dostępem sieciowym. Ten sam znak wejściowy potrafi dać w jednym miejscu drobny XSS, a w innym pełne przejęcie usługi.
Skąd to się bierze
Prawie zawsze z jednego wzorca: aplikacja skleja szablon z danych, zamiast przekazać dane do gotowego szablonu. W kodzie widać to jako budowanie treści szablonu przez konkatenację, formatowanie napisu albo wywołanie funkcji renderującej napis, którego fragment pochodzi z żądania, bazy lub pliku konfiguracyjnego edytowalnego przez użytkownika.
Miejsca, w których ten wzorzec pojawia się najczęściej, są przewidywalne. Personalizacja wiadomości e-mail, gdzie administrator może wpisać treść z polami {{imie}}. Generatory raportów i faktur z konfigurowalnym nagłówkiem. Systemy CMS pozwalające redaktorowi wstawić „blok HTML”. Powiadomienia i alerty z szablonem definiowanym w panelu. Brandowanie per klient w aplikacjach wielonajemcowych. Panele administracyjne z podglądem szablonu na żywo. Wszystkie mają wspólną cechę: ktoś świadomie chciał, żeby użytkownik dostarczał szablon, i uznał to za funkcję, a nie za powierzchnię ataku.
Drugie źródło jest mniej oczywiste: dane, które przechodzą przez szablon dwa razy. Wartość trafia do szablonu jako bezpieczna zmienna, wynik jest zapisywany, a następnie ten wynik jest renderowany ponownie w innym kontekście. Pierwsze renderowanie jest poprawne, drugie interpretuje to, co poprzednio było danymi.
Dlaczego sandbox nie wystarcza
Część silników oferuje tryb ograniczony: bez dostępu do niektórych atrybutów, z listą dozwolonych funkcji, z blokadą importów. To podnosi poprzeczkę, ale historia tych mechanizmów jest jednoznaczna — kolejne ucieczki z sandboxa pojawiają się regularnie, ponieważ silnik operuje na bogatym grafie obiektów języka. Wystarczy jedna ścieżka: atrybut prowadzący do klasy bazowej, metoda zwracająca obiekt spoza listy, mechanizm refleksji, filtr wywołujący dowolny getter.
Praktyczny wniosek nie brzmi „sandbox jest bezużyteczny”, tylko: sandbox jest utrudnieniem, nie granicą. Granicą jest proces, kontener, użytkownik systemowy i sieć. Jeżeli projekt zakłada, że użytkownik dostarcza szablony, to izolacja musi być zbudowana na poziomie, na którym ucieczka z sandboxa niczego nie zmienia.
Silniki różnią się też filozofią. Rodzina logic-less — szablony bez wyrażeń, z samymi podstawieniami i prostymi sekcjami — z założenia nie wykonuje kodu i jest jedynym rozsądnym wyborem dla treści pochodzącej od użytkownika. Silniki z pełnymi wyrażeniami są wygodne dla programisty i niebezpieczne jako format wejściowy.
Co realnie daje atakującemu
Zakres skutków zależy od silnika i od tego, co jest w zasięgu procesu, ale kolejność jest zwykle taka sama.
Najpierw odczyt kontekstu renderowania: zmienne przekazane do szablonu, obiekt konfiguracji, ustawienia aplikacji. Już na tym etapie wypływają klucze API i parametry połączeń, bo trzymanie ich w obiekcie konfiguracji jest normą.
Dalej odczyt środowiska: zmienne procesu, a wraz z nimi sekrety wstrzykiwane przy starcie kontenera. W chmurze do tego dochodzi dostęp do usługi metadanych instancji, a więc do tymczasowych poświadczeń roli — to jest ścieżka, przez którą SSTI zamienia się w kompromitację konta chmurowego.
Następnie operacje sieciowe wykonywane z serwera, czyli pełnoprawny SSRF z wnętrza sieci wewnętrznej.
Na końcu wykonanie kodu, jeżeli silnik na to pozwala. W silnikach z refleksją jest to zwykle kwestia znalezienia jednej ścieżki do konstruktora procesu.
Warto zauważyć, że pierwsze dwa etapy wystarczają, żeby incydent był poważny. Nie trzeba wykonać polecenia, żeby stracić klucze.
Metodyka testu
Testowanie SSTI zaczyna się od pytania, czy dane trafiają do szablonu jako kod, a nie od wysyłania nietypowych znaków w każde pole.
Krok pierwszy to rozpoznanie kontekstu. Odpowiedz sobie, gdzie w aplikacji użytkownik może dostarczyć treść, która później jest renderowana: pola profilu wyświetlane w wiadomościach, nazwa organizacji w nagłówku faktury, szablony powiadomień, konfiguracja eksportu, opisy widoczne dla innych. Im bardziej „administracyjna” funkcja, tym większa szansa, że ktoś użył pełnego silnika.
Krok drugi to odróżnienie SSTI od XSS i od zwykłego podstawienia. Kluczowa obserwacja jest taka, że silnik oblicza wyrażenia. Jeżeli wprowadzona konstrukcja matematyczna wraca jako wynik działania, a nie jako tekst, mamy do czynienia z ewaluacją po stronie serwera. Jeżeli wraca dosłownie, to jest podstawienie i temat kończy się na kodowaniu wyjścia.
Krok trzeci to identyfikacja silnika po zachowaniu i po komunikatach błędów. Różne silniki różnie reagują na niedomknięte konstrukcje i różnie formatują wyjątki. Ta informacja jest istotna, bo określa realny zasięg — od „można policzyć dodawanie” po „można sięgnąć do obiektów języka”.
Krok czwarty dotyczy przypadków ślepych, w których wynik renderowania nie wraca do testera: wiadomości e-mail, dokumenty generowane asynchronicznie, raporty w kolejce. Tam potwierdzeniem jest kanał poza pasmem — kontrolowana obserwacja tego, czy usługa wykonała żądanie sieciowe albo czy zmieniła czas przetwarzania. To wymaga zgody właściciela systemu i ostrożności, bo dokument z ładunkiem trafia do skrzynek realnych odbiorców.
Krok piąty to ustalenie zasięgu bez eskalacji. W raporcie z testu nie trzeba przejmować serwera, żeby wykazać krytyczność. Odczytanie jednej wartości konfiguracyjnej, do której tester nie powinien mieć dostępu, jest dowodem wystarczającym i bezpieczniejszym dla środowiska klienta niż uruchamianie poleceń.
Przez cały czas obowiązuje zasada, że aktywne próby wykonuje się wyłącznie w zakresie objętym zgodą, na kontach testowych i najlepiej na środowisku nieprodukcyjnym z odtwarzalnym stanem.
Wzorce naprawy
Dane jako kontekst, nigdy jako szablon. To jest naprawa właściwa i wystarczająca w większości przypadków. Szablon jest artefaktem programisty, znajduje się w repozytorium, przechodzi przegląd kodu i wdrożenie. Dane użytkownika są przekazywane jako zmienne. Konkatenacja szablonu z danymi to błąd tej samej rodziny co budowanie zapytania SQL przez sklejanie napisów.
Gdy użytkownik musi dostarczać szablon, wybierz silnik logic-less i traktuj listę dostępnych zmiennych jako kontrakt. Redaktor dostaje {{ nazwa_klienta }} i {{ kwota }}, a nie dostęp do obiektu zamówienia z całym grafem relacji. Filtry i funkcje pomocnicze powinny być jawną, krótką listą.
Izolacja renderowania. Proces renderujący szablony użytkownika powinien działać osobno: bez sekretów w zmiennych środowiskowych, bez poświadczeń chmurowych, bez dostępu do usługi metadanych, z zablokowanym ruchem wychodzącym, z limitem czasu i pamięci oraz z systemem plików tylko do odczytu. Wtedy nawet skuteczna ucieczka z sandboxa silnika daje atakującemu proces, w którym nie ma nic wartościowego.
Limity zasobów są częścią bezpieczeństwa, nie tylko wydajności. Szablon z zagnieżdżoną pętlą potrafi wyczerpać pamięć i procesor szybciej niż jakikolwiek inny wektor, a odmowa usługi w generatorze faktur bywa równie dotkliwa jak wyciek.
Przegląd kodu z konkretnym wzorcem. Warto mieć regułę w analizie statycznej, która wyłapuje wywołania renderujące napis zamiast pliku szablonu. To jedna z tych reguł, które mają bardzo mało fałszywych trafień i bardzo wysoką wartość.
Filtrowanie znaków przez WAF nie jest naprawą. Składnia silników jest bogata, a możliwości zapisu tego samego wyrażenia liczne. Reguła brzegowa może opóźnić skanowanie, ale nie zmienia faktu, że aplikacja wykonuje dostarczony kod.
Powiązanie z szablonami promptów
Ten sam błąd projektowy wraca w aplikacjach z modelami językowymi. Szablon promptu składany przez konkatenację treści od użytkownika ma dokładnie tę samą strukturę problemu: dane awansują do roli instrukcji. Konsekwencje są inne — nie wykonanie kodu, tylko przejęcie zachowania modelu — ale wniosek projektowy jest identyczny. Treść od użytkownika powinna trafiać do jasno oznaczonego pola danych, a nie być wklejana w miejsce, gdzie system spodziewa się poleceń.
Jeżeli aplikacja renderuje prompt silnikiem szablonów, obowiązują obie ostrożności naraz: szablon jest artefaktem programisty, a wstawiane wartości są danymi.
Detekcja w produkcji
SSTI zostawia ślady, jeżeli aplikacja loguje wyjątki silnika szablonów. Wzrost liczby błędów parsowania szablonu w krótkim czasie to sygnał, że ktoś sonduje składnię — użytkownicy nie generują takich błędów seriami.
Drugi sygnał to czas renderowania. Nagłe wydłużenie generowania dokumentu wskazuje na pętle lub kosztowne wyrażenia.
Trzeci to ruch wychodzący z procesu renderującego. Jeżeli architektura zakłada, że renderer nie łączy się z siecią, to każde takie połączenie jest alertem o wysokiej wartości i niemal zerowym poziomie szumu.
Czwarty to nietypowe wartości w polach, które trafiają do szablonów. Monitoring znaków charakterystycznych dla składni silników w polach profilowych nie jest zabezpieczeniem, ale jako sygnał detekcyjny działa dobrze.
Reakcja po potwierdzeniu podatności
Znalezienie SSTI w działającym systemie różni się od znalezienia większości błędów aplikacyjnych jednym szczegółem: trzeba założyć, że kontekst renderowania został odczytany. Nawet jeśli dowód ograniczał się do obliczenia wyrażenia arytmetycznego, sama podatność dawała dostęp do obiektu konfiguracji i zmiennych procesu.
Z tego wynika kolejność działań. Najpierw ograniczenie ekspozycji: wyłączenie funkcji przyjmującej szablony albo przełączenie jej w tryb, w którym treść jest wyłącznie podstawiana, a nie wykonywana. Potem rotacja wszystkiego, co było w zasięgu procesu: kluczy API, danych połączeń do bazy, sekretów podpisujących sesje i tokeny, poświadczeń do usług zewnętrznych. Rotacja sekretu podpisującego sesje jest szczególnie ważna, bo bez niej atakujący zachowuje możliwość wystawiania własnych tokenów długo po naprawie.
Równolegle warto przejrzeć dzienniki pod kątem ruchu wychodzącego z komponentu renderującego oraz odwołań do usługi metadanych chmury. Jeżeli takie połączenia wystąpiły, zakres incydentu obejmuje również poświadczenia roli w chmurze i trzeba je unieważnić.
Na końcu przychodzi część projektowa: ustalenie, dlaczego treść użytkownika w ogóle trafiła do silnika wykonującego wyrażenia. W większości przypadków odpowiedź brzmi „bo tak było najprościej dodać personalizację”, a właściwą naprawą jest zamiana silnika na logic-less i przeniesienie renderowania do izolowanego procesu — czyli dokładnie to, co opisano wyżej, tyle że wykonane po incydencie zamiast przed.
Checklista
Szablony pochodzą wyłącznie z repozytorium, a dane użytkownika są przekazywane jako kontekst. Nie istnieje ścieżka renderująca napis zbudowany z danych żądania. Treści dostarczane przez użytkowników są renderowane silnikiem logic-less z listą dozwolonych zmiennych. Proces renderujący jest izolowany, bez sekretów, poświadczeń chmurowych i ruchu wychodzącego. Renderowanie ma limity czasu, pamięci i głębokości. Analiza statyczna wykrywa renderowanie napisów. Wyjątki silnika są logowane i monitorowane. Środowisko testowe pozwala potwierdzić podatność bez eskalacji.
Wniosek
SSTI jest błędem architektonicznym, nie brakiem filtra. Powstaje w momencie, w którym ktoś uznaje, że szablon może pochodzić od użytkownika, i pozostaje niewidoczny do chwili, gdy ktoś sprawdzi, czy silnik oblicza wyrażenia. Naprawa jest tania, jeżeli podejmie się ją na poziomie projektu — dane jako kontekst, logic-less dla treści użytkownika, izolowany renderer. Jest droga, jeżeli próbuje się ją zrealizować listą zakazanych znaków, bo taka lista nigdy nie jest kompletna.
Źródła pierwotne: PortSwigger Research — Server-Side Template Injection, CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine, OWASP — Injection Prevention Cheat Sheet.


