Note finali

Giovedì, 20 Dicembre 2007

OBIETTIVI DELLA LEZIONE

Rivedere quanto appreso nelle scorse lezioni alla luce di una applicazione WEB leggermente più complessa; inoltre approfondire alcuni dettagli utili per sviluppare gli elaborati.

Capire che cosa siano le Command Injection Flaws.

Capire che cosa è Ajax e come si collega a quanto appreso nella seconda parte di questo corso.

Un lungo esempio

Attraverso questo lungo esempio (essessione3.php), ripassiamo le cose imparate nelle scorse lezioni e vediamo come, mettendo insieme le cose imparate, sia possibile costruire un applicazione che, seppure didattica, sia in grado di grarantire un grado di sicurezza minimale.

Il database contiene un solo utente sassi e la password è semplicemente sassi.

Funzioni di servizio

Un file iniziale di funzioni di servizio: miefunzioni.inc

<?php
function verifica_SSL()
{
    if( $_SERVER['HTTPS'] !== 'on' )
    {
        # La connessione non è su socket SSL; invita il browser del client a
        # ricaricare la stessa pagina tramite HTTPS
        header("Location: https://{$_SERVER['SERVER_NAME']}{$_SERVER['PHP_SELF']}");
        exit;
    }

    # Imposta opzione "cookie_secure": il cookie di sessione verrà
    # restituito dal client solo su SSL
    ini_set("session.cookie_secure", TRUE);
}

function verifica_utente()
{
    # Controlla che l'IP dell'utente non sia cambiato da quando
    # è stato identificato
    if( isset($_SESSION['utente']) and
        ($_SESSION['IP_utente']!==$_SERVER['REMOTE_ADDR']) )
        $_SESSION['utente']=FALSE;

    # Identifica l'utente se e' necessario
    if( !isset($_SESSION['utente']) or ($_SESSION['utente']===FALSE) )
    {
        header("Location: http://" . $_SERVER['HTTP_HOST'] .
          dirname($_SERVER['PHP_SELF']) . "/identifica1.php");
        exit;
    }
}

?>

Identificazione dell'utente

Lo script che identifica l'utente: identifica1.php. Utilizza la tabella utenti creata con il comando SQL:

CREATE TABLE `tab_utenti` (
	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
	`utente` VARCHAR( 50 ) NOT NULL ,
	`password` VARCHAR( 32 ) NOT NULL ,
	PRIMARY KEY ( `id` ) );
<?php
require('miefunzioni.inc');

verifica_SSL();

session_start();

# Identificazione Utente
if( !isset($_POST['utente']) or !isset($_POST['password']) )
{
?>

<html>
<head>
<title>Banca PincoPallino</title>
</head>
<body>
<h3>Benvenuto alla Banca PincoPallino</h3>
Per cortesia inserire il nome dell'utente e la password
<form action="<?php print $_SERVER['PHP_SELF']; ?>" method="POST">
<table>
<tr>
<td>Nome Utente:</td>
<td><input type="text" name="utente"></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Identificati"></td>
</tr>
</table>
<a href="registra1.php">Registrati ora!</a>
</form>
</body>
</html>

<?php
}
else
{
    $ptr_mysqls=mysql_connect("localhost","root","")
        or die("Impossibile connettersi al server MySQL.\n");
    mysql_select_db("dbprova1", $ptr_mysqls)
        or die("Impossibile aprire il database.\n");

    $md5password=md5($_POST['password']);
    $query_visualizzazione="SELECT * FROM tab_utenti" .
        " WHERE utente='{$_POST['utente']}' AND password='$md5password'";
    $ptr_risultato_query=mysql_query($query_visualizzazione, $ptr_mysqls);

    $numero_records=mysql_affected_rows($ptr_mysqls);
    mysql_close($ptr_mysqls);

    if($numero_records == 1)
    {
        # Utente identificato
        $_SESSION['utente']=$_POST['utente'];
        $_SESSION['IP_utente']=$_SERVER['REMOTE_ADDR'];
    header("Location: http://" . $_SERVER['HTTP_HOST'] . 
      dirname($_SERVER['PHP_SELF']) . "/essessione3.php");
        exit;
    }
?>

<html>
<head>
<title>Banca PincoPallino</title>
</head>
<body>
<h3>Attenzione, Nome Utente o Password non corretti.</h3>
</body>
</html>

<?php
}
?>

