← Powrót do bloga
BezpieczeństwoScanaris Team · 6 min czytania · October 04, 2026

Sekrety, które twoja aplikacja z Cursor lub Lovable wycieka w przeglądarce

Kiedy prosisz AI, żeby „podłączyła Twoją aplikację do Stripe, OpenAI albo Supabase", często wkłada klucz prosto do kodu frontendu. Działa za pierwszym razem i właśnie dlatego nikt tego nie sprawdza. Problem jest prosty i bezwzględny: wszystko, co trafia do przeglądarki, jest publiczne. Nie da się ukryć tekstu, który przeglądarka musi przeczytać. Ten poradnik wyjaśnia, dlaczego tak się dzieje, jak atakujący znajduje klucz w minutę, które klucze są niebezpieczne, a które nie, jak samodzielnie ich szukać i co zrobić, jeśli któryś już wyciekł.

Dlaczego klucz ląduje w przeglądarce

Są trzy typowe drogi. Pierwsza to publiczny prefiks zmiennych środowiskowych: w Next.js wszystko, co zaczyna się od NEXT_PUBLIC_, jest wbudowywane w JavaScript klienta, tak samo jak VITE_ w Vite, NUXT_PUBLIC_ w Nuxt czy REACT_APP_ w Create React App. AI widzi, że „zmienna nie dociera", i dodaje prefiks, żeby dotarła. Druga to bezpośrednie wywołanie: asystent pisze fetch('https://api.openai.com/...') w komponencie interfejsu z kluczem w nagłówku, bo to najkrótszy przykład, jaki zna. Trzecia to współdzielony plik ze stałymi, importowany zarówno przez serwer, jak i klienta, w którym ktoś zostawił klucz „tymczasowo".

We wszystkich trzech przypadkach efekt jest ten sam: klucz podróżuje w pliku .js, który każdy może pobrać, i zostaje w cache'ach CDN, w historii przeglądarki Twoich użytkowników i w archiwach crawlerów, które przeczesują internet w poszukiwaniu dokładnie tego.

Jak atakujący widzi to w 60 sekund

  • DevTools. Otwórz swoją stronę, wciśnij F12, przejdź do zakładki Sources, wciśnij Ctrl+Shift+F, żeby szukać we wszystkich plikach, i wpisz sk_live, sk-ant, AKIA, service_role albo -----BEGIN. Jeśli się pojawi, on już to ma.
  • Źródło strony. Prawy przycisk, wyświetl źródło, i otwarcie każdego podlinkowanego pliku .js. Bundle są zminifikowane, ale nie zaszyfrowane; wystarczy wyszukiwanie tekstu.
  • Source mapy. Jeśli publikujesz pliki .map, oprócz kodu widzi oryginalne nazwy zmiennych i komentarze, łącznie z // TODO: przenieść na serwer.
  • Automatyczne skanery. Są boty, które przechodzą po nowych domenach, pobierają ich JavaScript i testują każdy wzorzec klucza u odpowiedniego dostawcy. Klucz AWS, który wyciekł w bundle'u, zostaje użyty w minuty, nie w dni.

Które klucze są niebezpieczne i co się dzieje, gdy wyciekną

  • Stripe sk_live_. Pozwala tworzyć obciążenia, zwroty i czytać dane klientów. To prawdziwe pieniądze.
  • OpenAI, Anthropic i podobne (sk-…). Kto go ma, zużywa na Twoje konto. Faktury na tysiące euro za klucz w publicznym repozytorium to historia, która się powtarza.
  • AWS AKIA… i Google Cloud. W zależności od uprawnień: dostęp do bucketów, baz danych i stawianie maszyn do kopania na Twój koszt.
  • Supabase service_role. Całkowicie omija Row Level Security: pełny odczyt i zapis Twojej bazy danych. Najpoważniejszy ze wszystkich w aplikacjach z vibe codingu.
  • Konta serwisowe Firebase (JSON). Odpowiednik service_role: pełna kontrola nad projektem.
  • Dostawcy e-maili i SMS-ów (Resend, SendGrid, Twilio). Wysyłanie wiadomości w Twoim imieniu, czyli phishing z Twojej domeny.
  • Stringi połączenia z bazą danych z użytkownikiem i hasłem oraz sekrety JWT: mając sekret, podpisuje się sesje dowolnego użytkownika.
  • Tokeny dostępu trzymane w localStorage. To nie Twoje klucze, tylko Twoich użytkowników, a każdy XSS je odczytuje i kradnie.

Które klucze mogą trafić do klienta

Nie każdy klucz jest sekretem. Niektóre są zaprojektowane do życia w przeglądarce, o ile to, co za nimi stoi, jest dobrze skonfigurowane:

  • Klucz anon lub publishable Supabase, chroniony politykami RLS każdej tabeli.
  • Klucz publiczny Stripe (pk_live_), który służy tylko do rozpoczynania płatności, nie do ich pobierania.
  • Konfiguracja Firebase (apiKey, projectId…), chroniona regułami bezpieczeństwa, a najlepiej także App Check.
  • Klucz Google Maps ograniczony do domeny w konsoli Google.
  • Identyfikatory analityki i widżetów firm trzecich.

Zasada, żeby je rozróżnić: klucz może trafić do klienta, jeśli dostawca wprost nazywa go publicznym i jeśli nawet mając go, obcy nie może zrobić niczego, czego nie chcesz. W razie wątpliwości jest tajny.

Jak szukać ich samodzielnie

W zbudowanym bundle'u szukaj znanych wzorców. W Next.js lub Vite, po buildzie:

