Vai al contenuto

Basi della sicurezza

Un endpoint API aperto è un problema di sicurezza? Leggi il JSON

La tua scansione ha segnalato un endpoint API aperto. Quanto conti dipende da che cosa è tornato, e la maggior parte di quelli trovati era della piattaforma.

Vlad Tkachenko12 min di lettura
Due sportelli identici in un muro, da ciascuno spunta un foglio. Uno porta una fila di interruttori, l'altro una colonna di volti.

In breve

  • Un endpoint API aperto significa che un percorso scritto nel codice della tua app ha risposto a una richiesta senza login, e che quello che è tornato era JSON.
  • Di quelli che abbiamo trovato su 30.926 app, sette su dieci li ha messi lì la piattaforma su cui l'app è stata pubblicata, e restituiscono impostazioni.
  • La domanda che decide è se qualcosa nella risposta descrive una persona. Un listino prezzi è pubblico apposta. Un elenco dei tuoi clienti non lo è mai stato.

La tua scansione è tornata con una riga che non si legge come un verdetto: alcuni endpoint API rispondono senza login. Magari qualcuno te l'ha detto in modo più diretto, parlando di un endpoint API aperto. Sotto c'è un indirizzo, ed è piuttosto probabile che tu non lo riconosca.

Ecco la parte che guida dopo guida racconta male: il consiglio abituale per questo rilievo è mettere un login davanti al tuo endpoint, e nella maggior parte delle app che abbiamo scansionato non c'è nessun endpoint tuo davanti a cui metterlo. Sette su dieci di quelli che abbiamo trovato appartengono alla piattaforma su cui l'app è stata pubblicata. Leggere questo rilievo comincia quindi dal capire di chi è l'indirizzo, e la risposta di solito si vede già nella prima riga di quello che torna.

Che cosa significa un endpoint API aperto nel tuo report

Significa che un percorso scritto nel codice della tua app ha risposto a una richiesta che non portava alcun login, e che quello che è tornato era JSON.

Tre passaggi, e nessuno di questi è astuto. Carichiamo la tua app in un browser come fa un visitatore, e leggiamo il codice che quel browser ha scaricato. Lì dentro ci sono gli indirizzi che la tua app chiama quando le servono dei dati. Poi chiediamo dati a ciascuno, senza account, senza cookie e senza chiave. Se un indirizzo risponde con JSON che non è un messaggio di errore, finisce nel report.

Questo registra che cosa è successo. Non è un giudizio sui dati, perché niente che stia fuori dalla tua app può capire se un elenco di cose è un elenco di cose pubbliche. Per questo il rilievo è formulato così: questi endpoint hanno restituito dati senza alcuna autenticazione, può essere voluto per dati pubblici, controlla che nessuno esponga informazioni private.

Gli indirizzi escono dal tuo stesso codice, e a ciascuno si chiede senza allegare nulla. Quello che risponde è quello che finisce nel report.

Il report ti consegna quindi un elenco di indirizzi da aprire. Ognuno di loro va aperto in un browser disconnesso, e sono le prime righe della risposta a chiudere la questione.

Ogni endpoint API aperto è un problema di sicurezza?

No. Ce ne sono tre tipi, e solo il terzo è un problema.

Pensa a un condominio. Certe cose lì dentro sono fatte per chiunque entri: gli orari accanto alla porta, le uscite di sicurezza, l'avviso della manutenzione dell'ascensore di martedì. Da qualche altra parte nello stesso palazzo c'è uno schedario con i dati degli inquilini. Dal corridoio, una bacheca e uno schedario non chiuso a chiave sono entrambi mobili che si possono aprire, e l'unico modo per distinguerli è leggere che cosa c'è scritto sulla carta.

Che cosa ha rispostoDi chi è l'indirizzoA che punto sei
Impostazioni della piattaforma: avvisi cookie, interruttoriDel tuo builder, in ogni app che pubblicaNiente da fare
I tuoi dati pubblici: un listino prezzi, un articoloTuo, e fatto per essere lettoNiente da fare
Righe su persone: nomi, email, ordini, messaggiTuo, e raggiungibile da chiunque abbia l'indirizzoChiudilo oggi

La prima riga è una bacheca che ha avvitato al muro qualcun altro. La seconda è una bacheca che hai appeso tu apposta. La terza è lo schedario, ed è aperto da tanto quanto esiste l'indirizzo.

Come controllare che cosa restituiscono i tuoi endpoint

Aprili uno a uno da disconnesso e leggi quello che arriva. La lettura richiede più tempo di quanto il rilievo lasci intendere, ed è l'unico passaggio che risponde alla domanda.

