Powrót do bloga
13 maja 2026

Klucz API OpenAI w nagłówkach odpowiedzi HTTP: Wykryto w 7 minut

Viktor Bulanek
Founder & CTO, Penetrify
MSc IT Security · 20+ years in security · 4x Ex-CTO

Panel rozliczeniowy OpenAI pokazywał opłaty, których założyciel nie potrafił wyjaśnić. Aplikacja miała około 800 użytkowników w modelu freemium, ale użycie API zbliżało się do 2000 USD miesięcznie — znacznie wyższe niż uzasadniała aktywność użytkowników na platformie. Założyciel założył, że gdzieś mają nieefektywny prompt i zanotował, aby to zbadać.

Siedem minut po rozpoczęciu skanowania Penetrify, powód stał się jasny: klucz API OpenAI był przekazywany użytkownikom w nagłówkach odpowiedzi HTTP każdego pośredniczonego wywołania API. 800 użytkowników to widziało. Niektórzy z nich go używali.


Architektura: Skąd pochodził wyciek

Aplikacja była backendem FastAPI obsługującym frontend React. Jej główną funkcjonalnością było pośredniczenie w żądaniach użytkowników do API OpenAI, dodawanie niestandardowych promptów systemowych, przechowywanie historii rozmów i stosowanie autorskiej warstwy inżynierii promptów założyciela. To jest powszechny wzorzec dla produktów typu AI wrapper — wartość nie tkwi w modelu, lecz w produkcie zbudowanym wokół niego.

Sposób działania aplikacji:

  1. Użytkownik wysyła prompt z frontendu React
  2. Frontend wysyła go do POST /api/generate
  3. Handler FastAPI dodaje prompt systemowy i wywołuje API OpenAI
  4. FastAPI zwraca ukończenie do frontendu

Gdzieś w implementacji routingu FastAPI, nagłówek Authorization z wychodzącego żądania OpenAI — zawierający klucz API w formacie tokenu Bearer — był przekazywany z powrotem w odpowiedzi. Jest to specyficzna klasa błędu przekazywania nagłówków: aplikacja przekazywała nagłówki odpowiedzi z nadrzędnego wywołania API OpenAI zamiast konstruować własne nagłówki odpowiedzi.

Nagłówki odpowiedzi dla każdego wywołania /api/generate zawierały:

HTTP/1.1 200 OK
Content-Type: application/json
Authorization: Bearer sk-proj-...[OpenAI API key]
...
{"completion": "..."}

Każdy użytkownik, który kiedykolwiek korzystał z funkcji generowania — wszystkich 800 — otrzymał klucz API w nagłówkach odpowiedzi swoich żądań. Było to widoczne w zakładce Sieć Narzędzi Deweloperskich przeglądarki, w każdym proxy HTTP oraz w każdym programistycznym kliencie, który odczytywał nagłówki odpowiedzi.


Co daje klucz API OpenAI

Klucz API OpenAI bez ograniczeń użytkowania daje posiadaczowi pełny dostęp do limitu API odpowiadającego konta. Oznacza to:

  • Nieograniczony dostęp do modeli na koszt właściciela klucza — GPT-4o, o1, o3, generowanie obrazów, embeddingi, dostrajanie
  • Brak limitu na żądanie do momentu osiągnięcia miesięcznego limitu wydatków konta
  • Dostęp do wszystkich dostrojonych modeli utworzonych przez konto
  • Możliwość odczytu przechowywanych plików, jeśli konto korzysta z Files API

Dla indywidualnego założyciela, którego aplikacja przetwarza 200–400 USD miesięcznie w ramach legalnego użytkowania, zewnętrzne nadużycie klucza może podnieść miesięczny rachunek do 2000 USD, 5000 USD lub więcej — w zależności od tego, jak szeroko klucz krąży i co generują osoby nadużywające.

Model kosztów nadużycia API OpenAI jest asymetryczny: atakujący nie płaci nic, właściciel klucza płaci za wszystko.


Niewyjaśnione skoki rozliczeniowe, wyjaśnione

