Tutto ciò che gira nel browser è, per costruzione, disponibile al browser — e quindi a chi lo controlla. Non c'è configurazione, licenza o tecnica che cambi questo fatto: se il codice deve essere eseguito sulla macchina di qualcun altro, quella macchina deve poterlo leggere.
Detto questo, la conclusione «allora non c'è niente da fare» è sbagliata. C'è una differenza sostanziale fra ciò che si è deciso di esporre e ciò che si espone per distrazione, e la seconda categoria è più ampia di quanto si creda.
Il perimetro reale#
| Cosa | Proteggibile? | Come |
|---|---|---|
| Logica dell'interfaccia | no | il browser la esegue: leggibile per definizione |
| Sorgenti leggibili e commentati | sì | non pubblicarli: struttura delle cartelle |
| Source map | sì | non generarle in produzione, o tenerle fuori dalla cartella servita |
File di progetto (package.json, .git, .env) | sì | fuori dalla radice pubblica, più una guardia esplicita |
| Chiavi API e segreti | mai, se spediti al client | non spedirli: proxy sul vostro server |
| Logica di business e regole | sì, se sta sul server | il client chiede, il server decide |
| Algoritmi proprietari | solo lato server | un endpoint che restituisce il risultato, non il metodo |
La riga che conta è la penultima. La differenza fra un frontend che è una vetrina della logica e uno che è una superficie di rendering non è una questione di offuscamento: è una decisione di architettura, e determina quanto c'è da copiare.
La difesa strutturale#
// La regola strutturale: i sorgenti non stanno dentro la cartella servita.// Non "sono bloccati": non sono lì. È una differenza importante, perché// una regola si può disattivare per sbaglio, una cartella no.//// src/js, src/css ← sorgenti, mai serviti// public/js-build ← bundle offuscato, servito// public/css-build ← css minificato, servitoconst PUBLIC_DIR = path.join(__dirname, 'public');// La guardia serve al caso in cui la struttura sia giusta ma il SERVER no:// una copia vecchia rimasta su disco dopo un rsync senza --delete, una// cartella ripristinata a mano da un backup, un collega che ha fatto// una prova. Blocca i percorsi storici indipendentemente da cosa c'è lì.app.use(['/js', '/css', '/src'], (req, res) => notFound(res));app.use('/js-build', express.static(path.join(PUBLIC_DIR, 'js-build'), { maxAge: '365d', immutable: true}));app.use(express.static(PUBLIC_DIR, { index: false, dotfiles: 'ignore' }));Il punto è la distinzione fra «bloccato» e «non c'è». Una regola che blocca /src è una riga che qualcuno può cancellare o riordinare; una struttura in cui i sorgenti stanno fuori dalla radice pubblica non ha niente da bloccare. La guardia esplicita resta comunque utile, ma per un motivo diverso: copre il caso in cui il server abbia in mano qualcosa che il repository non ha più — una cartella vecchia sopravvissuta a un rsync senza --delete, un ripristino parziale da backup.
La riga che annulla tutto#
// L'errore che annulla tutto il lavoro, in una riga di configurazione.// La source map è la mappa che riporta il bundle al sorgente originale:// commenti, nomi delle variabili, struttura dei file. Pubblicarla accanto// al bundle significa pubblicare il sorgente con un passaggio in più.esbuild.build({ entryPoints: ['src/js/app.js'], bundle: true, sourcemap: false // ← in produzione: false, o 'external' su un}); // percorso NON servito// Il browser la trova da solo: in fondo al bundle c'è un commento// //# sourceMappingURL=app.js.map// e DevTools la scarica senza che nessuno gliela chieda.Se le source map servono per il monitoraggio degli errori in produzione — ed è un'esigenza legittima — la soluzione non è pubblicarle: è caricarle sul servizio di error tracking, che le usa lato suo per simbolizzare gli stack trace senza esporle a nessuno.
Verificare invece di supporre#
# Cinque secondi per sapere se i sorgenti sono online. curl vede quello# che vede un bot: fa la richiesta e stampa il codice, senza eseguire JS.for p in /js/app.js /src/js/app.js /css/base.css /js-build/app.js.map \> /.git/config /.env /package.json /server.js; do> printf '%-26s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "https://esempio.it$p")"> done/js/app.js 404/src/js/app.js 404/css/base.css 404/js-build/app.js.map 404/.git/config 404/.env 404/package.json 404/server.js 404# Qualunque 200 in questa lista e' un problema, non una curiosita'.La cosa che non si può proteggere#
Qui il malinteso è comune e costoso. L'offuscamento rende il codice illeggibile; non rende i dati irrecuperabili. E una credenziale che il browser deve usare è una credenziale che il browser deve poter ricostruire.
// Il malinteso più costoso: "la chiave è offuscata, quindi è protetta".// L'offuscamento rende il codice illeggibile, non i dati irrecuperabili.// Il browser DEVE poter usare la chiave, quindi la chiave DEVE essere// ricostruibile in memoria — e ci si arriva senza leggere una riga.const CHIAVE = decodifica(_0x4a2f[17]); // sembra protettafetch('https://api.esempio.it/v1/dati', { headers: { 'Authorization': 'Bearer ' + CHIAVE }});# Come si estrae, senza deoffuscare niente: si guarda cosa parte.# Pannello Rete, filtro Fetch/XHR, e la chiave e' nell'intestazione# della richiesta, in chiaro, perche' deve esserlo per funzionare.Request Headers Authorization: Bearer sk_live_9f3c2a...# Oppure, senza nemmeno aprire il pannello: un punto di interruzione# su fetch, e si legge l'argomento.# In console:> const _f = fetch;> window.fetch = (...a) => { console.log(a); return _f(...a); };# Tempo richiesto: trenta secondi.Vale per le chiavi API, per i token di servizio, per le credenziali di un database consultato dal client, per le password «di controllo» confrontate in JavaScript. La regola non ha eccezioni: ciò che arriva al browser è pubblico, indipendentemente da come è stato impacchettato.
La forma corretta: il proxy#
// ✓ La forma corretta: la credenziale non attraversa mai il browser.// Il frontend chiama VOI, voi chiamate il servizio esterno.app.post('/api/traduci', autenticato, limitaRichieste, async (req, res) => { const testo = String(req.body.testo || '').slice(0, 5000); // limite if (!testo) return res.status(400).json({ errore: 'testo mancante' }); const r = await fetch('https://api.esempio.it/v1/traduci', { method: 'POST', headers: { // La chiave sta qui, sul server, in una variabile d'ambiente. 'Authorization': 'Bearer ' + process.env.CHIAVE_TRADUZIONE, 'Content-Type': 'application/json' }, body: JSON.stringify({ testo, lingua: 'it' }) }); if (!r.ok) return res.status(502).json({ errore: 'servizio non disponibile' }); res.json(await r.json());});// Guadagno secondario, spesso più importante del primo: adesso avete un// punto in cui contare, limitare, registrare e — se il fornitore fattura// a consumo — impedire che qualcuno vi spenda il budget in una notte.Il beneficio secondario è spesso quello che convince: adesso esiste un punto in cui contare le chiamate, applicare un limite di frequenza, registrare chi ha usato cosa, e — se il fornitore fattura a consumo — impedire che una chiave finita in giro vi svuoti il budget in una notte. Una chiave nel frontend non ha nessuna di queste proprietà, e non ha nemmeno un modo di essere revocata senza pubblicare una nuova versione del sito.
Quanto costa l'offuscamento, e cosa compra#
// Che cosa fa davvero un offuscatore, e che cosa costa.{ compact: true, controlFlowFlattening: true, // +costo runtime, il più efficace controlFlowFlatteningThreshold: 0.5, deadCodeInjection: true, // +peso del file stringArray: true, // le stringhe non si cercano più stringArrayEncoding: ['base64'], stringArrayThreshold: 0.75, selfDefending: true, // il codice si rompe se riformattato debugProtection: false // ← lasciato a false: vedi sotto}// Il prezzo è reale e va messo in conto:// - il file cresce (nel nostro caso: 17 KB di bundle → 148 KB offuscato);// - il control flow flattening rallenta l'esecuzione;// - gli stack trace degli errori in produzione diventano illeggibili,// quindi il monitoraggio va ripensato;// - debugProtection: true blocca DevTools con un ciclo di debugger, e// rende l'esperienza ostile anche a chi sta solo ispezionando la// propria pagina. È una scelta di postura, non di sicurezza.Vale la pena essere espliciti sul rapporto costi/benefici, perché l'offuscamento viene di solito presentato come gratuito e non lo è.
Cosa compra. Tempo e attrito. Chi voleva copiare una funzione guardando il codice, con l'array di stringhe cifrato e il flusso di controllo appiattito, deve mettersi a lavorare sul serio. Per la stragrande maggioranza di chi guarda — che è curiosità, non spionaggio industriale — è sufficiente a far chiudere la scheda. È una difesa contro la copia opportunistica, e come tale funziona.
Cosa costa. Nel caso di questo sito, un bundle di 17 KB diventa 148 KB: quasi nove volte tanto, e sono byte che ogni visitatore scarica. Il control flow flattening ha un costo di esecuzione. E gli stack trace in produzione diventano illeggibili, il che significa che il monitoraggio degli errori va ripensato — o si accetta di non capire più perché qualcosa si rompe sui browser degli utenti.
Dove finisce la tecnica#
Se il valore da proteggere è la logica — un algoritmo, un modello di calcolo, una regola di business che vi distingue — la risposta non è un offuscatore più aggressivo: è non mandarla al browser. Il client raccoglie l'input, il server calcola, il client mostra il risultato. Quello che viaggia è il risultato, non il metodo.
E se il valore è il lavoro nel suo insieme — il design, la struttura, l'esperienza — la tutela è di natura diversa: il diritto d'autore protegge il codice sorgente come opera dell'ingegno indipendentemente da come è pubblicato, e una licenza esplicita nel repository e una nota nel footer valgono, in caso di copia palese, più di qualunque configurazione di build.