Trova gli indirizzi. Apri la tua app in produzione, premi F12 per far comparire gli strumenti per sviluppatori del browser, clicca su Rete e ricarica la pagina. Scorri le richieste che tornano come JSON. Quelli sono gli indirizzi da cui la tua app prende i dati, e l'elenco del report ne è un sottoinsieme.

Interroga ciascuno da sconosciuto. Copia una URL, apri una finestra di navigazione in incognito così da essere disconnesso, e incollala. Quello che vedi è esattamente quello che a quell'indirizzo vede chiunque su internet.

Leggi le prime righe. Tre cose possono tornare:

  • Un errore, un rifiuto, oppure []. L'indirizzo vuole un login, oppure non c'è niente lì dentro per un chiamante disconnesso. Vai avanti.
  • Impostazioni. Flag, interruttori, un colore, l'elenco delle funzioni attive, un numero di versione. Nessuno viene nominato e niente viene descritto. Vai avanti.
  • Righe. Un indirizzo email, il nome di una persona, il totale di un ordine, il testo di un messaggio, un numero di telefono. Fermati qui, è questo.

Se trovi delle righe, prova a cambiare un numero nella URL prima di chiudere la scheda. Un indirizzo che consegna la scheda di una persona su id=1 e quella di un'altra su id=2 le sta consegnando tutte, e vale la pena saperlo prima di decidere quanto sia urgente.

La nostra scansione gratuita fa i primi due passaggi al posto tuo: legge nel codice della tua app gli indirizzi che chiama, interroga ciascuno senza login, ed elenca quelli che hanno risposto con dei dati. Dura una ventina di secondi e non richiede alcun account: scansiona la tua app. Il terzo passaggio è tuo, perché sei l'unica persona che sa se un nome in quell'elenco è un cliente vero.

La maggior parte di quelli che abbiamo trovato è della piattaforma

Nella nostra rilevazione di agosto 2026, 3.852 delle 30.926 app in cui questo controllo è riuscito a ottenere una risposta avevano almeno un indirizzo che risponde senza login. Di queste, 2.705 erano app Base44, e su Base44 l'indirizzo è quasi sempre lo stesso.

È /api/consent/config, le impostazioni dei cookie della piattaforma. A settembre abbiamo riaperto un campione di app Base44 segnalate e l'unico indirizzo che ha risposto era quello. Restituisce impostazioni, è identico da un'app all'altra, e lì dentro non ci sono i dati di nessuno. Lo scanner lo sta riportando correttamente e il proprietario non ha niente da correggere.

PiattaformaApp con il rilievoApp in cui il controllo ha risposto
Base442.7055.419
Domini propri82955
Bolt41.120
Lovable818.518
v001.786

Una piattaforma manca da quella tabella apposta. A Replit va un blocco grosso dei rilievi rimanenti, e nessuno ne ha riaperto un campione come abbiamo fatto con quelli di Base44, quindi non sappiamo ancora di chi siano quegli indirizzi. Pubblicare un numero come problema del proprietario prima che qualcuno l'abbia guardato è esattamente il modo in cui un'impostazione di piattaforma di Base44 diventa una statistica sui proprietari negligenti. Ogni numero qui sopra viene dalla nostra scansione di 30.998 app vibe-coded in produzione, che pubblica ciascuno insieme alla base su cui è stato misurato.

Leggi insieme i due estremi di quella tabella, perché la distanza è la parte utile. Su Lovable sono 8 app su 18.518, e su v0 nessuna. Quelle app non hanno un piccolo server tutto loro: la pagina parla con Supabase direttamente dal browser, quindi non c'è da nessuna parte un /api/… tuo che questo controllo possa trovare. Quello che protegge i dati in un'app così sono le regole su ciascuna tabella, ed è un rilievo diverso con un suo modo di andare storto.

Quando il JSON sono impostazioni e quando sono persone

Una domanda decide: c'è qualcosa nella risposta che descrive una persona?

Le impostazioni descrivono la tua app. Un flag che dice se la modalità scura è attiva, l'elenco delle valute che accetti, la versione di qualcosa, il testo di un banner dei cookie. Pubblicare quello rivela come è configurata la tua app, e la tua app gira comunque davanti a tutti.

Le righe descrivono i tuoi utenti. Un nome, un indirizzo email, che cosa ha comprato qualcuno, che cosa ha scritto, dove abita. Quella non è una frase sulla configurazione, è il contenuto dello schedario, e conta per quanto è ordinario portarselo via. Non c'è nessuna effrazione. L'indirizzo è una riga di testo che funziona in qualsiasi browser, quindi finisce incollato in una chat di gruppo, salvato negli appunti di qualcuno, e scaricato a intervalli da uno script che nessuno sta guardando.