Po zidentyfikowaniu ekspozycji klucza, skoki rozliczeniowe nabrały sensu. Założyciel pobrał panel użycia OpenAI, filtrowany według punktu końcowego i czasu. Wzorzec skoków pokazywał żądania o dużej objętości, które nie korelowały z aktywnością użytkowników na platformie — żądania o 3 nad ranem, żądania z zakresów IP, które nie pasowały do żadnej znanej geografii użytkowników, żądania dotyczące typów modeli, których aplikacja nie używała.

Ktoś wydobył klucz — być może kilka osób — i używał go bezpośrednio przeciwko OpenAI API, całkowicie omijając aplikację. Żądania trafiały bezpośrednio do OpenAI, wykorzystując wydobyte dane uwierzytelniające, a nie przez aplikację założyciela.

Klucz był ujawniony od około pierwszego tygodnia publicznego uruchomienia aplikacji. W momencie skanowania był aktywny i wyciekał przez kilka miesięcy.


Pozostałe Ustalenia

Ujawnienie klucza OpenAI było najbardziej bezpośrednio szkodliwym ustaleniem, ale zgłoszono trzy dodatkowe problemy:

MEDIUM — IDOR na /api/history/:userId

Aplikacja przechowywała historię rozmów dla każdego użytkownika i udostępniała ją pod przewidywalnym punktem końcowym:

GET /api/history/abc123

Obsługa trasy pobierała historię rozmów dla identyfikatora użytkownika w parametrze ścieżki bez sprawdzania, czy użytkownik żądający był właścicielem tych rekordów. Każdy uwierzytelniony użytkownik mógł odczytać historię rozmów dowolnego innego użytkownika, podstawiając jego identyfikator. Ponieważ rozmowy zawierały podpowiedzi dostarczone przez użytkownika, było to również naruszenie prywatności: atakujący mógł odczytać, jakie pytania inni użytkownicy zadawali narzędziu AI.

MEDIUM — Tryb debugowania FastAPI włączony w środowisku produkcyjnym

Aplikacja działała z FastAPI(debug=True). W trybie debugowania, każdy nieobsłużony wyjątek zwraca pełny ślad stosu w odpowiedzi HTTP, w tym wewnętrzne ścieżki plików, wersje zależności i nazwy zmiennych środowiskowych (choć nie ich wartości). Te informacje są bezpośrednio przydatne do planowania dalszych ataków — znajomość dokładnej wersji FastAPI, Pydantic i Python znacząco zawęża listę możliwych CVE.

Tryb debugowania domyślnie włącza również interaktywną dokumentację FastAPI pod adresami /docs i /redoc, która była dostępna w środowisku produkcyjnym i dokumentowała każdy wewnętrzny punkt końcowy API, w tym te nieprzeznaczone do dostępu użytkowników.

LOW — HTTP nie przekierowuje do HTTPS

Wersja HTTP aplikacji serwowała pełną zawartość bez przekierowywania do HTTPS. W sieciach publicznych lub współdzielonych, atakujący przeprowadzający atak typu man-in-the-middle mógł przechwycić niezaszyfrowane sesje i wydobyć tokeny sesji, podpowiedzi użytkowników oraz odpowiedzi API.


Naprawa: Wdrożona Tego Samego Wieczoru

Założyciel wdrożył poprawki dla wszystkich ustaleń w ciągu trzech godzin od otrzymania raportu.

Najpierw zmień klucz

Zanim dotknięto jakiegokolwiek kodu, natychmiastowym działaniem było unieważnienie skompromitowanego klucza w panelu OpenAI i wygenerowanie nowego. To natychmiastowo przerwało wszelkie trwające nadużycia. Rotacja kluczy OpenAI jest natychmiastowa — stary klucz przestaje działać w momencie jego usunięcia.

Napraw błąd przekazywania nagłówków

Główną przyczyną było to, że trasa FastAPI używała generycznego klienta HTTP, który przekazywał wszystkie nagłówki odpowiedzi z nadrzędnego wywołania OpenAI. Naprawa polegała na konstruowaniu jawnych nagłówków odpowiedzi, zamiast przekazywania tych z góry strumienia:

