The Deep Tour: Prologo

Come tutti sappiamo, la rete globale ha un’estensione gigantesca e l’internet è un mondo davvero grande. E come ogni grande mondo anche esso nasconde segreti e posti oscuri. Ed è così che tutti ne parlano ma nessuno sa effettivamente cosa sia: parliamo infatti del Deep Web.
Il Deep Web o anche detto DarkNet è costituito da tutti i siti e servizi online che nostri comuni motori di ricerca non riportano nei risultati delle ricerche. Ma noi ci siamo stati e vi spiegheremo perché e sfateremo anche alcuni miti su questo mondo poco conosciuto.

Una delle più comuni rappresentazioni del Deep Web è la metafora di ciò che l’iceberg fa vedere e quello che è effettivamente invisibile a chi “naviga” nel web

Leggi tutto “The Deep Tour: Prologo”

Ada vs Injection Attacks

Secondo l’OWASP (Open Web Application Security Project) in testa alla top 10 dei più comuni bachi di sicurezza del web c’è l’injection attack, un tipo di attacco reso possibile dall’uso imprudente di dati ricevuti dall’utente.  In questo articolo mostriamo come proteggersi — a costo zero — dall’injection attack sfruttando la caratteristica di Ada di essere strongly typed.

Il problema: dati esterni &  injection attack

Avete appena avuto l’idea che vi farà ricchi: un sito web attraverso il quale vendere biciclette per cincillà (originale, non c’è che dire… forse un po’ di nicchia).  Vi create la vostra applicazione che include una pagina di login per i vostri numerosi (speriamo) clienti

Dal lato server, per autenticare il cliente,  interrogate il vostro database con qualcosa del tipo

$query = "SELECT pwd FROM clienti WHERE user='" . $_GET["usr"] . "';"
$password = $db->query($query)

Andate a dormire sognando di fare il bagno nell’oro come Paperone e la mattina dopo vi svegliate e… Il database non c’è più!  Cosa è successo?  Forse qualcosa di simile a questo

In effetti, usando come username lo strano nome nel fumetto si ottiene la query

SELECT pwd FROM clienti WHERE user='Robert'; DROP TABLE clienti; -- ';

Anche senza avere una conoscenza approfondita dell’SQL, si capisce che tale comando, oltre ad interrogare la tabella clienti, la cancella…

Questo tipo di attacco si chiama SQL Injection ed è un caso specifico di Injection attack, una delle cause più comuni di problemi di sicurezza su web. L’injection attack è reso possibile quando un dato proveniente dall’esterno (es. ricevuto da un utente, letto da un file, …) viene passato direttamente ad un interprete (shell, MySQL, …)  senza prima “depurarlo” (per esempio, quotando eventuali caratteri speciali).

In alcuni casi, come quello dell’esempio, la possibilità di un attacco di questo tipo è evidente, ma possono esistere altri casi in cui il problema è molto più nascosto se, per esempio, la query dipende dall’input dell’utente in maniera indiretta (ossia, usa dati che sono stati ricavati in un’altra porzione del codice dall’input dell’utente).

L’idea di dato contaminato (taint)

Per risolvere questo tipo di problema alcuni linguaggi (es. Ruby) introducono l’idea di dato taint (“contaminato”). Un dato letto dall’esterno è considerato “sporco” e non può essere usato con funzioni pericolose quali query o eval. Inoltre, la contaminazione è contagiosa e un risultato ottenuto elaborando un dato contaminato è contaminato a sua volta. Prima di poter passare un dato contaminato ad una funzione pericolosa è necessario dichiarare il dato  “pulito” usando —tipicamente— una specifica funzione fornita dal linguaggio (es. untaint in Ruby). Ovviamente, si suppone che il programmatore non sia pigro ed effettivamente “depuri” il dato (per esempio, quotando eventuali caratteri speciali) prima di dichiararlo pulito.

 

L’idea di dato contaminato è  chiaramente uno strumento molto utile per evitare code injection, ma è uno strumento che si presta più per i linguaggi interpretati che compilati. In molti linguaggi compilati quali C, C++, … la soluzione più semplice è forse la formulazione di opportune regole di programmazione (chiama system() solo con argomenti decontaminati!)  con pene esemplari (di fantozziana memoria) per chi sgarra

Static tainting in Ada

Anche se Ada può non sembrare una scelta intuitiva per un applicativo web, l’esistenza della libreria Ada Web Server (AWS, nulla a che fare con Amazon…) permette di implementare facilmente applicazioni robuste con interfaccia web. In particolare, per quanto riguarda la protezione da injection attack, in Ada è possibile sfruttare l’idea di dato contaminato in maniera statica (ossia verificata al momento della compilazione e non a runtime), senza overhead in fase di esecuzione.




Si consideri, per esempio il seguente “esempio giocattolo” di un package che esporta funzioni per interrogare un database

package DB_Queries is
   type Safe_Query (<>) is private;

   function Purify (Item : String) return Safe_Query;

   function MySQL_Query (Query : Safe_Query) return String;

   function "&"(Left, Right : Safe_Query) return Safe_Query;

   function "&"(Left  : Safe_Query; Right : String) 
                return Safe_Query
   is (Left & Purify(Right));  -- Syntactic sugar

   function "&"(Left  : String; Right : Safe_Query) 
                return Safe_Query
   is (Purify(Left) & Right);  -- More syntactic sugar
