Categorie
PHP

Prevenire attacchi XSS con PHP

Gli attacchi indicati come cross-site scripting (XSS) sono un’autentica piaga per i siti Web che non prevedono un corretto controllo degli input da parte dell’utente con richieste HTTP, sia GET che POST. Naturalmente questo controllo perchè sia realmente efficace è necessario che venga eseguito lato server, qualunque controllo eseguito lato client non fornisce garanzie sufficienti.

XSS

Utenti malintenzionati possono facilmente immettere tag HTML nei nostri moduli dati, per esempio nel campo commenti di un blog, tanto per rimanere in tema. Cosa c’è di sbagliato in questo, ci si potrebbe chiedere?
Molte applicazioni Web prendono in modo dinamico le informazioni trasmesse da un utente, le memorizzano in un db e quindi le visualizzano di nuovo in un’altra pagina. Questo potrebbe buttare all’aria il layout e l’estetica del tuo sito, basterebbe, per esempio inserire un div senza chiuderlo ed il gioco è fatto.
Facendo un passo avanti, se ad essere inserito fosse del codice JavaScript la situazione si complica ulteriormente, il browser dell’utente si troverebbe ed eseguire scripts malevoli che potrebbero per esempio creare finestre pop-up, rubare i cookies, o reindirizzare il browser ad altri siti. Questo genere di attacchi prende appunto il nome di cross-site scripting. Una curiosità: viene usata la X al posto della C per non confonderlo con i CSS.

PHP ci viene incontro

PHP include una manciata di funzioni per la gestione di codice HTML e altri codici trovati all’interno delle stringhe:

  • htmlspecialchars(): trasforma &, ", &#039, <, >, nelle rispettive entità HTML (&amp;, &quot;, etc.)
  • htmlentities() trasforma tutti i caratteri applicabili nelle proprie entità HTML.
  • strip_tags(): rimuove tutti i tags HTML, JavaScript o PHP.

Sia htmlspecialchars() che htmlentities() ricevono dei parametri opzionali, consultare eventualmente la documentazione online per approfondire.

La funzione strip_tags() accetta un parametro opzionale che indica quali tags non devono essere eliminati:

<?php
$text = '<p>Questo è un paragrafo</p><!-- Commento --> <a href="#">link</a>';
echo strip_tags($text);
echo "n";

// Permette i tags <p> e <a>
echo strip_tags($text, '<p><a>');
?>
Nota: Le funzioni htmlspecialchars_decode() e html_entity_decode() eseguono il compito inverso, convertono le entità HTML nei rispettivi caratteri.
Nota: La strip_tags() non esegue la convalida dei tags HTML, la presenza quindi di tags parziali o rotti può comportare la rimozione di più testo/dati del previsto.
strip_tags("<b grassetto</b>") comporterà la rimozione dell’intera stringa.

I Filtri di Input

A partire da PHP 5.2 c’è inoltre un modo migliore per recuperare i dati inviati dall’utente, sensa utilizzare direttamente le superglobals $_GET e $_POST. Gli sviluppatori di PHP hanno infatti dotato il linguaggio di un sistema di filtraggio che rende la validazione e la sanificazione dei dati banalmente semplice.
Le funzioni filter_input() e filter_input_array() sono in grado di raccogliere dati provenienti da fonti diverse difatti il primo parametro che accettano, può avere i seguenti valori INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, o INPUT_ENV, in base al dato di ingresso che si desidera testare.
Fondamentalmente possiamo avere due tipi di filtri:

  • Validazione: Fa in che modo i dati forniti siano conformi a quelli che ci aspettiamo. In questa modalità, il sistema di filtraggio indicherà (con un valore booleano) se i dati soddisfano un criterio.
  • Sanificazione: Esegue la rimozione dei dati indesiderati dall’input. In questa modalità il sistema di filtraggio restituisce i dati sterilizzati.
Nota: I filtri, ritorneranno il valore della variabile in caso di successo, oppure FALSE in caso contrario.
<?php
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
?>

L’esempio precedente verificherà se il campo email inviato via POST contiene un indirizzo email valido. In caso di successo restituirà l’indirizzo email (cioè il valore del campo) altrimenti FALSE.

<?php
if (filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, 
        array('options' => array('min_range' => 18, 'max_range' => 30)))) {
    echo 'ok puoi entrare';
} else {
    echo 'non sei autorizzato ad accedere';
}
?>

Questo esempio contiene anche un quarto parametro, cioè un array associativo che potrà contenere delle opzioni o dei flags aggiuntivi, qualora il filtro li supporti. Come è facile intuire, passeranno la verifica soltanto valori interi compresi tra 10 e 30.

<?php
/*
dati provenienti da get
$_GET = array(
    'email' => '(mia.email@email.com)',
    'age'   => '3a9',
    'nick'  => '@mynick',
);
*/
echo filter_input(INPUT_GET, 'email', FILTER_SANITIZE_EMAIL); //mia.email@email.com
echo filter_input(INPUT_GET, "number", FILTER_SANITIZE_NUMBER_INT); //39
$nick = filter_input(INPUT_GET, 'nick', FILTER_SANITIZE_ENCODED); //%40mynick
echo 'Trova';
?>