grep -rEo "sk_live_[0-9a-zA-Z]+|sk-ant-[0-9a-zA-Z-]+|AKIA[0-9A-Z]{16}|service_role|eyJhbGciOi[0-9a-zA-Z._-]+" .next/static dist 2>/dev/null | head

W repozytorium, zanim klucz trafi do bundle'a, narzędzia takie jak gitleaks czy trufflehog przechodzą całą historię, nie tylko ostatni commit. A w panelu hostingu sprawdź, które zmienne mają publiczny prefiks: każda z nich jest wysyłana do przeglądarki.

Trzy miejsca, w które ludzie nie patrzą: komponenty oznaczone 'use client', które importują współdzielony plik konfiguracyjny; parametry URL-a, w których czasem podróżuje token lądujący w logach i historii; oraz publiczne repozytoria na GitHubie, gdzie klucz może leżeć od miesięcy, nawet jeśli nie ma go już w aktualnym kodzie.

Jak to naprawić: klucz zostaje na serwerze

Poprawny wzorzec jest taki, że przeglądarka rozmawia z Twoim serwerem, a Twój serwer z dostawcą. Własny endpoint odbiera żądanie, sprawdza sesję użytkownika, stosuje limit użycia i wykonuje wywołanie z kluczem odczytanym ze zmiennej środowiskowej bez publicznego prefiksu. W Next.js:

// app/api/summarize/route.ts  (tylko serwer)
import { getSession } from '@/lib/auth'

export async function POST(req: Request) {
  const session = await getSession(req)
  if (!session) return new Response('Unauthorized', { status: 401 })

  const { text } = await req.json()
  if (typeof text !== 'string' || text.length > 5000) {
    return new Response('Bad request', { status: 400 })
  }

  const r = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: text }] }),
  })
  return new Response(r.body, { headers: { 'Content-Type': 'application/json' } })
}

Z Supabase ta sama idea: operacje wymagające service_role idą do Edge Function albo do Twojego backendu, nigdy do klienta. Z Firebase do Cloud Function. I w każdym przypadku zmienna środowiskowa nazywa się OPENAI_API_KEY, a nie NEXT_PUBLIC_OPENAI_API_KEY.

Dla tokenów sesji wybieraj ciasteczka HttpOnly zamiast localStorage: ciasteczka HttpOnly nie da się odczytać z JavaScriptu, więc XSS nie może go ukraść. Jeśli Twoja biblioteka domyślnie trzyma sesję w localStorage, jak robi to klient Supabase w przeglądarce, zrekompensuj to rygorystyczną CSP albo użyj wariantu serwerowego z ciasteczkami.

Jeśli już wyciekł: rotować, nie usuwać

Usunięcie klucza z kodu nic nie daje: jest już w cache'ach, w historii gita i prawdopodobnie w czyichś rękach. Poprawna kolejność:

  • Utworzyć nowy klucz u dostawcy i wdrożyć go na serwerze.
  • Unieważnić stary zaraz potem. W Stripe, OpenAI i AWS robi się to jednym kliknięciem w panelu.
  • Przejrzeć użycie: rozliczenia, logi API i obciążenia z ostatnich dni. Jeśli doszło do nadużycia, otwórz zgłoszenie u dostawcy; czasem anulują opłaty.
  • Ustawić limity wydatków i alerty u każdego dostawcy, który na to pozwala, żeby następnym razem bolało mniej.
  • Jeśli klucz dawał dostęp do danych osobowych użytkowników, oceń, czy musisz to zgłosić zgodnie z RODO.

Czyszczenie historii gita narzędziami takimi jak BFG czy git filter-repo to dobra higiena, ale nie zastępuje rotacji.

Jak sprawdza to Scanaris

Scanaris pobiera JavaScript, który serwuje Twoja strona, tak jak zrobiłby to atakujący, i szuka wzorców kluczy popularnych dostawców, tokenów JWT, adresów webhooków i stringów połączenia. Ostrzega też, jeśli Twoje source mapy są dostępne i czy wrażliwe pliki leżą w zasięgu ręki. Każde znalezisko ma prompt do wklejenia w Twoje AI z dokładną poprawką: przenieść wywołanie na serwer i zrotować klucz. Jeśli włączysz monitoring, ostrzeże Cię, gdy nowa sesja vibe codingu znów wsadzi klucz do klienta. To część checklisty startowej i poradnika bezpieczeństwa aplikacji tworzonych przez AI.

Najczęściej zadawane pytania

Czy klucz anon Supabase jest sekretem?

Nie. Jest przeznaczony do przeglądarki i chroni go RLS każdej tabeli. Tajny jest service_role. Jeśli Twoje RLS jest złe, problemem nie jest klucz anon, tylko polityki.

A jeśli klucz jest ograniczony do domeny?

Ograniczenia po refererze (jak w Google Maps) zmniejszają nadużycia z innych stron, ale nie ze skryptów, które fałszują nagłówek. Nadają się do kluczy niskiego ryzyka, nie do kluczy, które wydają pieniądze albo czytają dane.

Czy wystarczy usunąć go z repozytorium?

Nie. Klucz, który był w publicznym bundle'u albo publicznym commicie, jest skompromitowany. Zawsze go rotuj, a potem wyczyść historię, jeśli chcesz.

Jak radzi sobie twoja strona?

Pozwól Scanaris ją sprawdzić w 30 sekund, za darmo.

Przeskanuj moją stronę

Powiązane testy

Przewodniki bezpieczeństwa dla twojego stacku