twitterlinkedinmail

Come gestire in sicurezza i dati di input mediante la normalizzazione e la data sanitization

Indice degli argomenti:

  • Normalizzazione delle stringhe e dei percorsi file

  • Encodings dei files e Network I/O

  • Sanitization delle Regular Expressions

  • Verifica dei valori ritornati dai metodi

  • Verifica dei tipi numerici e degli operatori

Scarica le slides

03_input-e-data-sanitization
Scarica le slides

Sottoscrivi la Newsletter
per scaricare le slides:

Normalizzazione delle stringhe

Molte applicazioni che accettano stringhe di input non attendibili utilizzano meccanismi di convalida basati sui caratteri delle stringhe.

Ad esempio, la strategia di un’applicazione per evitare le vulnerabilità di cross-site scripting (XSS) può includere il divieto di tag <script> negli input.

Tali meccanismi di blacklist sono una parte utile di una strategia di sicurezza, anche se non sono sufficienti per la completa convalida e sanificazione degli input.

Una volta implementata, questa forma di convalida deve essere eseguita solo dopo aver normalizzato l’input.

La forma di normalizzazione più adatta per eseguire la convalida dell’input su stringhe codificate arbitrariamente è KC (NFKC) perché la normalizzazione in KC trasforma l’input in una forma canonica equivalente che può essere tranquillamente confrontata con la forma di input richiesta.

L’esempio che segue tenta di convalidare la stringa prima di eseguire la normalizzazione.

Di conseguenza, la logica di convalida non riesce a rilevare gli input che dovrebbero essere scartati, perché il controllo delle parentesi angolari non riesce a individuare rappresentazioni Unicode alternative.

// \uFE64 corrisponde a < mentre \uFE65 a > 
String s = "\uFE64" + "script" + "\uFE65";

Pattern pattern = Pattern.compile("[<>]"); 

Matcher matcher = pattern.matcher(s);

if (matcher.find()) {
   throw new IllegalStateException(); 
} else {
   // ... 
}

s = Normalizer.normalize(s, Form.NFKC);

Il metodo normalize () trasforma il testo Unicode in una forma composta o scomposta equivalente, consentendo una ricerca più semplice del testo.

Il metodo normalize supporta i moduli di normalizzazione standard descritti in Unicode Standard Annex # 15 — Unicode Normalization Forms.

Soluzione conforme

La soluzione conforme normalizza la stringa prima di convalidarla. Le rappresentazioni alternative della stringa sono normalizzate alle parentesi angolari canoniche. Di conseguenza, la convalida dell’input rileva correttamente l’input dannoso e genera un’eccezione IllegalStateException.


String s = "\uFE64" + "script" + "\uFE65";

s = Normalizer.normalize(s, Form.NFKC);

Pattern pattern = Pattern.compile("[<>]"); 
Matcher matcher = pattern.matcher(s);

if (matcher.find()) {
   throw new IllegalStateException(); 
} else {
  // ... 
} 

Normalizzazione dei nomi file

Un percorso file può essere assoluto o relativo.

Un nome di percorso assoluto è completo in quanto non sono necessarie altre informazioni per individuare il file che indica.

Un nome di percorso relativo, al contrario, deve essere interpretato in termini di informazioni prese da un altro nome di percorso.

"." si riferisce alla directory stessa

".." si riferisce alla directory principale della directory

Oltre a questi problemi specifici, esiste un’ampia varietà di convenzioni di denominazione specifiche del sistema operativo e del file system che rendono difficile la convalida.

Il processo di canonizzazione dei nomi di file semplifica la convalida di un nome di percorso.

Più di un nome di percorso può fare riferimento a una singola directory o file.

Inoltre, la rappresentazione testuale di un nome di percorso può fornire poche o nessuna informazione relativa alla directory o al file a cui si riferisce.

Di conseguenza, tutti i nomi di percorso devono essere completamente risolti prima della convalida.

La convalida può essere necessaria, ad esempio, quando si tenta di limitare l’accesso degli utenti ai file all’interno di una determinata directory o di prendere in altro modo decisioni di sicurezza in base al nome di un nome file o al nome di percorso.

