Kod z AI nie jest niebezpieczny losowo

Prawie wrzuciłem dziurę na produkcję, bo model podał ją z pełnym przekonaniem. Potem sprawdziłem liczby, te z 2026, nie sprzed dwóch lat. Modele halucynują rzadziej, ale wybór modelu przestał ratować, a powierzchnia ataku jest dziś wspólna dla wszystkich.

Jul 3, 2026~7 min czytania
Kod z AI nie jest niebezpieczny losowo

Pisałem komponent do renderowania postów na code-nomad.com i poprosiłem AI o pomoc. Pierwsze, co dostałem, to komponent z dangerouslySetInnerHTML.

tsx
function Post({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Napisałem modelowi, że to otwiera drzwi do XSS. Jeśli treść nie jest sanityzowana, ktoś wstrzykuje dowolny skrypt. Model odpowiedział: masz rację, mój błąd, zmieniam podejście.

I tu jest cały problem. Złapałem to, bo akurat wiedziałem, że mam tego szukać. Gdyby mi się śpieszyło, gdybym nie znał tej konkretnej pułapki, wrzuciłbym ten kod z czystym sumieniem. Działałby. Bomba tykałaby cicho, aż ktoś by ją kiedyś odpalił.

Pisałem już, że najtrudniejsza praca z AI to ta, której nie widać w żadnej metryce. Ten tekst idzie krok dalej i zadaje pytanie, które wtedy zostawiłem otwarte: skoro złapałem dangerouslySetInnerHTML, bo przypadkiem wiedziałem, to ile rzeczy przepuszczam, bo nie wiem, że w ogóle istnieją.

Sprawdziłem liczby

Zacznę od punktu odniesienia, bo bez niego nie widać, co się stało. Badanie z USENIX Security, na modelach wydanych między połową 2023 a połową 2024, pokazało, że 19,7 procent rekomendowanych przez AI paczek nie istniało w żadnym repozytorium. Prawie jedna na pięć. (16 modeli, 576 000 próbek kodu.)

Brzmi jak inny świat i słusznie, bo to dwa lata temu. Z danymi o bezpieczeństwie zawsze jest ten problem, że zanim ktoś je porządnie zbierze i opublikuje, mija rok i opisują nieaktualne modele. Dlatego ważniejsze jest to, co wyszło, kiedy ktoś powtórzył ten sam pomiar na świeżym sprzęcie.

W maju 2026 powtórzono tę metodologię na pięciu modelach z przełomu 2025 i 2026: Claude Sonnet 4.6, Claude Haiku 4.5, GPT-5.4-mini, Gemini 2.5 Pro, DeepSeek V3.2. Halucynacje nazw paczek spadły do przedziału od 4,62 do 6,10 procenta. Mniej więcej jedna na dwadzieścia. Modele zrobiły ogromny postęp i trzeba im to oddać. Ale "rzadziej" to nie "wcale", a przy skali, w jakiej dziś generujemy kod, jedna na dwadzieścia to nadal bardzo dużo.

To nie jest "model się czasem myli". To jest systemowa właściwość narzędzia, którego używam codziennie, i ona nie znika z kolejną wersją, tylko się zmniejsza.

Wybór modelu przestał ratować

Mam słabość do myślenia o tym przez tiering: dobierz model do zadania, a masz pół problemu z głowy. Przy halucynacjach paczek to już nie działa, i widać to wprost w liczbach.

W 2024 rozrzut między modelami był przepaścią. Modele komercyjne halucynowały paczki w 5,2 procentach, open-source odpalane lokalnie w 21,7. Czterokrotna różnica. Wtedy wybór modelu realnie zmieniał ryzyko, sięgnięcie po lepszy model było decyzją o bezpieczeństwie.

W 2026 ta przepaść się zasypała. Od 4,62 procenta (Claude Haiku 4.5) do 6,10 (GPT-5.4-mini). Najlepszy i najgorszy dzieli półtora punktu. Nie ma już modelu, do którego uciekniesz przed problemem. Dźwignia, która jeszcze dwa lata temu działała, dziś nie ma czym ruszać.

Dlaczego to w ogóle da się zaatakować

Przez chwilę pocieszałem się myślą, że skoro to halucynacja, to jest losowa. Następnym razem model zmyśli inną nazwę, atak się nie spina, sprawa rozchodzi się po kościach. Tyle że halucynacja jest stabilna, i to z dwóch stron naraz.

Oś pierwsza, czas. Ten sam model, ten sam prompt, dziesięć powtórzeń. W badaniu z 2024 43 procent zmyślonych nazw wracało za każdym razem. Nie błąd losowy, tylko powtarzalne zachowanie modelu.

Oś druga, i to ona mnie zatrzymała, modele. Te pięć modeli z 2026 wymyśliło 127 identycznych nieistniejących nazw paczek: 109 na PyPI, 18 na npm. Nie różne halucynacje u różnych modeli. Te same, u wszystkich pięciu. To jest powierzchnia ataku niezależna od modelu, której żadne badanie jednego modelu by nie pokazało.

Skoro ta sama nazwa wraca w kolejnych przebiegach i pojawia się u pięciu różnych modeli naraz, to nie jest szum. To coś, co da się przewidzieć. A przewidywalną nazwę atakujący rejestruje z wyprzedzeniem na npm albo PyPI, z własnym kodem w środku, i czeka, aż ktokolwiek, na jakimkolwiek modelu, dostanie tę podpowiedź. To jest slopsquatting, i dlatego wybór modelu z poprzedniej sekcji niczego tu nie ratuje.

I mechanizm już zadziałał w praktyce. Paczka react-codeshift, nazwa zlepiona przez model z dwóch prawdziwych (jscodeshift i react-codemod), rozlała się przez 237 repozytoriów. Napędzana nie przez ludzi kopiujących polecenie instalacji, tylko przez agenty wykonujące własny wygenerowany output. Bez człowieka, który rzuciłby okiem na nazwę i zauważył, że coś jest nie tak. Payloadu nie było tylko dlatego, że badacz z Aikido zdążył zarejestrować nazwę pierwszy, zanim zrobił to ktoś z gorszymi intencjami. Kanał dystrybucji był gotowy i działał. Brakowało wyłącznie ładunku.

Gdzie kończy się mój review

Mam w produkcji system code review oparty na agentach. Pierwsze, co ludzie mówią, kiedy o tym opowiadam: no to dorzuć kolejnego agenta, od bezpieczeństwa, i niech to wyłapuje. Próbowałem tak myśleć. Tylko że problem nie polega na tym, że mój prompt jest za słaby.

Mój agent czyta diff. W diffie jest import nazwa-paczki. Agent sprawdzi, czy kod używa jej sensownie. Nie przeczyta tego, co ta paczka robi po instalacji, bo jej kodu w diffie nie ma. Najlepszy AGENTS.md, jaki napiszę, opisuje, jak ja myślę o swoim kodzie. Nie opisuje czterdziestu tysięcy linii w node_modules, których nikt nigdy nie przeczytał. Review pokrywa autorstwo. Nie pokrywa zaufania, które rozdałem w momencie instalacji.

Powiedzmy, że i tak takiego agenta napiszę. Nie pomoże. Najgroźniejszy wariant, prompt injection schowany w paczce albo w odpowiedzi narzędzia, nie żyje w tekście, który widać podczas review. Opis narzędzia model sprawdza raz, przy podłączeniu. Odpowiedź tego narzędzia leci wprost do kontekstu modelu bez żadnej równoważnej kontroli, i właśnie ten kanał w czasie działania atakujący wykorzystuje. W chwili review tej instrukcji jeszcze nie ma. Pojawia się, kiedy kod już chodzi.

To jest różnica między granicą zakresu a granicą możliwości: zakres mogę poszerzać kolejnymi agentami i lepszymi promptami, ale możliwości nie, bo instrukcji, która pojawi się dopiero w runtime, żaden prompt w chwili review nie zobaczy.

Dlaczego nie rozwiążę tego uporem

I dopiero teraz widać, czemu to nie jest kwestia większego wysiłku. Mogę odpalić Opus na maksymalnym efforcie. Mogę kazać agentowi audytować każdą zależność, linijka po linijce. Spalę całe okno kontekstowe, dobiję limit sesji, będę czekał kolejne cztery godziny.

I to wszystko po nic, bo prompt injection odpala się dopiero w runtime, a w chwili, gdy patrzyłem na diff, jej tam jeszcze nie było. Zapłaciłbym cały ten friction nie za bezpieczeństwo, tylko za poczucie, że coś sprawdziłem.

Kod z AI nie jest niebezpieczny losowo. Jest niebezpieczny przewidywalnie. A przewidywalność ma to do siebie, że atakujący czyta te same badania co ja i ma z nich więcej pożytku, bo jemu wystarczy zarejestrować nazwę i czekać. Mój review łapie to, co widać w diffie. Najtaniej atakuje się dokładnie tę część, której tam nie ma.


Źródła:

Czy ten wpis był pomocny?

Slopsquatting: kod z AI jest niebezpieczny przewidywalnie | Code Nomad