Home page

Il file principale: essessione3.php

<?php
require('miefunzioni.inc');

verifica_SSL();

# Se si arriva a questa riga la connessione sta avvenendo su canale criptato;
# Inizializza o ripristina la sessione
session_start();

verifica_utente();
# Se si arriva a questa riga L'utente e' identificato e non ha cambiato
# IP di connessione
?>

<html>
<head>
<title>Banca PincoPallino</title>
</head>

<body>
<h3>Banca PincoPallino</h3>

Salve <?php print $_SESSION['utente']; ?>,
benvenuto al sito della Banca PincoPallino.<br />
Effettua le tue operazioni con la tranquillità della sicurezza!<br />
<br />
<br />
[... operazioni varie ...]
<br />
<br />
<br />
Cambia la tua password:
<form action="modifica1.php" method="post">
<input type="password" name="nuovapassword1">
<input type="password" name="nuovapassword2">
<input type="submit" value="Conferma">
</form>
<br />
<a href="chiudi1.php">Chiudi la sessione</a>.
</body>
</html>

Modifica della password

Lo script che modifica la password: modifica1.php

<?php
require('miefunzioni.inc');

verifica_SSL();

session_start();

verifica_utente();

# Cambia la password
$numero_records=0;
if( isset($_POST['nuovapassword1']) and isset($_POST['nuovapassword2']) and
    ($_POST['nuovapassword1']===$_POST['nuovapassword2']) )
{
    $ptr_mysqls=mysql_connect("localhost","root","")
        or die("Impossibile connettersi al server MySQL.\n");
    mysql_select_db("dbprova1", $ptr_mysqls)
        or die("Impossibile aprire il database.\n");

    # NON memorizziamo la password ma la sua trasformata md5
    $md5password=md5($_POST['nuovapassword1']);
    $query_modifica="UPDATE tab_utenti SET password='$md5password'" .
        " WHERE utente='{$_SESSION['utente']}'";
    $ptr_risultato_query=mysql_query($query_modifica, $ptr_mysqls);

    $numero_records=mysql_affected_rows($ptr_mysqls);
    mysql_close($ptr_mysqls);
}
?>

<html>
<head>
<title>Richiesta Utente</title>
</head>
<body>
<h3><?php
if($numero_records === 1)
    print "Operazione conclusa correttamente.";
else
    print "Attenzione, impossibile completare l'operazione richiesta.";
?></h3>
</body>
</html>

Chiusura delle sessione

Lo script di chiusura chiudi1.php

<?php
require('miefunzioni.inc');

verifica_SSL();

session_start();

verifica_utente();

# Disalloca tutte le variabili della sessione.
session_unset();

# Distrugge la sessione.
session_destroy();
?>

<html>
<head>
<title>Banca PincoPallino</title>
</head>
<body>
<h3>Arrivederci, grazie di aver utilizzato i nostri servizi online.</h3>
</body>
</html>

Command Injection Flaws (cenni)

Durante la seconda parte del corso abbiamo visto come sia possibile utilizzare lato client un form HTML per inviare dati inseriti dall'utente ad una CGI lato server.

Nei forms che abbiamo preparato, l'utente è stato libero di inserire ciò che più gli aggradava. Così quando chiedevamo una data, se l'utente avesse immesso una parola i nostri scripts PHP non avrebbero protestato, ma al più avrebbero funzionato in modo scorretto. E se siamo disposti a pagare questo prezzo in termini di utilizzabilità del sito, può anche andare bene così.

Ciò che non abbiamo mai tenuto in considerazione, è invece il caso in cui un malintenzionato volesse deliberatamente sfruttare il fatto che non verifichiamo gli input per manipolare o accedere al nostro sito.