Spesso, queste restrizioni possono essere aggirate da un utente malintenzionato sfruttando una vulnerabilità di attraversamento di directory o di equivalenza del percorso. Una vulnerabilità di attraversamento di directory consente a un’operazione di I/O di sfuggire a una directory operativa specificata.

Una vulnerabilità di equivalenza del percorso si verifica quando un utente malintenzionato fornisce un nome diverso ma equivalente a una risorsa per aggirare i controlli di sicurezza.

Esempio di codice non conforme

Questo esempio di codice non conforme accetta un percorso di file come argomento della riga di comando e utilizza il metodo File.getAbsolutePath () per ottenere il percorso di file assoluto.

Tuttavia, non risolve i collegamenti ai file né elimina gli errori di equivalenza.


    File f = new File(System.getProperty(“user.home”) + System.getProperty(“file.separator”) + args[0]);

    String path = f.getAbsolutePath();

    if (!validate(path)) { 
        // Validation
        throw new IllegalArgumentException(); 
    }

Il codice intende impedire all’utente di operare su file al di fuori della propria directory home.

Il metodo validate() cerca di assicurarsi che il nome del percorso risieda in questa directory, ma può essere facilmente aggirato.

Un attaccante può creare un collegamento nella propria directory home che fa riferimento a una directory o a un file al di fuori della propria directory home.

File.getAbsolutePath() risolve infatti i collegamenti simbolici, alias e scorciatoie.

Soluzione conforme

La soluzione conforme utilizza il metodo getCanonicalPath(), introdotto in Java 2, che risolve tutti gli alias, e i collegamenti simbolici in modo coerente su tutte le piattaforme.

Anche i nomi di file speciali come punto punto (..) vengono rimossi in modo che l’input venga ridotto a una forma canonica prima che venga eseguita la convalida.

Un utente malintenzionato non potrà pertanto utilizzare le sequenze ../ per uscire dalla directory specificata:


    File f = new File(System.getProperty(“user.home”) + System.getProperty(“file.separator”)+ args[0]);

    String path = f.getCanonicalPath();

    if (!validate(path)) { 
        throw new IllegalArgumentException(); 
    } 

Un modo più completo per gestire questo problema consiste nel concedere all’applicazione le autorizzazioni per operare solo sui file presenti nella directory prevista, sfruttando la politica di sicurezza implementata con un security policy file:


grant codeBase "file:/home/path/" {
permission java.io.FilePermission "${user.home}/*", "read, write";
}; 

Encodings dei files e Network I/O

Ogni piattaforma Java ha una codifica dei caratteri predefinita.

Una conversione tra caratteri e sequenze di byte richiede una codifica dei caratteri per specificare i dettagli della conversione.

Tali conversioni utilizzano la codifica predefinita del sistema in assenza di una codifica specificata esplicitamente.

Quando i caratteri vengono convertiti in una matrice di byte da inviare come output, trasmessi attraverso canali di comunicazione, e poi riconvertiti in caratteri, è necessario utilizzare codifiche compatibili su entrambi i lati della conversazione.

Il disaccordo sulle codifiche dei caratteri può compromettere l’integrità dei dati.

Esempio di codice non conforme

Questo esempio di codice non conforme legge un array di byte e lo converte in una stringa utilizzando la codifica dei caratteri predefinita della piattaforma. Quando la codifica predefinita è diversa dalla codifica utilizzata per produrre l’array di byte, è probabile che la stringa risultante non sia corretta. Si può verificare un comportamento indefinito quando una parte dell’input manca di una rappresentazione di caratteri valida nella codifica predefinita.


try {
  FileInputStream fis = new FileInputStream("miofile"); 
  DataInputStream dis = new DataInputStream(fis); 

  byte[] data = new byte[1024]; 
  dis.readFully(data);

  String s = new String(data);

} catch (IOException e) { 
   // handle exception
} finally {
  if (fis != null) {
    try { fis.close();
    } catch (IOException e) {
     // handle exception
    } 
  }
} 

Soluzione conforme

Il codice conforme specifica in modo esplicito la codifica dei caratteri prevista nel secondo argomento del costruttore String.