In questo esempio sono stati applicati dei filtri di sanificazione. Direi che non c’è bisogno di aggiungere niente, i commenti, che mostrano il risultato rendono chiaramente l’idea di quello che è successo.

Filtrare un array

Finora abbiamo visto solo operazioni di convalida su singole variabili. I filtri tuttavia possono essere usati anche su un array di valori attraverso la funzione filter_input_array(). Questa funzione è utile per il recupero di molti valori, senza chiamare ripetutamente filter_input().

<?php
/*
dati provenienti da post
$_POST = array(
    'id'         => '123',
    'name'       => 'alfa',
    'preview'    => '<p>preview descrizione<p>',
    'related_id' => array('5', '45', '67', '127'),
);
*/

function formatName($name){
  return ucwords(strtolower($name));
}


$args = array(
    'id'           => array(
                            'filter' => FILTER_VALIDATE_INT,
                            'flags'  => FILTER_REQUIRE_SCALAR,
                           ),
    'name'         => array('filter' =>FILTER_CALLBACK, 
                            'options' =>'formatName'),    
    'description'  => FILTER_SANITIZE_STRING,

    'related_id'   => array('filter'    => FILTER_VALIDATE_INT,
                            'flags'     => FILTER_REQUIRE_ARRAY, 
                            'options'   => array('min_range' => 1, 'max_range' => 100)
                           ),
    'doesnotexist' => FILTER_VALIDATE_INT,
);

var_dump($_POST);
$myinputs = filter_input_array(INPUT_POST, $args);

var_dump($myinputs);
echo "n";

/*
output
array
  'id' => int 123
  'name' => string 'Alfa' (length=4)
  'description' => string 'preview descrizione' (length=19)
  'related_id' => 
    array
      0 => int 5
      1 => int 45
      2 => int 67
      3 => boolean false
  'doesnotexist' => null
*/

Dal precedente frammento di codice possiamo comprendere come si comporta la funzione filter_input_array(), alla quale come secondo parametro viene passato un array associativo avente come chiave il nome dell’input da testare e come valore il filtro da applicarvi. Da notare:

  1. Nel caso vogliamo applicare il filtro “grezzo” passiamo come valore direttamente la costante con il nome del filtro, se vogliamo aggiungervi dei flags o delle opzioni, passiamo invece un array associativo.
  2. L’utilizzo di un filtro personalizzato usando il filtro FILTER_CALLBACK ed una funzione di callback appositamente creata (formatName()).
  3. L’utilizzo dei flags FILTER_REQUIRE_ARRAY e FILTER_REQUIRE_SCALAR che si aspettano rispettivamente che il dato in ingresso sia un array ed uno scalare.
  4. Quando andiamo a testare una variabile inesistente (doesnotexist) viene restituito NULL (e non FALSE).
PHP Filter

I filtri di input sono solo una parte dei filtri messi a disposizione da PHP, per esempio le funzioni filter_var() e filter_var_array() eseguono lo stesso compito dei rispettivi filtri di input, la differenza è che ad essere filtrate saranno delle variabili e non degli input esterni.
La funzione filter_list() ritorna un array di tutti i filtri disponibili e la filter_id() ritorna il numero ID di un filtro specifico.
La funzione filter_has_var() controlla se una variabile di un determinato tipo di input esiste. Ritorna TRUE in caso di successo e FALSE in caso contrario.
Due parole su quest’ultima funzione, che a prima vista può generare confusione, visto che sembra a tutti gli effetti un doppione di isset(). Prendiamo come esempio questo frammento:

<?php
$_GET['test'] = 'presente';

echo isset ($_GET['test']) ? 'Esiste' : 'Non Esiste'; //Esiste
echo filter_has_var(INPUT_GET, 'test') ? 'Esiste' : 'Non Esiste'; //Non Esiste
?>

Come possiamo notare dai commenti, che mostrano il risultato, la funzione filter_has_var() non va ad interrogare l’array superglobal $_GET, ma direttamente il parametro che arriva dalla richiesta, quindi si rivela un metodo migliore da usare, nel caso $_GET o $_POST siano in qualche modo sporcati dallo script PHP.

Conclusioni

Ci sono diversi modi in cui le nostre pagine possono essere oggetto di attacchi XSS, PHP ci offre una vasta gamma di funzioni built-in per difenderci da questa piaga. Funzioni come strip_tags(), htmlentities(), htmlspecialchars(), filter_input() etc. non rappresentano tuttavia la soluzione definitiva, utilizzare per esempio la libreria HTML Purifier potrebbe rappresentare un ulteriore passo in avanti dal punto di vista della sicurezza.
Quello che dobbiamo tenere sempre a mente e l’assoluto divieto di utilizzare nei nostri scripts direttamente $_GET (o $_POST e gli altri) senza una sufficiente azione di purificazione.

Risorse

it.wikipedia.org/wiki/Cross-site_scripting
it2.php.net/manual/en/function.htmlspecialchars.php
it2.php.net/manual/en/function.htmlentities.php
it2.php.net/manual/en/function.strip-tags.php
www.php.net/manual/en/function.filter-input.php
htmlpurifier.org/

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.