ash_typescript 0.18.0 naprawia siedem CVE na granicy RPC i generowanego klienta
Dwie ścieżki wyczerpania tablicy atomów, wyciek pól objętych polityką, brak walidacji constraints i błędy URL pokazują ryzyko na styku Elixir–TypeScript.
- AUTOR
- Karol Rapacz / CEO Breachroad · OSCP · PNPT
- PUBLIKACJA
- 1 września 2026
- CZAS CZYTANIA
- 22 min czytania
- TEMAT
- Podatności i CVE
Do dzisiejszych feedów NVD trafiło siedem podatności ash_typescript, biblioteki generującej typowany interfejs między aplikacją Ash/Elixir a klientem TypeScript. Najpoważniejsze problemy pozwalają nieuwierzytelnionemu użytkownikowi doprowadzić do awarii całego węzła BEAM przez trwałe tworzenie atomów albo odczytać wartość pola, której polityka Ash odmówiła. Pozostałe luki dotyczą ujawniania surowych błędów, pomijania constraints argumentów i niekodowanych parametrów ścieżki.
Wszystkie siedem problemów poprawiono w ash_typescript 0.18.0. Zakres początkowy zależy od funkcji: najstarszy błąd istnieje od 0.1.0, kolejne od 0.8.0, 0.11.0 lub 0.15.0. Wydanie 0.18.0 pojawiło się 31 sierpnia wieczorem, a rekordy CVE opublikowano w NVD 1 września. Seria dobrze pokazuje, że wygenerowany typ TypeScript jest pomocą dla programisty, ale nie kontrolą bezpieczeństwa na serwerze.
CVE-2026-74837 i CVE-2026-77856: trwałe atomy z nazw pól klienta
Wirtualna maszyna BEAM przechowuje atomy w globalnej tablicy. Atomy są świetne jako znane w czasie tworzenia programu identyfikatory, lecz nie są garbage-collected. Każda nowa wartość pozostaje do końca życia węzła. Gdy tablica osiągnie limit, VM kończy działanie. Dlatego String.to_atom/1 na dowolnym wejściu sieciowym jest znanym antywzorcem bezpieczeństwa.
W CVE-2026-74837 klient RPC mógł dostarczać nazwy wybieranych pól. parse_input_field/2 próbował String.to_existing_atom/1, ale dla nieznanej wartości zwracał string. Następny etap, convert_to_field_atom/2, ponownie zmieniał go w atom przez String.to_atom/1, zanim sprawdzono, czy pole w ogóle istnieje. Każda unikalna nazwa trwale powiększała tablicę. Nazwa dłuższa niż 255 znaków mogła dodatkowo wywołać nieobsłużony SystemLimitError.
CVE-2026-77856 dotyczy równoległej ścieżki dla pól typed struct. Gdy reverse map nie zawierał nazwy, resolve_typed_struct_field/2 również używał String.to_atom/1. Atakujący nie potrzebował znać schematu: właśnie brak dopasowania uruchamiał tworzenie nowej wartości.
Pierwsza luka ma CVSS 4.0 równy 8,7 i obejmuje wersje od 0.1.0. Druga ma 8,2 i zaczyna się od 0.11.0. Skutek jest większy niż błąd pojedynczego requestu. Tablica atomów jest współdzielona przez węzeł, a zużycia nie odzyska się bez restartu. Niski ruch rozłożony w czasie może więc akumulować trwały koszt.
CVE-2026-82730: ForbiddenField ujawniał original_value
Polityki pól Ash mogą odmówić odczytu atrybutu. Framework podstawia wtedy %Ash.ForbiddenField{}. Marker zachowuje prawdziwą wartość w original_value, ponieważ osadzone zasoby muszą pozostać zapisywalne, lecz ukrywa ją w standardowym Inspect. ash_typescript usuwał takie markery do nil w ścieżkach opartych na szablonie wyniku.
Nie każda serializacja przechodziła jednak tą drogą. normalize_primitive/1 nie miał osobnej klauzuli dla ForbiddenField, więc marker wpadał do obsługi dowolnego structa. Map.from_struct/1 zamieniał go w zwykłą mapę ze wszystkimi kluczami, w tym original_value. W efekcie odpowiedź RPC mogła zawierać zarówno informację o odmowie, jak i tajną wartość, której odmówiono.
Najprostsza osiągalna ścieżka to akcja zwracająca embedded resource jako mapę i przetwarzanie bez template. Publiczna funkcja normalize_value_for_json/1 prowadziła do tego samego rezultatu. CVE-2026-82730 ma CVSS 8,2 i obejmuje wersje od 0.11.0. To ważna lekcja: redakcja w Inspect chroni log lub konsolę, ale nie automatycznie wszystkie serializatory.
CVE-2026-82732: typ został rzutowany, lecz constraints nie zastosowano
Typed controller przyjmował argument HTTP i wywoływał Ash.Type.cast_input/3. Wynik {:ok, cast} traktował jako pełną walidację. W Ash rzutowanie i ograniczenia są oddzielnymi etapami. cast_input zmienia reprezentację na właściwy typ, natomiast Ash.Type.apply_constraints/3 wymusza one_of, długość, minimum, maksimum, wzorzec oraz normalizację pustego stringa do nil.
Pominięcie drugiego kroku sprawiało, że dowolny klient HTTP mógł wysłać wartość poza allowlistą lub zakresem. Wygenerowany klient TypeScript pokazywał ograniczony typ, więc błąd był słabo widoczny podczas zwykłego developmentu. Typ znika jednak w runtime i nie wiąże klienta napisanego ręcznie. Gdy constraint koduje rolę, stan workflow, kierunek sortowania lub identyfikator wariantu operacji, rezultat może stać się obejściem logiki uprawnień albo state machine.
CVE-2026-82732 obejmuje wersje od 0.15.0 i ma CVSS 6,3. Poprawka stosuje constraints po rzutowaniu. Test bezpieczeństwa powinien sprawdzać serwer za pomocą surowego HTTP, a nie wyłącznie wygenerowanego SDK.
CVE-2026-77950 i CVE-2026-82733: dwa wyjątki od polityki bezpiecznych błędów
W CVE-2026-77950 aplikacja mogła skonfigurować error handler do redakcji lub całkowitego pominięcia błędu. Handlery często używają pattern matching dla znanych kształtów. Nieoczekiwany kształt wywoływał FunctionClauseError. Rescue logował ostrzeżenie, po czym zwracał oryginalną, ni zredagowaną mapę. Intencja „ukryj ten błąd” zmieniała się więc w „opublikuj go bez zmian”. Luka ma CVSS 6,3 i dotyczy wersji od 0.8.0.
CVE-2026-82733 występowało, gdy handler typed controllera zwrócił coś innego niż %Plug.Conn{}. Funkcja unexpected_return/2 umieszczała inspect(value, limit: 50) bezpośrednio w treści HTTP 500. limit: 50 ogranicza liczbę elementów kolekcji, nie całkowity rozmiar lub wrażliwość termu. Krotka błędu zawierająca %User{}, changeset albo inny bogaty struct mogła ujawnić hashe haseł, tokeny i identyfikatory tenantów. Ten kanał był zawsze aktywny, mimo że wyjątki rzucane w innym miejscu podlegały konfiguracji typed_controller_show_raised_errors?. CVSS również wynosi 6,3, a zakres zaczyna się od 0.15.0.
Bezpieczny model to fail closed: awaria redaktora musi zakończyć się komunikatem ogólnym, nie fallbackiem do surowych danych. Szczegóły powinny trafić do kontrolowanego logu z maskowaniem sekretów i ograniczonym dostępem.
CVE-2026-82731: typowany parametr ścieżki bez encodeURIComponent
Generator tras podstawiał parametr :param do template stringu bez encodeURIComponent. Wartość zawierająca segment nadrzędny mogła po normalizacji trafić do innej trasy. ? lub # zmieniały granicę ścieżki i query, a szczególny przypadek, gdy trasa zaczynała się parametrem, pozwalał zbudować protocol-relative URL. Fetch mógł wówczas wysłać żądanie i skonfigurowane credentials do innego originu.
Query string nie był podatny tą samą drogą, ponieważ URLSearchParams.set koduje wartości. Problem obejmuje wersje od 0.15.0 i ma niskie CVSS 2,3, ale przy kliencie dołączającym token jego lokalny wpływ może być istotny. TypeScript sprawdza typ wartości podczas kompilacji, nie jej semantykę jako fragmentu URL.
Kogo dotyczy seria i jak ustalić osiągalność
Zacznij od wersji faktycznie dołączonej do release’u Elixir. Następnie zinwentaryzuj trzy powierzchnie: RPC field selection, typed controllers oraz wygenerowane klienty używane w przeglądarce lub Node. Nie każda aplikacja wystawia każdą funkcję.
Weryfikacja powinna odpowiedzieć:
- czy anonimowy klient może wybierać pola RPC lub pola typed struct;
- czy odpowiedzi zawierają embedded resources i pola chronione politykami;
- czy typed-controller arguments polegają na
one_of, boundach lub regexie; - czy error handler używa częściowego pattern matchingu;
- czy handler może zwrócić term zawierający dane użytkownika lub sekrety;
- czy wygenerowane URL-e zaczynają się od parametru i automatycznie dołączają credentials.
Priorytetem jest aktualizacja do ash_typescript 0.18.0 lub późniejszej kompatybilnej wersji. Jeśli rollout musi poczekać, ogranicz anonimowy RPC na brzegu, odrzuć nieznane nazwy pól przed biblioteką, wymuś serwerową allowlistę argumentów, wyłącz szczegółowe błędy i nie używaj podatnego klienta dla tras, w których pierwszy segment jest kontrolowany.
Detekcja i bezpieczna walidacja
Monitoruj liczbę atomów w BEAM oraz tempo jej wzrostu względem ruchu RPC. W logach szukaj wielu nieznanych nazw pól, SystemLimitError, odpowiedzi 500 zawierających reprezentacje structów, klucza original_value i wartości spoza deklarowanych constraints. Po stronie klienta obserwuj żądania do nieoczekiwanych originów oraz rozbieżności między trasą logiczną a finalnym URL.
Testy po aktualizacji mogą być małe. Kilkaset unikalnych błędnych nazw powinno zostać odrzuconych bez mierzalnego przyrostu atomów; zabronione pole musi być nil bez original_value; niedozwolony enum i pusty required argument powinny dać kontrolowany błąd; awaria redaktora ma zwrócić komunikat ogólny; a specjalne znaki parametru muszą być zakodowane.
Nie próbuj doprowadzać produkcyjnego węzła do limitu atomów. Trend i test na izolowanym węźle wystarczają do potwierdzenia zachowania.
Fakty źródłowe i wnioski Breachroad
Mechanizmy, wersje, wyniki CVSS i poprawka 0.18.0 pochodzą z advisory ash_typescript, commitów i rekordów CNA/NVD. Sposób inwentaryzacji, telemetria i kolejność testów są wnioskami Breachroad. Źródła nie raportują potwierdzonego aktywnego wykorzystania tej serii.
Źródła pierwotne
- CVE-2026-74837: atomy z pól RPC
- CVE-2026-77856: atomy z typed struct
- CVE-2026-77950: awaria error handlera
- CVE-2026-82730: wyciek ForbiddenField
- CVE-2026-82731: niekodowany parametr URL
- CVE-2026-82732: pominięte constraints
- CVE-2026-82733: surowy term w HTTP 500
- ash_typescript 0.18.0
Granica między typowanym klientem i serwerem nadal jest granicą niezaufanego wejścia. Na szkoleniach z cyberbezpieczeństwa uczymy zespoły testować rzeczywiste zachowanie runtime, nie tylko deklaracje typów. Kontrolowany pentest web i API może zweryfikować polityki pól, RPC, walidację i kanały błędów w konkretnym wdrożeniu.


