NLTK CVE-2026-78683 i CVE-2026-79657: bezpieczny pickle nadal wykonywał kod
TransitionParser używał nieograniczonego unpicklera, a pierwsza allowlista ufała całym modułom. Analiza RCE, wersji 3.10.0 i 3.10.3 oraz ochrony pipeline'u NLP.
- AUTOR
- Karol Rapacz / CEO Breachroad · OSCP · PNPT
- PUBLIKACJA
- 25 sierpnia 2026
- CZAS CZYTANIA
- 20 min czytania
- TEMAT
- Bezpieczeństwo AI
Dwa krytyczne rekordy opublikowane 25 sierpnia pokazują, dlaczego plik modelu w Pythonie nie może być traktowany jak pasywny zbiór danych. CVE-2026-78683 dotyczyło nieograniczonej deserializacji pickle w TransitionParser.parse(). Pierwsza naprawa, dostępna w NLTK 3.10.0, zastąpiła ją loaderem z allowlistą. CVE-2026-79657 wykazało jednak, że allowlista ufała całym przestrzeniom nazw, w których nadal znajdowały się funkcje pozwalające wykonać kod. Pełna poprawka wymaga NLTK 3.10.3.
Pierwszy rekord otrzymał CVSS 4.0 9,4 i CVSS 3.1 9,6, a drugi odpowiednio 9,3 i 9,8. Ocena skutku jest wysoka, lecz biblioteka sama nie udostępnia sieciowego endpointu do wgrywania modelu. RCE pojawia się wtedy, gdy aplikacja, notebook, pipeline albo usługa NLP przekazuje do podatnego loadera artefakt kontrolowany przez napastnika.
Dlaczego pickle jest formatem wykonawczym
Python pickle zapisuje sposób odtworzenia grafu obiektów. Strumień może wskazywać klasy i funkcje, a opcode REDUCE poleca wywołać callable z argumentami. To pozwala odtworzyć złożone modele, ale oznacza też, że deserializacja nieznanego pliku jest bliższa uruchomieniu programu niż parsowaniu JSON.
Ostrzeżenie w logu nie zmienia tej właściwości. W NLTK funkcja pickle_load() mogła kierować dane do WarningUnpickler, który sygnalizował ryzyko, lecz dziedziczył standardowe zachowanie pickle.Unpickler i nie blokował rozwiązywania klas przez find_class(). Kod wykonywał się podczas load(), zanim aplikacja mogła sprawdzić typ zwróconego obiektu.
Dlatego walidacja „po odczycie” jest spóźniona. Jeżeli loader najpierw rekonstruuje obiekt, a potem sprawdza, czy to oczekiwany model, funkcja wywołana przez pickle zdążyła już wykonać skutek. Bezpieczna granica musi kontrolować globalne symbole i opcode’y przed ich użyciem.
Pierwsza luka: TransitionParser i unrestricted loader
W wersjach do 3.9.4 włącznie metoda TransitionParser.parse() otwierała plik modelu i wywoływała pickle_load() bez restricted=True. Wbudowany RestrictedUnpickler istniał, ale produkcyjna ścieżka parsera go nie wybierała. Przygotowany artefakt mógł więc rozwiązać dowolną dostępną funkcję Pythona i wykonać ją z prawami procesu uruchamiającego NLTK.
Atak wymaga, aby nieufny plik dotarł do parametru modelFile. W notebooku może to być model pobrany z linku w publikacji. W pipeline’ie MLOps — obiekt z bucketu lub registry, do którego zapisuje inny zespół. W usłudze webowej — plik przesłany przez użytkownika albo wybrany przez identyfikator mapowany na zewnętrzny storage. Samo posiadanie NLTK w zależnościach nie dowodzi osiągalnej ścieżki.
NLTK 3.10.0 zmieniło parser na allowlisted_pickle_load i dodało osobny helper dla starszych artefaktów Punkt. To był właściwy kierunek: ograniczyć rekonstrukcję do symboli potrzebnych prawidłowemu modelowi. Problem polegał na tym, jak szeroko zdefiniowano zaufanie.
Druga luka: przestrzeń nazw nie jest granicą bezpieczeństwa
Pierwsza allowlista dopuszczała moduły poprzez prefiksy, między innymi przestrzenie potrzebne modelom NumPy, SciPy i scikit-learn. Dla Punkt dopuszczono również szeroki fragment nltk.tokenize. Loader sprawdzał więc, czy deklarowana nazwa należy do zaufanego modułu, ale nie zawsze czy konkretny callable jest bezpiecznym konstruktorem danych.
CVE-2026-79657 pokazało dwie klasy gadżetów. W przestrzeni NLTK dostępna była funkcja ReppTokenizer._execute, która uruchamia proces. W NumPy funkcja z numpy.f2py.crackfortran prowadziła do oceny kontrolowanego wyrażenia. Pickle nie musiał wychodzić poza dozwolony prefiks — wykorzystywał niebezpieczną funkcję znajdującą się wewnątrz niego.
To ogólny błąd projektowy. Moduł jest jednostką organizacji kodu, a nie jednolitą domeną zaufania. Obok klas reprezentujących tablicę mogą znajdować się funkcje pobierające sieć, otwierające plik, wywołujące subprocess albo interpretujące wyrażenie. Re-export dodatkowo sprawia, że niebezpieczny symbol może być osiągalny przez pozornie dozwoloną ścieżkę.
Co zmienia 3.10.3
Wersja 3.10.3, opublikowana w PyPI 12 sierpnia, jest pierwszą wersją obejmującą oba rekordy. Poprawki odrzucają kropkowane nazwy pozwalające na przechodzenie po atrybutach, blokują niebezpieczne moduły i callable oraz zawężają wybrane loadery do dokładnych par (module, qualname). Dla TransitionParser projekt zbudował listę konkretnych globali potrzebnych prawidłowemu modelowi SVC zamiast zezwalać na całe NumPy, SciPy i sklearn.
Projekt dodał również testy dla gadżetów, re-exportów, opcode’ów rozszerzeń i legalnych modeli. To ważne, ponieważ denylista bez testu regresji łatwo traci kompletność wraz ze zmianą zależności. NumPy lub SciPy może w kolejnej wersji eksportować nową funkcję przez inną nazwę.
Administrator powinien aktualizować bezpośrednio do 3.10.3 lub nowszej wspieranej wersji. Zatrzymanie się na 3.10.0, 3.10.1 albo 3.10.2 usuwa pierwotny unrestricted loader, lecz pozostawia klasę obejść allowlisty opisaną przez drugie CVE. Numer należy potwierdzić w rzeczywistym środowisku uruchomieniowym, nie tylko w requirements.txt: notebook, worker i obraz batch mogą używać różnych virtualenvów.
Inwentaryzacja osiągalności
Szukaj wywołań TransitionParser.parse, allowlisted_pickle_load, punkt_pickle_load i własnych wrapperów nad NLTK. Ustal, skąd pochodzi każdy plik: paczka aplikacji, kontrolowane registry, wspólny storage, upload, adres URL czy katalog notebooka. Następnie sprawdź, kto może zmienić obiekt oraz czy wersja jest przypięta do niezmiennego digestu.
Nie ograniczaj przeglądu do rozszerzenia .pickle lub .pkl. Nazwa pliku nie zmienia formatu, a artefakt modelu może być ukryty w archiwum, nazwany ogólnie albo pobrany przez manifest. Warto także przejrzeć cache użytkowników i warstwy obrazu, ponieważ aktualizacja biblioteki nie usuwa wcześniej pobranych, niezatwierdzonych modeli.
SBOM powinien wskazać wersję NLTK, ale potrzebny jest też manifest artefaktów AI: źródło, właściciel, rewizja, hash, format i loader. Bez tej informacji zespół odpowie, jaka biblioteka była uruchomiona, lecz nie odtworzy, jaki strumień danych został przez nią zdeserializowany.
Detekcja i reagowanie
Po stronie procesu szukaj nietypowych dzieci procesów Pythona, uruchomień powłoki, narzędzi sieciowych i interpreterów w czasie ładowania modelu. Skoreluj je z pobraniem artefaktu, startem joba i logiem parsera. Alarmujące są też połączenia wychodzące przed rozpoczęciem właściwego przetwarzania NLP, zmiany w katalogu roboczym i odczyt sekretów przez krótko żyjący worker.
Jeżeli nieufny pickle został załadowany przez podatną wersję, nie wystarcza przeskanowanie samego pliku pod kątem znanych ciągów. Pickle ma wiele sposobów złożenia grafu, a legalne funkcje zależności tworzą szeroką powierzchnię gadżetów. Traktuj proces jako potencjalnie naruszony, zachowaj artefakt i logi, odizoluj workload oraz ustal, do jakich sekretów i usług miał dostęp.
Po potwierdzeniu wykonania kodu odbuduj worker z czystego obrazu, zmień osiągalne poświadczenia i zweryfikuj współdzielony cache. Rotację wykonuj po zatrzymaniu trwałości. Jeżeli notebook miał token chmury lub registry, przejrzyj historię użycia tej tożsamości także poza hostem NLP.
Projektowanie bezpiecznego pipeline’u modeli
Najbezpieczniejszym rozwiązaniem jest format danych bez semantyki wywołań. Jeżeli kompatybilność wymaga pickle, używaj dokładnej allowlisty klas i funkcji, blokuj nieznane opcode’y, ogranicz rozmiar oraz uruchamiaj import w izolowanym procesie bez sekretów i szerokiego egressu. Pozytywna lista powinna wynikać z prawdziwego, zatwierdzonego modelu i być objęta testem.
Promocja artefaktu powinna przypominać promocję kodu. Środowisko badawcze może proponować model, skaner i review ustalają format oraz symbole, a produkcja pobiera wyłącznie przypięty digest z wewnętrznego registry. To nie eliminuje wszystkich błędów parsera, ale usuwa możliwość arbitralnej podmiany przez użytkownika końcowego.
Fakty źródłowe i wnioski Breachroad
Podatne wersje, metody loaderów, mechanizm unrestricted pickle, obejście szerokiej allowlisty, przykładowe klasy funkcji oraz wyniki CVSS pochodzą z rekordów CVE i advisories projektu. Daty pakietów 3.10.0 oraz 3.10.3 pochodzą z PyPI i tagów NLTK. Źródła nie zgłaszają aktywnej eksploatacji tych dwóch CVE.
Model inwentaryzacji, monitoring procesu, manifest artefaktu i zalecenia izolacji są wnioskami Breachroad. Szkolenia bezpieczeństwa AI i MLOps pomagają zespołom rozpoznawać wykonywalne formaty, a audyty aplikacji i API mogą ocenić upload, registry, loader i tożsamość workera.


