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:
- Użytkownik wysyła prompt z frontendu React
- Frontend wysyła go do
POST /api/generate - Handler FastAPI dodaje prompt systemowy i wywołuje API OpenAI
- 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.
