Kod pisany przez AI w produkcji — co znajdujemy najczęściej w audytach

Vibe coding trafił do produkcji, a kod z AI często nigdy nie został przeczytany przez człowieka ze zrozumieniem. Pokazujemy pięć wzorców, które w audytach jakości kodu generowanego przez AI znajdujemy najczęściej — i jak je wychwycić, zanim zrobią to użytkownicy.

Zespół Mobilesoft· Engineering team· 21 lipca 2026· 5 min czytania

Vibe coding trafił do produkcji. My sprzątamy po nim w audytach

Ostatni rok zmienił sposób, w jaki powstaje oprogramowanie. Zespoły — także te bez doświadczonych seniorów — dowożą działające funkcje w tempie, które jeszcze niedawno było nieosiągalne. „Vibe coding", czyli pisanie kodu przez opisywanie intencji asystentowi AI i akceptowanie tego, co wygeneruje, przestał być ciekawostką z Twittera. Trafił do repozytoriów, które audytujemy.

Efekt? Kod, który działa na demie, ale nie był nigdy przeczytany przez człowieka ze zrozumieniem. A my, wchodząc do takich projektów na audyt, widzimy powtarzalny zestaw problemów. Nie dlatego, że AI „jest głupie" — modele generują zaskakująco poprawny kod. Problem w tym, że generują go lokalnie poprawnie, ale bez kontekstu całego systemu i bez odpowiedzialności za konsekwencje.

Poniżej pięć wzorców, które w audytach jakości kodu generowanego przez AI znajdujemy najczęściej. Z konkretami i z tym, jak je wychwycić, zanim zrobią to Wasi użytkownicy albo audytor bezpieczeństwa.

1. Brak walidacji danych wejściowych

To zdecydowanie najczęstsze znalezisko. Asystent AI proszony o „endpoint, który zapisuje zamówienie" napisze endpoint, który zapisuje zamówienie. Dokładnie to — i nic więcej. Nie zapyta, co się stanie, gdy quantity będzie ujemne, email pusty, a price przyjdzie jako string "0.00; DROP TABLE".

Typowy wygenerowany handler wygląda tak:

app.post('/api/orders', async (req, res) => {
  const { userId, items, total } = req.body;
  const order = await db.orders.create({ userId, items, total });
  res.json(order);
});

Działa. Na demie z jednym poprawnym requestem — bez zarzutu. W produkcji total jest sterowany przez klienta (można kupić za grosz), items może być czymkolwiek, a brak walidacji userId to gotowy IDOR. To samo dotyczy warstwy bazy: pola „na wszelki wypadek" nullable, brak ograniczeń, brak indeksów unikalności.

Jak to łapiemy: szukamy granic systemu — kontrolerów, handlerów kolejek, webhooków — i sprawdzamy, czy każde wejście przechodzi przez schemat walidacji (Zod, class-validator, Pydantic, cokolwiek). Reguła praktyczna: dane z zewnątrz są wrogie, dopóki schemat nie udowodni, że są bezpieczne. AI tego założenia domyślnie nie przyjmuje.

2. Sekrety zapisane w repozytorium

Klucz API do Stripe'a. Connection string do produkcyjnej bazy. Token do S3. Wszystkie wklejone wprost do kodu albo do commitowanego .env, bo „na razie żeby zadziałało".

To nie jest wina wyłącznie AI — programiści robili to od zawsze. Ale vibe coding ten problem skaluje. Asystent chętnie wygeneruje const stripe = new Stripe('sk_live_...') z kluczem podanym w promptcie, a początkujący developer nie ma odruchu, żeby przenieść go do zmiennych środowiskowych i dopisać .env do .gitignore.

Kluczowy fakt, którego wiele osób nie rozumie: usunięcie sekretu kolejnym commitem go nie usuwa. Klucz zostaje w historii Gita i jest dostępny dla każdego, kto ma dostęp do repo — a przy publicznym repo zostaje zindeksowany przez boty w minuty.

Jak to łapiemy: skan historii (gitleaks, trufflehog), przegląd konfiguracji i CI/CD. Znaleziony żywy sekret oznacza jedno: rotacja natychmiast, bo trzeba założyć, że wyciekł. Sam commit „usuwający" go z kodu to fałszywe poczucie bezpieczeństwa.

3. Problem N+1 w zapytaniach do bazy

Wzorzec, który nie wywali się na code review „na oko" i nie wywali się na testach z pustą bazą. Wybucha dopiero pod realnym ruchem — i wtedy jest drogi.

AI świetnie generuje kod, który jest czytelny dla człowieka: pobierz użytkowników, dla każdego pobierz jego zamówienia. Problem w tym, że „dla każdego" to osobne zapytanie do bazy:

users = User.objects.all()
for user in users:
    # osobny SELECT dla KAŻDEGO użytkownika
    print(user.orders.count())

