Fakturační panel OpenAI ukazoval poplatky, které si zakladatel nedokázal vysvětlit. Aplikace měla zhruba 800 uživatelů na freemium modelu, ale využití API směřovalo k 2 000 $ měsíčně — mnohem více, než by odpovídala uživatelská aktivita na platformě. Zakladatel předpokládal, že někde mají neefektivní prompt, a poznamenal si, že to prošetří.
Sedm minut po zahájení skenu Penetrify se důvod objasnil: klíč OpenAI API byl předáván zpět uživatelům v hlavičkách HTTP odpovědí každého proxy API volání. 800 uživatelů ho vidělo. Někteří z nich ho používali.
Architektura: Odkud únik pocházel
Aplikace měla backend FastAPI obsluhující frontend React. Její hlavní funkcí bylo proxyování uživatelských požadavků na OpenAI API, přidávání vlastních systémových promptů, ukládání historie konverzací a aplikování vlastní vrstvy prompt engineeringu zakladatele. Toto je běžný vzor pro produkty typu AI wrapper — hodnota není v modelu, ale v produktu, který je kolem něj postaven.
Jak aplikace fungovala:
- Uživatel odešle prompt z React frontendu
- Frontend ho odešle na
POST /api/generate - Handler FastAPI přidá systémový prompt a volá OpenAI API
- FastAPI vrátí dokončení (completion) frontendu
Někde v implementaci routy FastAPI byla hlavička Authorization z odchozího požadavku OpenAI — obsahující API klíč ve formátu Bearer tokenu — předávána zpět v odpovědi. Jedná se o specifickou třídu chyby předávání hlaviček: aplikace předávala hlavičky odpovědí z upstream volání OpenAI API namísto vytváření vlastních hlaviček odpovědí.
Hlavičky odpovědí u každého volání /api/generate zahrnovaly:
HTTP/1.1 200 OK
Content-Type: application/json
Authorization: Bearer sk-proj-...[OpenAI API key]
...
{"completion": "..."}
Každý uživatel, který kdy použil funkci generování — všech 800 — obdržel API klíč v hlavičkách odpovědí svých požadavků. Byl viditelný v záložce Síť (Network) v DevTools prohlížeče, v jakémkoli HTTP proxy a v jakémkoli programovém klientovi, který četl hlavičky odpovědí.
Co vám dává OpenAI API klíč
OpenAI API klíč bez omezení použití poskytuje držiteli plný přístup k API kvótě příslušného účtu. To znamená:
- Neomezený přístup k modelům na náklady majitele klíče — GPT-4o, o1, o3, generování obrázků, embeddings, fine-tuning
- Žádný limit na požadavek, dokud není dosaženo měsíčního limitu útraty účtu
- Přístup k jakýmkoli fine-tuned modelům, které účet vytvořil
- Možnost číst uložené soubory, pokud účet používá Files API
Pro individuálního zakladatele, jehož aplikace zpracovává 200–400 $ měsíčně v legitimním využití, může zneužití jeho klíče externě zvýšit měsíční účet na 2 000 $, 5 000 $ nebo více — v závislosti na tom, jak široce se klíč rozšíří a co útočníci generují.
Nákladový model pro zneužití OpenAI API je asymetrický: útočník neplatí nic, majitel klíče platí za vše.
Nevysvětlitelné nárůsty fakturace, vysvětleno
Jakmile bylo odhaleno zveřejnění klíče, nárůsty fakturace dávaly smysl. Zakladatel si stáhl panel využití OpenAI filtrovaný podle endpointu a času. Vzor nárůstů ukázal požadavky s vysokým objemem, které nekorelovaly s uživatelskou aktivitou na platformě — požadavky ve 3 ráno, požadavky z IP rozsahů, které neodpovídaly žádné známé uživatelské geografii, požadavky na typy modelů, které aplikace nepoužívala.
Někdo extrahoval klíč — možná několik lidí — a používal jej přímo proti OpenAI API, zcela obcházel aplikaci. Požadavky směřovaly přímo na OpenAI s použitím extrahovaných pověření, nikoli přes aplikaci zakladatele.
Klíč byl vystaven přibližně od prvního týdne veřejného spuštění aplikace. V době skenování byl aktivní a unikal po dobu několika měsíců.
Další zjištění
Vystavení klíče OpenAI bylo nejbezprostředněji škodlivým zjištěním, ale byly nahlášeny tři další problémy:
STŘEDNÍ — IDOR na /api/history/:userId
Aplikace ukládala historii konverzací pro každého uživatele a vystavovala ji na předvídatelném koncovém bodě:
GET /api/history/abc123
Obslužná rutina trasy načítala historii konverzací pro ID uživatele v parametru cesty, aniž by kontrolovala, zda požadující uživatel vlastní tyto záznamy. Jakýkoli ověřený uživatel mohl číst historii konverzací jakéhokoli jiného uživatele nahrazením jeho ID. Jelikož konverzace obsahovaly uživatelem zadané výzvy, jednalo se také o narušení soukromí: útočník mohl číst, jaké otázky kladli ostatní uživatelé nástroji AI.
STŘEDNÍ — Režim ladění FastAPI povolen v produkčním prostředí
Aplikace běžela s FastAPI(debug=True). V režimu ladění jakákoli neošetřená výjimka vrací úplný stack trace v HTTP odpovědi, včetně interních cest k souborům, verzí závislostí a názvů proměnných prostředí (i když ne jejich hodnot). Tyto informace jsou přímo užitečné pro plánování dalších útoků — znalost přesné verze FastAPI, verze Pydantic a verze Pythonu významně zužuje seznam použitelných CVEs.
Režim ladění také ve výchozím nastavení umožňuje interaktivní dokumentaci FastAPI na /docs a /redoc, která byla přístupná v produkčním prostředí a dokumentovala každý interní API koncový bod, včetně těch, které nebyly určeny pro uživatelský přístup.
NÍZKÁ — HTTP nepřesměrovává na HTTPS
HTTP verze aplikace poskytovala plný obsah bez přesměrování na HTTPS. Ve veřejných nebo sdílených sítích mohl útočník provádějící útok man-in-the-middle zachytit nešifrované relace a extrahovat tokeny relací, uživatelem zadané výzvy a API odpovědi.
Oprava: Nasazeno tentýž večer
Zakladatel nasadil opravy pro všechna zjištění do tří hodin od obdržení zprávy.
Nejprve obměňte klíč
Než se sáhne na jakýkoli kód, okamžitou akcí bylo zrušit kompromitovaný klíč v ovládacím panelu OpenAI a vygenerovat nový. Tím se okamžitě zastavilo jakékoli probíhající zneužívání. Obměna klíče OpenAI je okamžitá — starý klíč přestane fungovat v okamžiku, kdy jej smažete.
Opravte chybu přeposílání hlaviček
Hlavní příčinou bylo, že trasa FastAPI používala generického HTTP klienta, který přeposílal všechny hlavičky odpovědi z upstream volání OpenAI. Opravou bylo konstruovat explicitní hlavičky odpovědi namísto propouštění těch z upstreamu:
# Před (zranitelné) — přeposílání všech upstream hlaviček
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) # ← toto přeposílá hlavičku Authorization zpět
)
# Po (opravené) — explicitní konstrukce odpovědi
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"]})
# Pouze data, která explicitně chceme vrátit — žádné upstream hlavičky nejsou přeposílány
Opravte IDOR
Koncový bod historie konverzací byl aktualizován tak, aby extrahoval ID uživatele z ověřeného JWT namísto z parametru cesty:
@router.get("/api/history")
async def get_history(current_user: User = Depends(get_current_user)):
# User ID comes from the verified JWT — can't be spoofed
history = await db.get_history(user_id=current_user.id)
return history
Zakázat režim ladění
# In config.py
app = FastAPI(
debug=settings.DEBUG, # reads from environment variable
docs_url=None if not settings.DEBUG else "/docs", # hide docs in production
redoc_url=None if not settings.DEBUG else "/redoc"
)
S nastavením DEBUG=false v produkčním prostředí interaktivní dokumentace a podrobné chybové odpovědi okamžitě zmizely při dalším nasazení.
Přidání limitů využití OpenAI jako bezpečnostní síť
Kromě opravy úniku zakladatel přidal dvě obranná opatření k omezení rozsahu dopadu jakéhokoli budoucího odhalení klíče:
Limity využití: Na řídicím panelu OpenAI v sekci Billing → Usage limits nastavte měsíční pevný limit a měkký notifikační práh. I když dojde k opětovné kompromitaci klíče, schopnost útočníka navyšovat poplatky je omezena.
Vyhrazené klíče pro každou službu: Vytvořte samostatný API klíč pro každou aplikaci nebo prostředí. Pokud dojde ke kompromitaci klíče, můžete rotovat pouze tento klíč, aniž byste narušili ostatní služby, a protokoly využití pro každý klíč jsou čistě odděleny — což výrazně usnadňuje detekci neoprávněného přístupu.
Jak časté to je?
Odhalení API klíče v HTTP odpovědích je méně časté než odhalení v JavaScriptových balíčcích, ale pravidelně se s ním setkáváme konkrétně u aplikací s AI wrappery. Tento vzorec má téměř vždy stejnou hlavní příčinu: vývojář vytvářející proxy vrstvu používá generického HTTP klienta, který přeposílá hlavičky odpovědí, a neprověřuje, co tyto hlavičky obsahují.
Chyba v přeposílání hlaviček se snadno udělá, protože často zjednodušuje implementaci. Proč vytvářet novou odpověď, když můžete přeposlat tu z upstreamu? Odpověď v tomto případě zní, že upstreamová odpověď obsahuje přihlašovací údaje, které nechcete sdílet se svými uživateli.
Pokud vaše aplikace proxyuje volání na OpenAI, Anthropic nebo jakékoli jiné externí API, explicitně prověřte hlavičky svých odpovědí. Použijte nástroj jako curl -v nebo DevTools vašeho prohlížeče k prohlédnutí každé hlavičky vrácené každým API koncovým bodem. Hlavičky se snadno přehlédnou právě proto, že většinu času jsou nezajímavé — což z nich dělá tak efektivní úkryt pro únik.
Kontext žádosti o YC
Zakladatel připravoval žádost o YC v době skenování. Kombinace nevysvětlitelných nárůstů fakturace, odhaleného API klíče a IDOR zranitelnosti ovlivňující historii konverzací všech uživatelů by byla významným problémem k vysvětlení investorům — nebo, což je horší, k objevení po získání financování.
Bezpečnostní problémy ve fázi před spuštěním nebo rané trakce jsou opravitelné během hodin. Stejné problémy objevené po bezpečnostním incidentu, oznámení o úniku dat nebo nepřátelské mediální zprávě trvá měsíce, než se z nich společnost zotaví, a mohou ukončit společnost, která si ještě nevybudovala dobrou pověst, aby přežila mediální cyklus.
Zakladatel znovu spustil Penetrify před odesláním žádosti o YC. Zpráva se vrátila čistá.