# Przed (podatne) — przekazywanie wszystkich nagłówków z góry strumienia
upstream_response = await client.post(
    "https://api.openai.com/v1/chat/completions",
    headers={"Authorization": f"Bearer {settings.OPENAI_API_KEY}", ...},
    json=payload
)
return Response(
    content=upstream_response.content,
    headers=dict(upstream_response.headers)  # ← to przekazuje nagłówek Authorization z powrotem
)

# Po (naprawione) — jawna konstrukcja odpowiedzi
upstream_response = await client.post(
    "https://api.openai.com/v1/chat/completions",
    headers={"Authorization": f"Bearer {settings.OPENAI_API_KEY}", ...},
    json=payload
)
completion_data = upstream_response.json()
return JSONResponse(content={"completion": completion_data["choices"][0]["message"]["content"]})
# Tylko dane, które jawnie chcemy zwrócić — brak przekazywania nagłówków z góry strumienia

Napraw IDOR

Punkt końcowy historii rozmów został zaktualizowany, aby wyodrębniać ID użytkownika ze zweryfikowanego JWT, a nie z parametru ścieżki:

@router.get("/api/history")
async def get_history(current_user: User = Depends(get_current_user)):
    # ID użytkownika pochodzi ze zweryfikowanego JWT — nie może zostać sfałszowane
    history = await db.get_history(user_id=current_user.id)
    return history

Wyłącz tryb debugowania

# W config.py
app = FastAPI(
    debug=settings.DEBUG,  # odczytuje ze zmiennej środowiskowej
    docs_url=None if not settings.DEBUG else "/docs",  # ukryj dokumentację w środowisku produkcyjnym
    redoc_url=None if not settings.DEBUG else "/redoc"
)

Po ustawieniu DEBUG=false w środowisku produkcyjnym, interaktywna dokumentacja i szczegółowe odpowiedzi błędów zniknęły natychmiast po kolejnym wdrożeniu.


Dodawanie limitów użycia OpenAI jako siatki bezpieczeństwa

Oprócz naprawy wycieku, założyciel dodał dwa środki obronne, aby ograniczyć zasięg szkód w przypadku przyszłego ujawnienia klucza:

Limity użycia: W panelu OpenAI w sekcji Rozliczenia → Limity użycia, ustaw miesięczny twardy limit i miękki próg powiadomień. Nawet jeśli klucz zostanie ponownie skompromitowany, zdolność atakującego do generowania kosztów jest ograniczona.

Dedykowane klucze dla każdej usługi: Utwórz oddzielny klucz API dla każdej aplikacji lub środowiska. Jeśli klucz zostanie skompromitowany, możesz obrócić tylko ten klucz bez zakłócania działania innych usług, a dzienniki użycia dla każdego klucza są wyraźnie oddzielone — co znacznie ułatwia wykrycie nieautoryzowanego dostępu.


Jak często to się zdarza?

Ujawnienie klucza API w odpowiedziach HTTP jest mniej powszechne niż ujawnienie w pakietach JavaScript, ale regularnie obserwujemy to w aplikacjach opakowujących AI. Wzorzec ten prawie zawsze ma tę samą przyczynę źródłową: programista tworzący warstwę proxy używa generycznego klienta HTTP, który przekazuje nagłówki odpowiedzi, i nie audytuje, co te nagłówki zawierają.

Błąd przekazywania nagłówków jest łatwy do popełnienia, ponieważ często upraszcza implementację. Po co konstruować nową odpowiedź, skoro można przekazać tę z góry? Odpowiedź, w tym przypadku, jest taka, że odpowiedź z góry zawiera dane uwierzytelniające, których nie chcesz udostępniać swoim użytkownikom.

Jeśli Twoja aplikacja pośredniczy w wywołaniach do OpenAI, Anthropic lub dowolnego innego zewnętrznego API, jawnie audytuj nagłówki swoich odpowiedzi. Użyj narzędzia takiego jak curl -v lub narzędzi deweloperskich przeglądarki, aby sprawdzić każdy nagłówek zwracany przez każdy punkt końcowy API. Nagłówki są łatwe do przeoczenia właśnie dlatego, że przez większość czasu są nieciekawe — co czyni je tak skutecznym miejscem ukrycia wycieku.