Przy 10 użytkownikach — niezauważalne. Przy 10 000 — to 10 001 zapytań na jedno żądanie HTTP, timeouty i baza na kolanach. To samo dzieje się z ORM-ami (Prisma, Hibernate, ActiveRecord), gdy leniwe ładowanie relacji trafia do pętli.

Jak to łapiemy: włączamy logowanie zapytań i patrzymy na liczbę SQL-i per request, przeglądamy pętle iterujące po kolekcjach z dostępem do relacji. Fix jest zwykle prosty (select_related/prefetch, include, JOIN) — pod warunkiem, że ktoś w ogóle szuka. Sam asystent tego nie zoptymalizuje, bo nie widzi profilu obciążenia.

4. Martwe testy — zielone, ale niczego nie sprawdzają

Najbardziej podstępne znalezisko, bo daje fałszywe poczucie bezpieczeństwa. Repozytorium ma testy, CI świeci na zielono, pokrycie wygląda przyzwoicie. A mimo to testy nie chronią przed niczym.

Poproszony o testy, asystent często generuje kod, który:

  • testuje mocki zamiast logiki — asercja sprawdza, że zamockowana funkcja zwróciła to, co jej kazano zwrócić;
  • nie ma sensownych asercji — wywołuje funkcję i sprawdza tylko, że „nie rzuciła wyjątku";
  • jest tautologicznyexpect(sum(2,2)).toBe(sum(2,2));
  • pokrywa happy path i tylko happy path — zero przypadków brzegowych, których dotyczą punkty 1–3.
it('creates a user', async () => {
  const save = jest.fn().mockResolvedValue({ id: 1 });
  await createUser({ save });
  expect(save).toHaveBeenCalled(); // sprawdza mock, nie logikę
});

Ten test przejdzie także wtedy, gdy createUser zapisuje śmieci albo ignoruje walidację. Pokrycie rośnie, realne bezpieczeństwo — nie.

Jak to łapiemy: czytamy asercje, nie liczbę testów. Dobry sprawdzian to mutation testing (Stryker, PIT) — celowo psujemy logikę i patrzymy, czy testy to wychwycą. Jeśli mutant przeżywa, test jest martwy.

Dlaczego to się dzieje — i dlaczego to nie jest argument przeciw AI

Warto to powiedzieć wprost: nie jesteśmy przeciwnikami kodu generowanego przez AI. Sami używamy tych narzędzi na co dzień i przyspieszają nas realnie. Rzecz w tym, żeby rozumieć, gdzie leży granica ich kompetencji.

Asystent AI optymalizuje pod „czy to wygląda na poprawną odpowiedź na moje pytanie". Nie optymalizuje pod bezpieczeństwo, wydajność pod obciążeniem, spójność z resztą systemu ani utrzymywalność za dwa lata. Te rzeczy wymagają kontekstu i odpowiedzialności, których model po prostu nie ma. Wygenerowany kod jest jak świetny pierwszy szkic od bardzo szybkiego juniora — i tak trzeba go traktować w review.

Problem nie w tym, że ktoś użył AI. Problem w tym, że wygenerowany kod trafił na produkcję bez tego review.

Jak się zabezpieczyć

Kilka rzeczy, które warto wdrożyć, jeśli w Waszym zespole powstaje dużo kodu z pomocą AI:

  1. Walidacja na granicach systemu jako standard. Każde wejście z zewnątrz przez schemat. Bez wyjątków.
  2. Skan sekretów w CI. gitleaks jako blokujący krok pipeline'u — taniej zapobiegać niż rotować po wycieku.
  3. Budżet zapytań na request. Monitoring liczby SQL-i, alert przy przekroczeniu progu. N+1 wychodzi od razu.
  4. Mutation testing na krytycznych ścieżkach. Przynajmniej tam, gdzie błąd kosztuje — płatności, autoryzacja, dane osobowe.
  5. Review z założeniem, że autor nie zna kontekstu. Bo przy kodzie z AI dosłownie tak jest.

Kiedy warto zamówić audyt

Jeśli w ostatnim roku Wasz produkt urósł głównie dzięki szybkiemu dowożeniu funkcji z pomocą AI — a nikt systematycznie nie przeglądał tego kodu pod kątem bezpieczeństwa i wydajności — to jest dokładnie ten moment, w którym audyt zwraca się najszybciej. Nie dlatego, że „AI namieszało", tylko dlatego, że tempo powstawania kodu wyprzedziło tempo jego weryfikacji.

W Mobilesoft robimy takie audyty regularnie — od przeglądu bezpieczeństwa i wydajności, po ocenę jakości testów i długu technologicznego. Wynik to nie lista wyrzutów, tylko priorytetyzowany plan naprawczy: co grozi najbardziej, co da się poprawić szybko i co warto przebudować, zanim urośnie dalej.

Jeśli chcecie sprawdzić, co siedzi w Waszym kodzie, zanim znajdzie to ktoś inny — porozmawiajmy.