try {
  FileInputStream fis = new FileInputStream("miofile");
  DataInputStream dis = new DataInputStream(fis);

  byte[] data = new byte[1024];
  dis.readFully(data);

  String encoding = "UTF-16LE"; 

  String s = new String(data, encoding);

} catch (IOException e) { 
  // handle exception
} finally {
  if (fis != null) {
   try { 
     fis.close();
   } catch (IOException e) { 
     // handle exception
   }
  }

} 

Sanitization delle Regular Expressions

Ipotizziamo di consentire agli utenti la ricerca di informazioni, impedendo al contempo l’esposizione di informazioni riservate.

Un programma potrebbe consentire all’utente di fornire il proprio testo di ricerca, che diventa parte della seguente regex:


     (.*? +free\[\d+\] +.*SEARCH.*)

Tuttavia, se un utente malintenzionato può inserire qualsiasi stringa in SEARCH, può eseguire una regex injection inserendo il seguente testo:


     .*)|(.*

la regex diventa pertanto:


     (.*? +free\[\d+\] +.*.*)|(.*.*)

Questa regex corrisponderà a qualsiasi riga, comprese quelle riservate.

Soluzione conforme

Questa soluzione conforme filtra i caratteri non alfanumerici (eccetto lo spazio e le virgolette singole) dalla stringa di ricerca, impedendo l’injection di espressioni regolari:


          StringBuilder sb = new StringBuilder(search.length()); 

          for (int i = 0; i &lt; search.length(); ++i) {

              char ch = search.charAt(i);

              if (Character.isLetterOrDigit(ch) || ch == ' ' || ch == '\'') { 
                  sb.append(ch);
              }

          }

          search = sb.toString();


          String regex = "(.*? +free\\[\\d+\\] +.*" + search + ".*)";

Verifica dei valori ritornati dai metodi

I metodi possono restituire valori per comunicare errori o esiti positivi o per aggiornare oggetti o campi locali.

Possono sorgere rischi per la sicurezza quando i valori restituiti dal metodo vengono ignorati o quando il metodo di richiamo non riesce a intraprendere un’azione adeguata.

Di conseguenza, i programmi non devono ignorare i valori restituiti dal metodo.

Quando i metodi getter prendono il nome da un’azione, un programmatore potrebbe non rendersi conto che è previsto un valore di ritorno.

Verifica dei tipi numerici e degli operatori

Rilevare e prevenire l’overflow di numeri interi.

I programmi non devono consentire alle operazioni matematiche di superare gli intervalli di numeri interi forniti dai loro tipi di dati interi primitivi.

Gli unici operatori interi che possono generare un’eccezione sono l’operatore intero di divisione / e l’operatore intero resto %, che genera un’eccezione ArithmeticException se l’operando di destra è zero, oltre agli operatori di incremento e decremento ++ e –, che possono generare un’eccezione OutOfMemoryError se è richiesta la conversione boxing e la memoria non è sufficiente per eseguire la conversione.

Esempio non conforme

Questo esempio di codice non conforme può causare un errore di divisione per zero durante la divisione degli operandi num e div:


    long num, div, ris;

    /* Initialize num and div */ 

    ris = num / div;

Soluzione conforme per la divisione

Questa soluzione conforme verifica il divisore per garantire che non vi sia alcuna possibilità di errori di divisione per zero:


    long num, div, ris;

    /* Initialize num and div */ 

    if (div == 0) { 
        // handle error
    } else {
        ris = num / div;
    }

Esempio di codice non conforme (operatore Modulo)

L’operatore % fornisce il resto quando vengono divisi due operandi di tipo intero. Questo esempio di codice non conforme può causare un errore di divisione per zero durante l’operazione di resto sugli operandi con segno num e mod:


    long num, mod, ris;

    /* Initialize num and mod */ 

    ris = num1 % mod;

Soluzione conforme per operatore Modulo

Questa soluzione conforme verifica il divisore per garantire che non vi sia alcuna possibilità di errore di divisione per zero:


    long num, mod, ris;

    /* Initialize num and mod */ 

    if (mod == 0) { 
       // handle error 
    } else {
      ris = num % mod;
    }