Documentazione
/

Risoluzione dei problemi

/

Offuscamento VM con eval e new Function

Offuscamento VM con eval e new Function

Come l'offuscamento VM gestisce la costruzione dinamica del codice (eval diretto e costruttore Function), cosa viene convertito in bytecode e cosa viene escluso, quali avvisi emette l'offuscatore e come diagnosticare un ReferenceError durante l'esecuzione.

Perché è importante

L'offuscamento VM compila i corpi delle funzioni in bytecode eseguito da un interprete incluso nel runtime. Anche gli identificatori dello scope circostante vengono rinominati. Entrambe le trasformazioni interagiscono male con il codice costruito a partire da una stringa durante l'esecuzione: eval(s), new Function(...s) e Function(...s). Se il codice costruito durante l'esecuzione fa riferimento a un identificatore che l'offuscatore ha rinominato, ottieni Uncaught ReferenceError: <renamed-name> is not defined la prima volta che la funzione generata viene eseguita.

L'offuscatore gestisce ogni schema in modo diverso. La tabella seguente è la versione breve; il resto della pagina spiega ogni riga.

Cosa fa l'offuscatore, in sintesi

Schema nel tuo sorgenteCosa succede
eval('literal string') (il corpo è un letterale stringa)Funziona correttamente. La funzione che contiene questa chiamata, e ogni funzione definita al suo interno, non viene convertita in bytecode della VM (l'eval diretto legge le variabili locali circostanti, che la VM non preserva una volta che una funzione è compilata in bytecode).
eval(dynamicExpression)Può fallire durante l'esecuzione con un ReferenceError. Anche la funzione che contiene questa chiamata, e ogni funzione definita al suo interno, non viene convertita in bytecode della VM.
(0, eval)(s) / window.eval(s) (indiretto)La funzione che contiene questa chiamata viene convertita normalmente in bytecode della VM. L'eval indiretto viene eseguito nello scope globale e non vede le variabili locali circostanti, quindi le variabili locali rinominate non possono romperlo. Può comunque fallire se il codice valutato fa riferimento a una variabile globale rinominata o rimossa.
new Function('a', 'b', 'return a + b') (tutti gli argomenti sono letterali stringa)Funziona correttamente. La funzione che contiene questa chiamata viene convertita normalmente in bytecode della VM.
new Function(dynamicBody) / Function(dynamicBody)Può fallire durante l'esecuzione con un ReferenceError. Anche la funzione che contiene questa chiamata, e ogni funzione definita al suo interno, non viene convertita in bytecode della VM.

Il compilatore rileva inoltre in modo conservativo eval?.(code) e lo gestisce come un eval diretto. JavaScript definisce questa forma di chiamata opzionale come eval indiretto; il rilevamento del compilatore non cambia il comportamento del linguaggio.

Solo alcuni di questi schemi producono avvisi non bloccanti, e solo quando la chiamata si trova all'interno di una funzione: una chiamata dinamica a eval, new Function o Function segnala sia DynamicCodeRenameRisk sia VMDynamicCodeSkipped, un eval('...') statico segnala solo VMDynamicCodeSkipped, mentre l'eval indiretto e un new Function completamente statico non segnalano nulla. Una chiamata dinamica al livello superiore di un file, fuori da qualsiasi funzione, non viene mai segnalata. Consulta Rilevare il problema prima dell'esecuzione più avanti per i tipi di avviso e un esempio per la CI.

L'esclusione si propaga attraverso le IIFE. Se una IIFE di primo livello racchiude l'intero bundle e una qualsiasi funzione al suo interno usa un eval o un new Function dinamico, l'intera IIFE viene esclusa dalla conversione in bytecode della VM. Consulta Comportamento di eval diretto per lo schema di rimozione della IIFE che limita la portata del danno.

Perché statico e dinamico vengono trattati diversamente

eval(s) può leggere e scrivere le variabili locali della funzione in cui viene chiamato. Quando s è un letterale stringa, l'offuscatore può analizzare il corpo in fase di offuscamento e rinominare gli identificatori in modo coerente con il codice circostante. Quando s è un'espressione dinamica, l'analisi avviene solo durante l'esecuzione, quando gli identificatori sono già stati rinominati, quindi il sorgente costruito durante l'esecuzione fa riferimento a vecchi nomi che non esistono più.

new Function(s) e l'eval indiretto funzionano diversamente: il corpo viene sempre eseguito nello scope globale, con accesso solo alle variabili globali e mai a quelle locali attorno alla chiamata. Non possono dipendere da variabili locali rinominate, ma possono comunque fallire se fanno riferimento a variabili globali rinominate o rimosse, oppure se costruisci il corpo concatenandovi un identificatore rinominato (ad esempio tramite func.toString() di una funzione il cui contenuto è stato riscritto dall'offuscatore).