Tutte e due sono un 200 e un blocco di JSON, ed è tutto quello che il controllo vede. La differenza fra le due è l'intera domanda, e puoi rispondere leggendo.

Chiuderne uno, e perché rinominare il percorso non lo chiude

Chiedi un login all'indirizzo, e rispondi 401 a chi chiama senza averlo.

La correzione è tutta qui, e va all'indirizzo e da nessun'altra parte. Tre cose che sembrano la correzione e non lo sono:

  • Rinominare il percorso, o toglierlo dal codice. Di questa bisogna diffidare, perché il rilievo sparisce. Chi aveva già la URL ce l'ha ancora, sta nelle cronologie dei browser e nei log del server, e lo schedario è ancora aperto, solo senza l'etichetta davanti.
  • Restringere l'header CORS. Quello decide quali altri siti possono leggere una risposta dentro al browser di qualcuno, e nient'altro lo consulta. Uno script o un terminale legge lo stesso indirizzo in entrambi i casi, ed è per questo che il rilievo del wildcard è un'altra domanda.
  • Filtrare nell'app. Se la pagina nasconde le righe che non dovrebbe mostrare, l'indirizzo continua a mandarle. Il filtro deve avvenire prima che la risposta esca.

C'è una quarta opzione, e su una pagina pubblica è spesso la migliore: smettere di avere l'endpoint. Se i dati servono solo alla tua pagina, la pagina può riceverli mentre viene costruita, e l'indirizzo esce dal tuo codice insieme a loro.

Che aspetto ha la correzione nel codice

Due cose, nell'handler che risponde all'indirizzo: capire chi sta chiedendo, e fermarsi se la risposta è nessuno.

Forse non scriverai mai queste righe di persona, e va benissimo. Leggile lo stesso, perché sono quello con cui verifichi che il tuo builder abbia davvero fatto ciò che gli hai chiesto. Questa è la forma di prima, quella che la scansione ha trovato:

app.get('/api/orders', async (req, res) => {
  const orders = await db.orders.findMany()
  res.json(orders)
})

E dopo:

app.get('/api/orders', async (req, res) => {
  const user = await getUserFromRequest(req)
  if (!user) {
    return res.status(401).json({ error: 'Not signed in' })
  }

  const orders = await db.orders.findMany({ where: { userId: user.id } })
  res.json(orders)
})

Sono cambiate due cose e contano entrambe. Il 401 è la porta. Il where è il motivo per cui la porta vale la pena: senza, un visitatore autenticato può leggere gli ordini di tutti gli altri clienti, che è un pubblico più piccolo per gli stessi dati e resta comunque quello sbagliato.

Sulle Edge Function di Supabase, leggi la configurazione prima di scrivere qualsiasi cosa. Le Edge Function controllano da sole il token di chi chiama: la documentazione di configurazione di Supabase mette verify_jwt a true di default, quindi una funzione che risponde agli sconosciuti è una funzione in cui qualcuno l'ha disattivato, con una riga in supabase/config.toml oppure pubblicando con --no-verify-jwt:

[functions.orders]
verify_jwt = false

Se la tua funzione deve essere privata, cancella quelle due righe e ripubblica. Lasciale solo dove l'endpoint deve davvero rispondere a chiunque, che è il caso che la documentazione di Supabase stessa prende come esempio: un webhook di pagamento. Dentro la funzione, l'SDK attuale ti dà un client già limitato alla persona che ha chiamato:

import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
    const { data } = await ctx.supabase.from('orders').select()
    return Response.json(data)
  }),
}

ctx.supabase gira come chi ha chiamato, quindi sono le tue regole di sicurezza a livello di riga a decidere che cosa torna, cioè la stessa protezione su cui si appoggia il resto della tua app e la cosa che vale la pena controllare a parte.

Il prompt da incollare nel tuo builder

Se costruisci in Lovable, Bolt, Cursor, Replit o v0, dagli questo. Lascialo in inglese, in qualunque lingua tu stia leggendo, perché è la lingua in cui questi strumenti ragionano, e sostituisci le due parti fra parentesi quadre con quello che dice il tuo report:

A security scan of [my-app.com] found these API endpoints answer
without any authentication: [/api/orders, /api/users].

Please look at each one and decide whether it is meant to be public.
For the ones that are not, require a valid session or API token and
return 401 otherwise. Make sure each one returns only the rows that
belong to the person asking, not the whole table filtered in the UI.
For any that must stay open, make sure they return only non-sensitive
data and add rate limiting so they cannot be abused.

