La dashboard di fatturazione di OpenAI mostrava addebiti che il fondatore non riusciva a spiegare. L'applicazione contava circa 800 utenti su un modello freemium, ma l'utilizzo dell'API tendeva a raggiungere i 2.000 dollari al mese — molto più alto di quanto giustificato dall'attività degli utenti sulla piattaforma. Il fondatore ipotizzò che ci fosse un prompt inefficiente da qualche parte e prese nota di indagare.
Sette minuti dopo l'inizio di una scansione Penetrify, il motivo divenne chiaro: la chiave API di OpenAI veniva ritrasmessa agli utenti negli header di risposta HTTP di ogni chiamata API proxata. 800 utenti l'avevano vista. Alcuni la stavano usando.
L'Architettura: Da Dove Proveniva la Fuga
L'applicazione era un backend FastAPI che serviva un frontend React. La sua funzionalità principale era quella di proxyare le richieste degli utenti all'API di OpenAI, aggiungendo prompt di sistema personalizzati, memorizzando la cronologia delle conversazioni e applicando lo strato proprietario di prompt engineering del fondatore. Questo è un modello comune per i prodotti wrapper AI — il valore non è il modello, ma il prodotto costruito attorno ad esso.
Il funzionamento dell'applicazione:
- L'utente invia un prompt dal frontend React
- Il frontend lo invia a
POST /api/generate - Il gestore FastAPI aggiunge il prompt di sistema e chiama l'API di OpenAI
- FastAPI restituisce il completamento al frontend
Da qualche parte nell'implementazione della route FastAPI, l'header Authorization della richiesta OpenAI in uscita — contenente la chiave API in formato token Bearer — veniva ritrasmesso nella risposta. Si tratta di una classe specifica di bug di inoltro degli header: l'applicazione stava inoltrando gli header di risposta dalla chiamata API OpenAI a monte anziché costruire i propri header di risposta.
Gli header di risposta su ogni chiamata a /api/generate includevano:
HTTP/1.1 200 OK
Content-Type: application/json
Authorization: Bearer sk-proj-...[OpenAI API key]
...
{"completion": "..."}
Ogni utente che avesse mai utilizzato la funzione di generazione — tutti gli 800 — aveva ricevuto la chiave API negli header di risposta delle proprie richieste. Era visibile nella scheda Rete degli Strumenti per sviluppatori del browser, in qualsiasi proxy HTTP e in qualsiasi client programmatico che leggesse gli header di risposta.
Cosa Ti Offre una Chiave API OpenAI
Una chiave API OpenAI senza restrizioni d'uso conferisce al titolare pieno accesso alla quota API dell'account corrispondente. Ciò significa:
- Accesso illimitato ai modelli a spese del proprietario della chiave — GPT-4o, o1, o3, image generation, embeddings, fine-tuning
- Nessun limite per richiesta fino al raggiungimento del limite di spesa mensile dell'account
- Accesso a qualsiasi modello fine-tuned che l'account ha creato
- Possibilità di leggere i file memorizzati se l'account utilizza l'API Files
Per un singolo fondatore la cui applicazione elabora 200-400 dollari/mese di utilizzo legittimo, subire un abuso esterno della propria chiave può far salire la fattura mensile a 2.000, 5.000 dollari o più — a seconda di quanto ampiamente la chiave circola e di cosa stanno generando gli abusatori.
Il modello di costo per l'abuso dell'API OpenAI è asimmetrico: l'attaccante non paga nulla, il proprietario della chiave paga per tutto.
I Picchi di Fatturazione Inspiegabili, Spiegati
Una volta identificata l'esposizione della chiave, i picchi di fatturazione ebbero senso. Il fondatore consultò la dashboard di utilizzo di OpenAI filtrata per endpoint e ora. Il modello dei picchi mostrava richieste ad alto volume che non correlavano con l'attività degli utenti sulla piattaforma — richieste alle 3 del mattino, richieste da intervalli IP che non corrispondevano a nessuna geografia utente nota, richieste per tipi di modello che l'applicazione non utilizzava.
Qualcuno aveva estratto la chiave — forse più persone — e la stava usando direttamente contro l'API di OpenAI, bypassando completamente l'applicazione. Le richieste andavano direttamente a OpenAI usando le credenziali estratte, non tramite l'applicazione del fondatore.
La chiave era stata esposta approssimativamente dalla prima settimana del lancio pubblico dell'applicazione. Al momento della scansione, era stata attiva e in perdita per diversi mesi.
Gli Altri Rilievi
L'esposizione della chiave OpenAI è stato il rilievo più immediatamente dannoso, ma sono stati segnalati tre problemi aggiuntivi:
MEDIUM — IDOR su /api/history/:userId
L'applicazione memorizzava la cronologia delle conversazioni per utente e la esponeva a un endpoint prevedibile:
GET /api/history/abc123
Il gestore della rotta recuperava la cronologia delle conversazioni per l'ID utente nel parametro del percorso senza verificare se l'utente richiedente fosse il proprietario di tali record. Qualsiasi utente autenticato poteva leggere la cronologia delle conversazioni di qualsiasi altro utente sostituendo il proprio ID. Poiché le conversazioni includevano prompt forniti dall'utente, si trattava anche di un'esposizione della privacy: un attaccante poteva leggere quali domande altri utenti avevano posto allo strumento AI.
MEDIUM — Modalità debug di FastAPI abilitata in produzione
L'applicazione era in esecuzione con FastAPI(debug=True). In modalità debug, qualsiasi eccezione non gestita restituisce una traccia completa dello stack nella risposta HTTP, inclusi percorsi di file interni, versioni delle dipendenze e nomi delle variabili d'ambiente (anche se non i valori). Queste informazioni sono direttamente utili per pianificare ulteriori attacchi — conoscere la versione esatta di FastAPI, la versione di Pydantic e la versione di Python restringe significativamente l'elenco delle CVE applicabili.
La modalità debug abilita anche la documentazione interattiva di FastAPI su /docs e /redoc per impostazione predefinita, che era accessibile in produzione e documentava ogni endpoint API interno, inclusi quelli non destinati all'accesso degli utenti.
LOW — HTTP non reindirizza a HTTPS
La versione HTTP dell'applicazione serviva il contenuto completo senza reindirizzare a HTTPS. Su reti pubbliche o condivise, un attaccante che esegue un attacco man-in-the-middle potrebbe intercettare sessioni non crittografate ed estrarre token di sessione, prompt inviati dall'utente e risposte API.
La Correzione: Implementata la Stessa Sera
Il fondatore ha implementato le correzioni per tutti i rilievi entro tre ore dalla ricezione del rapporto.
Per prima cosa, ruotare la chiave
Prima di toccare qualsiasi codice, l'azione immediata è stata quella di revocare la chiave compromessa nella dashboard di OpenAI e generarne una nuova. Ciò ha interrotto istantaneamente qualsiasi abuso in corso. La rotazione delle chiavi di OpenAI è immediata — la vecchia chiave smette di funzionare nel momento in cui la si elimina.
Correggere il bug di inoltro degli header
La causa principale era che la rotta FastAPI utilizzava un client HTTP generico che inoltrava tutti gli header di risposta dalla chiamata upstream di OpenAI. La correzione consisteva nel costruire header di risposta espliciti anziché passare quelli upstream:
# Prima (vulnerabile) — inoltro di tutti gli header upstream
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) # ← questo inoltra l'header Authorization indietro
)
# Dopo (corretto) — costruzione esplicita della risposta
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"]})
# Solo i dati che vogliamo esplicitamente restituire — nessun header upstream inoltrato
Correggere l'IDOR
L'endpoint della cronologia delle conversazioni è stato aggiornato per estrarre l'ID utente dal JWT verificato anziché dal parametro del percorso:
@router.get("/api/history")
async def get_history(current_user: User = Depends(get_current_user)):
# L'ID utente proviene dal JWT verificato — non può essere falsificato
history = await db.get_history(user_id=current_user.id)
return history
Disabilitare la modalità debug
# In config.py
app = FastAPI(
debug=settings.DEBUG, # legge dalla variabile d'ambiente
docs_url=None if not settings.DEBUG else "/docs", # nasconde la documentazione in produzione
redoc_url=None if not settings.DEBUG else "/redoc"
)
Con DEBUG=false impostato nell'ambiente di produzione, la documentazione interattiva e le risposte di errore dettagliate sono scomparse immediatamente al successivo deployment.
Aggiungere limiti di utilizzo OpenAI come rete di sicurezza
Oltre a correggere la fuga, il fondatore ha aggiunto due misure difensive per limitare il raggio d'azione di qualsiasi futura esposizione di chiavi:
Limiti di utilizzo: Nella dashboard di OpenAI, sotto Fatturazione → Limiti di utilizzo, imposta un limite massimo mensile e una soglia di notifica soft. Anche se una chiave viene nuovamente compromessa, la capacità dell'attaccante di accumulare addebiti è limitata.
Chiavi dedicate per servizio: Crea una API key separata per ogni applicazione o ambiente. Se una chiave viene compromessa, puoi ruotare solo quella chiave senza interrompere altri servizi, e i log di utilizzo per ogni chiave sono chiaramente separati — rendendo l'accesso non autorizzato molto più facile da rilevare.
Quanto è comune questo?
L'esposizione di API key nelle risposte HTTP è meno comune rispetto all'esposizione nei bundle JavaScript, ma la riscontriamo regolarmente, in particolare nelle applicazioni wrapper AI. Il modello ha quasi sempre la stessa causa principale: uno sviluppatore che costruisce un livello proxy utilizza un client HTTP generico che inoltra le intestazioni di risposta, e non verifica cosa contengono tali intestazioni.
L'errore di inoltro delle intestazioni è facile da commettere perché spesso semplifica l'implementazione. Perché costruire una nuova risposta quando si può inoltrare quella a monte? La risposta, in questo caso, è che la risposta a monte contiene credenziali che non si desidera condividere con i propri utenti.
Se la tua applicazione esegue il proxy delle chiamate a OpenAI, Anthropic o qualsiasi altra API esterna, verifica esplicitamente le tue intestazioni di risposta. Utilizza uno strumento come curl -v o i DevTools del tuo browser per esaminare ogni intestazione restituita da ogni endpoint API. Le intestazioni sono facili da trascurare proprio perché la maggior parte delle volte sono poco interessanti — il che le rende un nascondiglio così efficace per una fuga.
Il contesto della domanda YC
Il fondatore stava preparando una domanda YC al momento della scansione. La combinazione di picchi di fatturazione inspiegabili, una API key esposta e una vulnerabilità IDOR che interessava la cronologia delle conversazioni di tutti gli utenti sarebbe stata un problema significativo da spiegare agli investitori — o, peggio, da scoprire dopo il finanziamento.
I problemi di sicurezza nella fase di pre-lancio o di early traction sono risolvibili in poche ore. Gli stessi problemi scoperti dopo un incidente di sicurezza, una notifica di violazione dei dati o una storia mediatica ostile richiedono mesi per essere risolti e possono porre fine a un'azienda che non ha ancora costruito la buona volontà necessaria per sopravvivere al ciclo di notizie.
Il fondatore ha eseguito Penetrify di nuovo prima di inviare la domanda YC. Il rapporto è risultato pulito.
