Sicurezza Applicativa: best practice per la programmazione sicura

Aspetto estremamente importante della sicurezza applicativa è la programmazione sicura. Programmare in modo sicuro significa utilizzare i costrutti del linguaggio di programmazione e le caratteristiche dell'ambiente in cui verrà eseguito, in modo sicuro.

Proprio per aiutare lo sviluppatore nel compito di scrivere del codice sicuro, vi illustro una tassonomia degli errori di programmazione che, se la si vede come un insieme di errori da non commettere, rappresenta delle "best practice" molto utili.

Per definire tale tassonomia vengono considerate due note classificazioni degli errori di programmazione:
Nella tassonomia c'è quindi un primo livello (più astratto) costituito dai "Seven Pernicious Kingdoms" ed un secondo livello (più di dettaglio) con i "19 Deadly Sins".
Il primo livello, con solo 7 elementi che permette una facile memorizzazione, definisce dei raggruppamenti di errori di codifica che possono portare a delle vulnerabilità nelle applicazioni. Identificare il raggruppamento già ci permette di restringere le possibili vulnerabilità.

Il secondo livello invece ha il merito di esemplificare alcune tipologie di errori particolari e più specifici e ci viene in aiuto una volta identificato il primo livello.

Sono state escluse le classificazioni OWASP Top Ten 2004 e 2007 in quanto prettamente orientate alla sicurezza delle applicazioni Web e perché definiscono delle tipologie di vulnerabilità non contemporaneamente allo stesso livello di astrazione o di dettaglio.


Analizziamo quindi la tassonomia esplicando volta per volta le diverse best practice:

1 - Validazione e rappresentazione dell’input: è il controllo che l’input dell’utente e la sua rappresentazione non contengano caratteri o sequenze di caratteri sfruttabili in modo malizioso. Si divide in validazione e rappresentazione: il processo di validazione analizza l'input fornito dall'utente e decide se questo può o meno essere accettato; il processo di rappresentazione invece elabora l'input effettuando una serie di "bonifiche" che mirano ad ottenere un input "privo di elementi maliziosi" (in effetti sarebbe più corretto parlare di "Data Validation").

La validazione dell'input può essere implementata mediante delle espressioni regolari, o degli algoritmi di filtro, che definiscono solo quello che può essere accettato con un approccio di tipo "white list". E' da evitare invece un approccio di tipo "black list" che definisca invece gli input da NON validare. Mentre infatti possiamo definire l'insieme delle stringhe che rappresentano un input fidato, non è possibile identificare l'insieme delle stringhe da non accettare, in quanto gli attacchi possibili sono in continua evoluzione e le stringhe si evolvono con essi, rendendo questo insieme di fatto infinito.

La corretta rappresentazione dell'input elabora l'input effettuando una serie di sostituzioni in modo da depurarlo da tutti quei caratteri (o sequenze di essi) che potrebbero essere utilizzati in modo malizioso. La corretta rappresentazione dipende dal sistema con il quale verrà appunto "rappresentato": se è un DB dovremo porre particolare attenzione agli apici e ai caratteri che permettono attacchi di tipo SQL Injection; se è un CMS, o comunque un portale, dovremo porre invece attenzione al codice JavaScript, e via dicendo.

Una procedura standard è quella di canonicalizzare per prima cosa l'input e poi eseguire tutte le varie validazioni o bonifiche del caso. Possiamo utilizzare un approccio più conservativo spostando l'intero controllo sulla fase di validazione e rappresentarlo poi così come è. Oppure accettare l'input dopo dei controlli grossolani ed eseguire delle bonifiche per evitare di memorizzare informazioni maliziose.

Sta a noi quindi decidere se spostare l'asticella più verso la parte "Validation" o più verso la "Representation".

La cosa importante da capire è che la bonifica del dato è relativa al sistema verso il quale questo andrà inviato. E' quindi consigliabile:

  1. effettuare una prima e grossolana validazione dell'input (che non modifica il dato);
  2. memorizzare il dato così come è stato fornito dall'utente;
  3. rappresentarlo, dopo averlo opportunamente bonificato, nel sistema destinatario.

  • 1.1 Cross Site Scripting (XSS)
  • 1.2 SQL Injection
  • 1.3 Command Injection
  • 1.4 Formato delle stringhe
  • 1.5 Buffer overrun
  • 1.6 Integer Overflow

2 - Abuso delle API: consiste nell'utilizzare API con modalità che vanno al di fuori del "contratto che l'API stabilisce tra chiamante e chiamato". L'API assume alcune condizioni al contorno che non devono essere quindi "abusate".