Tell me what you changed for each endpoint, and do not rename or
remove the paths.

Quell'ultima frase è lì apposta. Quando gli si chiede di far sparire un rilievo, un modello a volte prende la strada più corta e sposta l'indirizzo, che è l'unico risultato che alla scansione successiva sembra un successo e non cambia niente.

Che cosa fare adesso

Cosa fare

  • Apri ogni indirizzo segnalato in una finestra di navigazione in incognito e leggi quello che torna. È il passaggio che il report non può fare al posto tuo.
  • Se la risposta sono impostazioni e l'indirizzo è uno che non hai mai scritto, è del tuo builder e non c'è niente da correggere. Cerca il percorso nella documentazione della tua piattaforma prima di spenderci qualcosa.
  • Se la risposta contiene un nome, una email o un ordine, chiedi oggi stesso un login a quell'indirizzo e restituisci 401 a chi non ce l'ha.
  • Non rinominare il percorso e non cancellarne il riferimento. Il rilievo sparisce e l'indirizzo continua a rispondere.
  • Prova a cambiare un id nella URL sulla tua app. Un indirizzo che serve una scheda a uno sconosciuto di solito le serve tutte.

Gli indirizzi di questo elenco li ha scritti qualcuno che allora sapeva che cosa ci fosse dietro, e quello che cambia è proprio quello che c'è dietro. Reeve Care ripassa questa scansione sulla tua app in produzione a intervalli regolari e ti manda una email quando un risultato peggiora, che è esattamente il caso che il giro da disconnesso qui sopra non può prendere: che cosa sorveglia e quanto costa.

Se preferisci sistemare tutto in una volta sola, la checklist di sicurezza in 10 minuti copre questo insieme al resto di ciò che un'app appena lanciata tende a lasciare aperto, e quali chiavi sono sicure nel frontend è l'altro rilievo con cui la gente arriva di solito.

FAQ

Che cosa significa "endpoint API aperto" nel mio report?

Significa che abbiamo letto il codice che la tua app manda a un browser, ci abbiamo trovato dentro un indirizzo a cui la tua app chiede dati, e a quell'indirizzo abbiamo chiesto dati senza account e senza login. Ha risposto, e la risposta era JSON. Il rilievo è tutto qui. Registra che cosa è successo, e non giudica se i dati fossero privati, perché niente fuori dalla tua app può saperlo.

Ogni endpoint API aperto è un problema di sicurezza?

No. Molti indirizzi sono fatti per rispondere a chiunque: un listino prezzi pubblicato, un articolo, l'elenco delle funzioni attive. Il problema è l'indirizzo che restituisce righe che appartengono a delle persone, perché lo fa per chiunque abbia la URL. Leggi quello che è tornato e chiediti se qualcosa lì dentro descrive una persona.

Il report segnala un endpoint che non ho scritto io. Che cos'è?

Di solito è del tuo builder. Le piattaforme che danno alla tua app un piccolo server tutto suo ci infilano anche qualche indirizzo loro, e quelli stanno nel codice di ogni app della piattaforma. L'esempio più netto che abbiamo misurato sono le impostazioni dei cookie di Base44 su /api/consent/config, che valgono 2.705 dei 3.852 rilievi della nostra rilevazione di agosto 2026. Restituiscono impostazioni, sono identiche su ogni app Base44, e lì dentro non c'è niente che ti appartenga.

Come controllo che cosa restituiscono gli endpoint della mia app?

Apri la tua app in produzione, premi F12, clicca su Rete e ricarica. Le richieste che tornano come JSON sono gli indirizzi da cui la tua app prende i dati. Copia ogni URL, apri una finestra di navigazione in incognito così da essere disconnesso, e incollale una alla volta. Leggi quello che arriva. Un errore o una lista vuota vanno benissimo. Le righe con nomi, email o totali di ordini sono leggibili da chiunque abbia quell'indirizzo.

Nascondere il percorso lo risolve?

No. Rinominare l'indirizzo, o togliere dal codice il riferimento, impedisce a uno scanner di trovarlo e lo lascia rispondere esattamente come prima. Chi aveva già la URL ce l'ha ancora, e resta nelle cronologie dei browser, nei log del server e in tutto ciò che ha scansionato il tuo sito. Si risolve all'indirizzo: chiedere un login e rispondere 401 a uno sconosciuto.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Tutti gli articoli

Non sai come sta messa la tua app?

Esegui una scansione gratuita e ottieni un voto chiaro da A a F in una ventina di secondi. Senza account e senza carta.

Scansiona la tua app gratis

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.