Sono detti command injection flaws (falle per l'iniezione di comando) tutti quegli input (non controllati) che vengono passati come argomenti a programmi esterni o come parte di query SQL a databases. Vediamone un esempio. Torniamo all'esempio visto in precedenza. Nel file identifica1.php sostituiamo la riga

$query_visualizzazione="SELECT * FROM tab_utenti" .
    " WHERE utente='{$_POST['utente']}' AND password='$md5password'";

con la seguente (differenza evidenziata in giallo):

$query_visualizzazione="SELECT * FROM tab_utenti" .
    " WHERE utente='{$_POST['utente']}' AND password='{$_POST['password']}'";

Sembra che la differenza non sia sostanziale. Proviamo ora ad utilizzare il nuovo script essessione4.php con

utente: ' OR ''='
password: ' OR ''='

Il sito ci riconosce come utente registrato anche se l'unico utente registrato è sassi. L'attaccante è riuscito quindi con una SQL injection (iniezione di codice SQL) a forzare il meccanismo di autenticazione. Come ha fatto? Se sostituiamo gli input utente nella stringa della query verifichiamo facilmente che l'utente è riuscito a modificare la condizione logica su cui si basava la nostra query.

$query_visualizzazione="SELECT * FROM tab_utenti" .
    " WHERE utente='' OR ''='' AND password='' OR ''=''";

L'operazione logica ''='' in SQL è una tautologia (è sempre vera). Problemi come questi sono spesso frequenti nelle applicazioni WEB. Per evitarlo alla radice bisogna porre notevole attenzione e aver studiato bene il PHP (e l'SQL). Molti problemi di vulnerabilità nascono dal fatto che alcuni programmatori utilizzano il taglia/incolla di codice scritto da altri e che non hanno capito a fondo. Una seconda utile abitudine è quella di "disinfettare" (sanitize) l'input dell'utente per evitare che possa inserire caratteri o sequenze problematiche. In questo modo, l'input utente PRIMA di essere utilizzato viene trattato in modo da eliminare tutti i caratteri che non sono ammessi in quel contesto. L'operazione va fatta lato server! Lato client (ad esempio in JavaScript) non è affatto efficace a prevenire questo tipo di problemi (l'utente può sempre evitare i controlli connettendosi al server in telnet sulla porta 80). Esistono numerose librerie di funzioni già pronte allo scopo; un esempio di riferimento è la collezione OWASP PHP Filters, che fa parte di un più ambizioso progetto di validazione degli input, l' OWASP Validation Project.

PHP Top 5

Un elenco delle 5 tecniche di attacco più utilizzate nel 2005 contro una applicazione PHP è riportato nello studio PHP Top 5. Un buon programmatore PHP deve comprendere a fondo questi problemi per evitare che gli attacchi al sito abbiano efficacia. SQL injection è stata una delle 5 tecniche più utilizzate nel 2005.

AJAX (cenni)

AJAX (Asynchronous Javascript and XML) è una serie di tecniche di programmazione che, unendo l'utilizzo di codice Javascript interpretato lato client e veicolando i dati in formato XML permette di costruire applicazioni web più efficaci e potenti. Un esempio di questo tipo di applicazioni è GMail: mail.google.com. Non è materia per questo corso comprendere a fondo le problematiche relative ad AJAX, ma è invece utile comprendere come si colloca quanto abbiamo imparato in questo contesto.

Per prima cosa è utile capire che AJAX è indipendente dalla particolare tecnologia utilizzata lato server: questa può essere sviluppata in PHP, C#, JSP o quant'altro. Il comune denominatore alle applicazioni AJAX è che alcune parti delle pagine HTML vengono aggiornate in modo asincrono rispetto all'intera pagina. A tal punto che alcune applicazioni AJAX sono costituite da una sola pagina! Soprassedendo al problema di veicolare i dati tramite XML, vediamo come potremmo costruire una pagina di registrazione per nuovi utenti che implementi alcune caratteristiche AJAX. Ecco il listato del file registra1.php. La parte che esegue le richieste asincrone è evidenziata in giallo.

<?php
require('miefunzioni.inc');
verifica_SSL();
session_start();

$usernameValido=false;
if(isset($_POST['username'])) {
  # Verifica che lo username sia valido, cioè non ne esista uno uguale nel database
  $ptr_mysqls=mysql_connect("localhost","root","")
    or die("Impossibile connettersi al server MySQL.\n");
  mysql_select_db("dbprova1", $ptr_mysqls)
    or die("Impossibile aprire il database.\n");

  $query_selezione="SELECT * FROM tab_utenti WHERE utente='{$_POST['username']}'";
  $ptr_risultato_query=mysql_query($query_selezione, $ptr_mysqls);

  $numero_records=mysql_affected_rows($ptr_mysqls);
  mysql_close($ptr_mysqls);

  if($numero_records==0) {
    $usernameValido=true;
  }
}


if( !$usernameValido or 
  !isset($_POST['password1']) or !isset($_POST['password2']) or
  ($_POST['password1']!==$_POST['password2']) )
{
?>
<html>
<head>
<script type="text/javascript">
var richiestaAjax = false;

if (window.XMLHttpRequest) { // Firefox/Mozilla/Safari
    richiestaAjax = new XMLHttpRequest();
}
else if (window.ActiveXObject) { // Microsoft Internet Explorer
    richiestaAjax = new ActiveXObject("Microsoft.XMLHTTP");
}

function verifica(username) {
  // Cancella la richiesta eventualmente ancora in esecuzione
  richiestaAjax.abort();
  // il terzo parametro indica se l'elaborazione può proseguire
  // senza aspettare la risposta dopo il metodo send()
  // AJAX sta tutto quì
  richiestaAjax.open("GET", "verifica1.php?username=" + username, true);
  // onreadystatechange è una funzione che viene lanciata 
  // ad ogni cambiamento di stato
  richiestaAjax.onreadystatechange=gestoreStato;
  // invia la richiesta asincrona
  richiestaAjax.send(null);
}

function gestoreStato() {
  // stato corrente, 4 significa "risposta ricevuta"
  if(richiestaAjax.readyState == 4) {
    var corpoRisposta = richiestaAjax.responseText;
    document.getElementById('messaggio').innerHTML = corpoRisposta;
  }
}
</script>
<title>Registrazione Nuovo Utente</title>
</head>

<body>
<h2>Registrazione Nuovo Utente</h2>
<form action="<?php print $_SERVER['PHP_SELF']; ?>" method="POST">
<table>
<tr>
<td>Scegli uno username<?php 
if(isset($_POST['username']) and !$usernameValido) 
  print ' <font color="#FF0000">(username non disponibile)<font>';
?>:</td>
<td><input type="text" name="username"<?php
if( isset($_POST['username']) ) print " value=\"{$_POST['username']}\"";
?> onkeyup="verifica(this.value)"></td><td><div id="messaggio"></div></td>
</tr>
<tr>
<td>Scegli una password<?php 
if( isset($_POST['password1']) and isset($_POST['password2']) and
  ($_POST['password1']!==$_POST['password2']) ) 
  print ' <font color="#FF0000">(le password indicate non coincidono)<font>';
?>:</td>
<td><input type="password" name="password1"></td>
</tr>
<tr>
<td>Ri-digita la password:</td>
<td><input type="password" name="password2"></td>
</tr>
<tr>
<td></td><td><input type="submit" value="Registrati!"></td>
</tr>
</table>
</form>
</body>
</html>
<?php
}
else
{
  # Registra il nuovo utente
  $ptr_mysqls=mysql_connect("localhost","root","")
      or die("Impossibile connettersi al server MySQL.\n");
  mysql_select_db("dbprova1", $ptr_mysqls)
      or die("Impossibile aprire il database.\n");

  # NON memorizziamo la password ma la sua trasformata md5
  $md5password=md5($_POST['nuovapassword1']);
  $query_inserimento="INSERT INTO tab_utenti (utente, password) ".
    "VALUES ('{$_POST['username']}', '$md5password')";
  $ptr_risultato_query=mysql_query($query_inserimento, $ptr_mysqls);

  $numero_records=mysql_affected_rows($ptr_mysqls);
  mysql_close($ptr_mysqls);
    
  # L'utente è stato registrato con successo
  session_unset();
  $_SESSION['utente']=$_POST['username'];
  $_SESSION['IP_utente']=$_SERVER['REMOTE_ADDR'];
  header("Location: http://" . $_SERVER['HTTP_HOST'] . 
    dirname($_SERVER['PHP_SELF']) . "/essessione3.php");
}
?>

Al caricamento della pagina HTML, il browser del client esegue lo script Javascript che crea innanzitutto un oggetto XMLHttpRequest. XMLHttpRequest è una API (Application Programming Interface) che può essere utilizzata per effetture richieste asincrone tra il client ed il server. È stata originariamente introdotta da Microsoft con il nome di XMLHTTP e successivamente il consorzio W3C ne ha proposto una standarizzazione che è ancora in divenire. Non tutti i browser la supportano; inoltre talvolta è supportata con nomi differenti.

Javascript/Jscript è un linguaggio di scripting orientato agli oggetti che viene interpretato dal browser. È un linguaggio potente e permette di manipolare, senza l'intervento del server, la pagina visualizzata dal browser client. Tipicamente con Javascript si manipola il DOM (Documento Object Model) della pagina HTML che viene mappata con un oggetto.

In una pagina HTML è possible introdurre tra le proprietà dei controlli, l'indicazione di come vogliamo che un particolare evento sia gestito (tipicamente tramite una funzione Javascript). È quello che fa il nostro codice con la proprietà del tag <input>: onkeyup="verifica(this.value)". onkeyup è l'evento che si verifica ogni volta che un tasto viene rilasciato mentre il focus è all'interno del controllo in questione. Quando si verifica tale evento, viene invocata la funzione Javascript verifica() (che avevamo definito all'inizio della pagina) sull'oggetto this.value, cioè sul valore attuale contenuto nella casella di testo.