Classico esempi sono: considerare fidate delle API che ritornano informazioni di un nodo di rete effettuando chiamate ad un DNS; considerare fidate le funzioni che ritornano numeri casuali. Se vogliamo generare una stringa casuale in modo sicuro (ovvero una stringa che non si ripeta in un tempo computazionalmente lungo), dovremmo usare delle API opportune e non la classica "rand()". Potremmo infatti violare il contratto dell'API il quale ci dice che "rand()" è una funzione per la generazione di stringhe pseudo-casuali, poco adatta a contesti in cui la sicurezza è un fattore rilevante e quindi la impredicibilità del numero casuale è fondamentale per essa.

  • 2.1 Risoluzione impropria dei nomi di rete
3 - Caratteristiche di sicurezza: un software non è sicuro solo perché usiamo delle primitive di sicurezza nel codice. E' proprio per questo motivo che l'uso improprio di tali primitive può essere estremamente pericoloso in quanto fornisce allo sviluppatore un falso senso di sicurezza. Utilizzare funzioni randomiche deboli o archiviare in modo insicuro delle chiavi sono alcuni esempi di questo uso improprio.
  • 3.1 Fallimento nella protezione del traffico di rete
  • 3.2 Fallimento nell’archiviazione e protezione dei dati in modo sicuro
  • 3.3 Uso improprio di funzioni per la generazione di numeri casuali
  • 3.4 Accesso improprio ai file
  • 3.5 Uso improprio del protocollo SSL/TSL
  • 3.6 Uso di password deboli nel sistema
  • 3.7 Scambio di chiavi crittografiche non autorizzato

4 - Tempo e Stato: race condition (tempo) e stati condivisi tra i sistemi possono divenire degli ottimi veicoli di attacco. Questo tipo di vulnerabilità è legato alla percezione, errata, che qualche volta gli sviluppatori possono avere e che consiste nel considerare il proprio sistema un sistema ad esecuzione sequenziale ed in cui lo stato non può essere maliziosamente modificato. Questa tipologia di problemi è molto subdola è può essere veramente complessa da individuare.

Alcuni esempio sono: l'utilizzo di thread in modo non opportuno che può portare a delle race condition, oppure è lo stesso sistema che, utilizzando delle particolari funzioni per la gestione della concorrenza, lascia spazio ad esse; la manipolazione dello stato di un'applicazione, anche distribuita, che modifica le strutture dati che lo rappresentano, magari agendo su campi nascosti o su primitive del linguaggio di programmazione (es: con la famigerata "register_globals" in PHP e possibile impostare delle variabili interne semplicemente fornendo un valore ad un argomento nella URI).

  • 4.1 Race Conditions (Time Of Check Time Of Use)
  • 4.2 Uso di URL insicure e campi HTML nascosti

5 - Errori: una cattiva gestione degli errori, non conforme alle specifiche del linguaggio utilizzato e ai suoi comportamenti attesi, rappresenta la fonte di probabili comportamenti indesiderati o situazioni di inconsistenza.

Un esempio è quello di blocchi "try & catch" dove nella clausola catch non venga inserita nessuna istruzione, oppure il catch di una eccezione troppo generica che può mascherare la vera eccezione.
  • 5.1 Fallimento nella gestione degli errori

6 - Qualità del codice: assicurarsi che il codice rispetti dei parametri di qualità non sembrerebbe essere legato alla sicurezza del software, mentre invece è un aspetto estremamente importante, poiché una cattiva qualità del codice può portare a degli errori di programmazione molto difficili da individuare.

Assicurarsi, per esempio, di commentare in modo opportuno il codice o scrivere codice comprensibile o far sì che il sistema sia usabile, sono solo alcuni parametri di qualità del codice che possono avere ricadute nella sicurezza del software.
  • 6.1 Scarsa usabilità

7 - Incapsulamento: un non corretto incapsulamento si verifica quando i dati o il codice eseguibile non sono correttamente confinati nell’opportuno perimetro di sicurezza del sistema software.

Sessioni di applicazioni di diversi utenti che scrivono in aree comuni o perdita di informazioni causata da messaggi di sistema non opportunamente filtrati, oppure codici eseguibili (ActiveX, Inner class, Mobile code) eseguiti in un contesto diverso da quello previsto inizialmente, sono degli esempi di un cattivo incapsulamento.

  • 7.1 Perdita di informazioni

* - Ambiente di esecuzione: anche se l'ambiente di esecuzione non appartiene al dominio del software, sarebbe illusorio pensare che la sicurezza del software dipenda unicamente dal linguaggio di programmazione. Essa dipende anche dal contesto in cui tale software è in esecuzione ed è quindi estremamente importante valutarne la sicurezza.

Esempi sono controlli di accesso (ACL) non opportunamente impostati per i file di una applicazione Web o l'impostazione della directory temporanea in modo non corretto (e magari condiviso con più applicazioni), oppure un file delle password posto all'interno dell'albero della Web application.

  • *.1 Configurazione insicura dell’ambiente

Se volete approfondire l'argomento questi sono i link agli articoli:

Commenti

Post popolari in questo blog

Exploit: icsploit o espluà?

TrueCrypt 5.0: nuova release

ING Direct: ancora con il PAD numerico rotante!