private
   type Safe_Query is new String;
end DB_Queries;

Un paio di osservazioni per chi non è familiare con Ada

  • In Ada  i package sono un po’ lo strumento base per organizzare il codice.  Un package tipicamente esporta una o più funzioni/procedure per operare sui tipi.  Ogni package ha un body che contiene il codice eseguibile (una specie di  *.c, insomma) ed una spec che contiene la dichiarazione delle risorse esportate verso l’esterno (all’incirca l’equivalente di un *.h).  Ogni file di spec ha poi due parti: una parte pubblica ed una parte privata, separate dalla keyword private.  Ovviamente solo la parte pubblica è visibile alla parte di  codice esterna al package.
  • La decorazione “(<>)” nella definizione pubblica di Safe_String sta ad indicare che Safe_String è un indefinite type. Spiegare nel dettaglio cosa sia un tipo indefinito ci porterebbe troppo lontano, diciamo semplicemente che si tratta di un tipo che non può essere allocato senza fornire altri dati.  Per esempio, per definire una variabile di tipo String (che è un array di caratteri) bisogna fornire la sua lunghezza, così che non posso scrivere
    X : String;  -- Quanto lunga deve essere?

    ma devo scrivere

    X : String(1..10);       -- Stringa di 10 caratteri
    Y : String := Get_Line;  -- OK, Y inizializzata col risultato di Get_Line

Nel codice sopra vediamo all’inizio la dichiarazione del tipo Safe_String che rappresenta una stringa di testo “ripulita” in modo da poter essere usata in una query SQL.  Poiché il tipo è dichiarato private, la sua struttura interna non è visibile al di fuori del package. La dichiarazione  completa di Safe_String si trova nella parte privata ed è

type Safe_String is new String;

Tale dichiarazione afferma che Safe_String è… semplicemente una stringa, ma diversa…  Il compilatore genererà quindi per Safe_String lo stesso codice che avrebbe generato per una stringa normale, ma impedirà al programmatore di mescolare direttamente i due tipi.

Questo è un aspetto importante di Ada: in C Safe_String sarebbe un sinonimo di String e il programmatore potrebbe mescolare i due tipi tra loro; in Ada Safe_String e String sono considerati due tipi diversi che non possono essere mescolati, anche se la loro implementazione interna è la stessa.  Questo tipo di separazione, se usato correttamente, è un potente strumento per ridurre il numero di errori nel codice (been there, done that…).

Più avanti vediamo la dichiarazione della funzione

function MySQL_Query (Query : Safe_Query) return String;

che, intuiamo, manda una query al database.  Si osservi che non possiamo interrogare il database con una stringa qualsiasi, ma solo con una Safe_Query, ossia una stringa che è garantita essere esente da injection attack.  Se provassimo a chiamare

Result := MySQL_Query
         ("SELECT * FROM t WHERE usr='" & Get_Param("usr") & "'" );

riceveremmo un errore in fase di compilazione poiché stiamo chiamando MySQL_Query con il tipo sbagliato, ossia una String e non una Safe_String.  L’unico modo per creare una stringa sicura è attraverso la funzione

  function Purify (Item : String) return Safe_Query;

che —possiamo supporre— verifica la presenza di caratteri speciali e,  eventualmente, li quota.  Questa funzione è l’unico cancello attraverso il quale deve passare ogni stringa prima di poter  essere usata come query, così che il programmatore non può, nemmeno volendo, passare una stringa pericolosa alla funzione MySQL_Query.

Onde permettere un minimo di elaborazione con le Safe_Query il package definisce anche tre operatori di concatenazione

function "&"(Left, Right : Safe_Query) return Safe_Query;

function "&"(Left  : Safe_Query; Right : String) 
             return Safe_Query
is (Left & Purify(Right));  -- Syntactic sugar

function "&"(Left  : String; Right : Safe_Query) 
             return Safe_Query
is (Purify(Left) & Right);  -- More syntactic sugar

Il primo può essere implementato come un alias della normale concatenazione tra stringhe (in fondo una Safe_String è pur sempre una stringa…), gli altri due permettono la concatenazione tra una stringa normale e una sicura chiamando “dietro le quinte” la funzione Purify. Come specificato dal commento, gli ultimi due operatori di concatenazione sono semplicemente zucchero sintattico, ma sono convenienti perché permettono di scrivere codice come

Result := MySQL_Query
          ("SELECT * FROM t WHERE usr='" & Purify(Get_Param("usr")) & "'" );
Si osservi come  gli operatori di concatenazione scelti dal compilatore per tradurre la linea sopra siano necessariamente quelli definiti nel package (e non l’operatore “standard” di concatenazione tra stringhe) poiché Purify restituisce una Safe_String.
La sintassi per la definizione di funzione usata nell’esempio (introdotta in Ada 2012 ) è detta espression function ed è molto conveniente.

 

Altri approcci

L’approccio presentato qui è forse il più semplice, ma non è l’unico. Due possibili esempi di approcci alternativi sono l’uso dei dynamic predicate e l’uso di package privati per controllare l’accesso a porzioni “rischiose” del  codice. Descriverli qui rischierebbe però di appesantire eccessivamente questo articolo e ne parleremo più diffusamente in un’altra occasione.