Kontekst aplikacji YC

Założyciel przygotowywał aplikację YC w momencie skanowania. Połączenie niewyjaśnionych skoków w rozliczeniach, ujawnionego klucza API i luki IDOR wpływającej na historię rozmów wszystkich użytkowników byłoby znaczącym problemem do wyjaśnienia inwestorom — lub, co gorsza, do odkrycia po uzyskaniu finansowania.

Problemy bezpieczeństwa na etapie przedpremierowym lub wczesnego rozwoju można naprawić w ciągu godzin. Te same problemy odkryte po incydencie bezpieczeństwa, powiadomieniu o naruszeniu danych lub wrogiej historii medialnej wymagają miesięcy na odzyskanie i mogą zakończyć działalność firmy, która nie zbudowała jeszcze wystarczającej reputacji, aby przetrwać cykl medialny.

Założyciel ponownie uruchomił Penetrify przed złożeniem aplikacji YC. Raport wrócił czysty.

Frequently Asked Questions

Jakie typy podatności wykrywa Penetrify?

Penetrify wykrywa wszystkie kategorie podatności OWASP Top 10, w tym SQL injection, XSS, CSRF, IDOR, złamaną autentykację, błędne konfiguracje zabezpieczeń i ujawnianie wrażliwych danych. Testuje również bezpieczeństwo API, zarządzanie sesją oraz typowe błędy konfiguracji w Supabase, Firebase i Bubble.

Jak długo trwa test penetracyjny AI?

Szybkie skanowanie kończy się w 15–30 minut. Standardowe skanowanie trwa 1–2 godziny z szerszym zakresem. Głęboke skanowanie może trwać kilka godzin dla złożonych aplikacji.

Co zawiera raport Penetrify?

Każdy raport zawiera podsumowanie wykonawcze, ogólny wynik bezpieczeństwa, znaleziska sklasyfikowane według wagi (Krytyczne, Wysokie, Średnie, Niskie), szczegółowe kroki reprodukcji oraz konkretne wskazówki dotyczące naprawy napisane dla deweloperów – nie dla specjalistów ds. zgodności.

Related articles

Błędna konfiguracja Supabase RLS: Jak brakująca polityka ujawniła profile wszystkich użytkowników
Samodzielny założyciel wdrożył SaaS oparty na Next.js i Supabase dla ponad 200 użytkowników. Po ośmiu minutach skanowania Penetrify znaleźliśmy krytyczną błędną konfigurację RLS, która pozwalała każdemu uwierzytelnionemu użytkownikowi odczytać dane profilowe każdego innego użytkownika. Oto dokładnie, co było nie tak, dlaczego tak się dzieje i jak zostało to naprawione w mniej niż dwie godziny.
Klucz tajny Stripe w pakiecie frontendowym: 4 miesiące cichej ekspozycji
Dwuosobowy zespół stworzył marketplace na platformie Bubble.io, który przetwarzał płatności o wartości ponad 40 tys. dolarów. Ich tajny klucz API Stripe znajdował się w klienckim pakiecie JavaScript przez cztery miesiące — co dawało każdemu, kto go znalazł, pełny dostęp do odczytu i zapisu w całej ich infrastrukturze płatniczej. Oto jak do tego doszło, co było zagrożone i co z tym zrobili.
Czym jest Penetration Testing? Wprowadzenie do Ethical Hacking dla początkujących
Włożyłeś niezliczone godziny w budowę swojej aplikacji, ale wciąż dręczy Cię pytanie: czy jest ona naprawdę bezpieczna? W świecie nieustannych zagrożeń cyfrowych, liczenie na szczęście to żadna strategia. Jedyny sposób, aby się o tym przekonać, to przetestować swoje zabezpieczenia, myśląc jak atakujący… Możesz to zrobić, na przykład, poprzez Penetration Testing. Alternatywnie, warto wdrożyć zasady DevSecOps i uwzględniać zalecenia OWASP w swoim procesie CI/CD.

Explore more