La funzione verifica() prepara la richiesta da effettuare con opportuni metodi istanza dell'oggetto XMLHttpRequest ed effettua la richiesta senza stare ad aspettare la risposta. Una volta che la risposta del server arriva, l'evento è gestito dalla funzione gestoreStato() (sempre definita da noi) che modifica il DOM della pagina corrente come desiderato.

Lo script PHP che riceve la richiesta lato server è verifica1.php. Eccone il listato:

<?php
if(isset($_GET['username']) and $_GET['username']!=="") {
  # Verifica che lo username sia valido, cioè non ne esista uno uguale nel database
  $ptr_mysqls=mysql_connect("localhost","root","")
    or die("Impossibile connettersi al server MySQL.\n");
  mysql_select_db("dbprova1", $ptr_mysqls)
    or die("Impossibile aprire il database.\n");
  
  $utente=trim($_GET['username']);
  $query_selezione="SELECT * FROM tab_utenti WHERE utente='$utente'";
  $ptr_risultato_query=mysql_query($query_selezione, $ptr_mysqls);

  $numero_records=mysql_affected_rows($ptr_mysqls);
  mysql_close($ptr_mysqls);

  if($numero_records==0) {
    print '<font color="#00FF00"><b>(username disponibile)</b></font>';
  }
  else {
    print '<font color="#FF0000"><b>(username NON disponibile)</b></font>';
  }
}
?>

In definitiva, AJAX non è per niente complicato, ma è l'unione di una serie di tecniche utilizzate da tempo che ora hanno raggiunto un livello di maturità più elevato. Nello sviluppo di progetti di una certa rilevanza è tipico utilizzare un "framework AJAX", cioè una libreria di funzioni Javascript che semplifichino molti dei compiti più comuni, come il drag and drop, o la gestione delle richieste ad un webservice.

Lab

Esercizio Facoltativo

Dato il database dell'esercizio 2 della scorsa lezione, preparare lo script sommaspese.php che chieda all'utente due date e calcoli in uscita la somma del campo "importo" per le spese effettuate nell'intervallo specificato.

©2007 Roberto Sassi