Un corpo statico che usa solo i propri parametri, come new Function('a', 'b', 'return a + b'), non ha questa dipendenza: il corpo è una semplice stringa che il meccanismo di rinomina non esamina mai. L'offuscatore lascia la chiamata così com'è e la funzione circostante resta idonea alla conversione in bytecode della VM. Cambiare soltanto la sintassi dell'eval non rende sicuro un codice dinamico arbitrario: controlla gli avvisi e testa il bundle finale.

L'errore che vedi durante l'esecuzione

Il sintomo tipico è un ReferenceError la prima volta che viene eseguita la funzione costruita dinamicamente:

Text

Qui TU è un identificatore rinominato che l'offuscatore ha introdotto nello scope della IIFE del bundle. La chiamata dinamica a eval / costruttore Function valuta un corpo che vi fa riferimento, ma il corpo viene eseguito in uno scope in cui TU non è definito.

Rilevare il problema prima dell'esecuzione

L'offuscatore emette avvisi non bloccanti tramite l'API, così puoi individuare alcuni di questi schemi in CI prima della distribuzione. Due tipi di avviso sono rilevanti, ed entrambi vengono segnalati solo per le chiamate all'interno di una funzione:

  • DynamicCodeRenameRisk - una funzione costruisce codice a partire da una stringa durante l'esecuzione: un eval diretto o una chiamata new Function / Function il cui corpo non è statico, oppure fn.toString() iniettato in uno <script> o in un Worker. È l'avviso che preannuncia un ReferenceError.
  • VMDynamicCodeSkipped - la conversione in bytecode della VM è stata saltata per una funzione, e per ogni funzione definita al suo interno, perché contiene un eval diretto o una chiamata dinamica new Function / Function. Scatta anche per un eval('literal') statico e sicuro, quindi segnala una perdita di protezione VM piuttosto che un crash durante l'esecuzione. Include il nome della funzione (se disponibile) e il costrutto che ha causato l'esclusione.

Il pacchetto npm javascript-obfuscator non espone gli avvisi nel suo risultato, quindi un controllo in CI li legge dalla risposta dell'API: i messaggi result e chunk_end contengono un array warnings. L'esempio seguente usa readObfuscationResponse(), il lettore dello stream presente nel Riferimento API, e fa fallire la build solo per DynamicCodeRenameRisk; aggiungi VMDynamicCodeSkipped al filtro se anche la perdita della protezione VM su una funzione deve bloccare il rilascio.

Code

Se un tipo di avviso è previsto nella tua build e preferisci silenziarlo alla fonte invece di filtrarlo in CI, l'opzione warnings (v7.8.0+) controlla quali avvisi vengono emessi: 'none' sopprime tutto, mentre una mappa per tipo come { VMDynamicCodeSkipped: false } silenzia un solo tipo mantenendo gli altri.

Soluzioni alternative

  • Passa all'eval indiretto ((0, eval)(s))

    Utile solo nel caso di eval. L'eval indiretto viene eseguito nello scope globale, quindi non vede le variabili locali circostanti e, per lo stesso motivo, non può nemmeno fare riferimento a variabili locali rinominate. La funzione che contiene la chiamata resta convertita in bytecode della VM. Il codice valutato non deve comunque dipendere da variabili globali rinominate dall'offuscatore.

  • Rendi il corpo completamente statico

    Per new Function, se puoi esprimere il corpo come un unico letterale stringa o template literal senza interpolazioni, la chiamata non comporta rischi di rinomina e la funzione che la contiene viene comunque convertita in bytecode. new Function('a', 'b', 'return a + b') va bene; new Function('return ' + expr) no.

  • Sposta la chiamata in una funzione di primo livello dedicata ed elimina la IIFE

    Ogni funzione di primo livello viene verificata in modo indipendente. Spostare la chiamata con codice dinamico in una propria funzione di primo livello significa che solo quella funzione perde la conversione in bytecode della VM, invece di propagare l'esclusione a una IIFE di primo livello che racchiude l'intero bundle.

  • Passa a vmTargetFunctionsMode: 'comment'

    Modalità su richiesta: vengono convertite in bytecode solo le funzioni contrassegnate con /* javascript-obfuscator:vm */. Non annotare la funzione che contiene la chiamata con codice dinamico e contrassegna le altre. Conserva questi commenti fino alla fase di offuscamento. Consulta Targeting delle funzioni.

  • Forza l'inclusione con vmForceCompileDynamicCode: true (v6.14.0+)

    Via di fuga di ultima istanza. Quando è attiva, l'offuscatore converte comunque in bytecode la funzione circostante e sopprime l'avviso VMDynamicCodeSkipped. Non può riparare le dipendenze dallo scope o dagli identificatori rinominati: usala solo quando puoi garantire che il corpo costruito durante l'esecuzione non faccia mai riferimento a un identificatore rinominato dall'offuscatore, altrimenti scambi un offuscamento pulito con un ReferenceError durante l'esecuzione. DynamicCodeRenameRisk viene comunque emesso, così la CI può continuare a usarlo come controllo bloccante. Nella dashboard corrisponde all'interruttore "Force Compile Dynamic Code" nel gruppo Override della sezione VM.

Pagine correlate