In questo capitolo affronteremo il tema della sicurezza dei plugin di WordPress. Si possono riassumere le pratiche consigliate per questo argomento in una sola espressione: "essere paranoici" o, se non altro, estremamente prudenti.
Analisi di una procedura per l'attacco contro un sito basato su WordPress
Anche se esistono plugin che affermano di nascondere un’installazione di WordPress, esistono tuttavia tool automatici per la scansione dei siti che possono letteralmente eseguire una radiografia di un sito. Non solo un malintenzionato sa che usiamo WordPress, ma sa anche che plugin stiamo utilizzando, quali sono i temi installati e molto altro ancora. Questo è il prezzo da pagare per aver pubblicato un sito pubblico.
Una volta effettuata la scansione, un malintenzionato di solito passa alla ricerca di vulnerabilità nei temi e nei plugin e, se WordPress non è aggiornato, anche nel Core di WordPress stesso. Trovata la vulnerabilità, l’attaccante può usare un exploit già disponibile in rete o scriverne uno (questo nel caso in cui il malintenzionato sia molto esperto).
All’exploit, eseguito da remoto su un file specifico dell’installazione di WordPress, segue il payload, ossia il codice malevolo che verrà eseguito. A questo punto se PHP viene eseguito con i vincoli dell’utenza corrente, i danni causati interesseranno lo spazio Web dell’utente stesso. Se questi vincoli non dovessero esistere, il codice malevolo potrà propagarsi su più utenze e quindi su più siti Internet.
A questo punto il danno sarà ormai fatto. L'unica soluzione valida rimane come al solito è la prevenzione.
Difendersi dagli input imprevisti
PHP gestisce i metodi HTTP GET e POST e i file con array superglobali. Quando dobbiamo manipolare un input esterno, dobbiamo sempre validare e filtrare i dati in ingresso. In altre parole dobbiamo considerare qualsiasi forma di input come potenzialmente pericoloso ed agire di conseguenza.
Quindi sarà necessario:
- filtrare le query SQL con il metodo
prepare()
della classewpdb
; - filtrare ogni variabile veicolata tramite GET o POST da inserire nel flusso di output con le funzioni di escape (con prefisso
esc_
) di WordPress; - validare i dati inseriti dagli utenti secondo il formato dati richiesto (numero, stringa, indirizzo e-mail.. );
- se si gestiscono file (upload, manipolazione, inclusione..) verificare sempre che il file sia del tipo corretto e che l’operazione che l’utente vuole eseguire sia consentita;
- se si usano le sessioni di PHP, validare sempre la sessione corrente tramite token o autenticazione utente.
Si analizzi il codice seguente:
global $wpdb;
$reservation_id = $_GET[‘res_id’];
$reservation = $wpdb->get_results( “SELECT * FROM reservations WHERE id = $reservation_id” );
Tendenzialmente la variabile GET potrebbe contenere qualsiasi valore ed è una porta spalancata per un tentativo di SQL injection. Sappiamo che ci dobbiamo aspettare un valore numerico intero. Quindi come validazione preliminare e grossolana potremmo scrivere:
$sane_res_id = 0;
if( is_numeric( $reservation_id ) ) {
$r_id = intval( $reservation_id );
if( filter_var( $r_id, FILTER_VALIDATE_INT ) ) {
$sane_res_id = $r_id;
}
}
Tale accorgimento è sufficiente ai fini della sicurezza? Assolutamente no. Quindi usiamo wpdb::prepare()
:
$prepared = $wpdb->prepare( “SELECT * FROM reservations
WHERE id = %d”, array( $sane_res_id ) );
$reservation = $wpdb->get_results( $prepared );
Sulla base di quanto già detto, consideriamo ora quest’altro esempio:
$title = $_GET[‘title’];
echo ‘<h1>’ . $title . ‘</h1>’;
Questo è un esempio tipico in cui può verificarsi un attacco di tipo XSS (Cross-site scripting), ossia l’inserimento nella pagina di codice JavaScript malevolo. Per prevenirlo dobbiamo fare in modo che la stringa passata non contenga appunto tale codice:
$raw_title = wp_kses( $title );
echo esc_html( $raw_title );
Qui abbiamo semplicemente combinato le funzioni di WordPress wp_kses() ed esc_html() per essere sicuri che la stringa non contenga nessun tag HTML.
Abbiamo visto come i malintenzionati cerchino di eseguire il codice dei file da remoto. Per impedire questa pratica possiamo utilizzare una costante PHP globale nei nostri plugin:
define( ‘MY_PLUGIN_C’, ‘1’ );
Quindi nei nostri file eseguiamo questa verifica sempre all’inizio:
if( !defined( ‘MY_PLUGIN_C’ ) ) {
exit();
}
In questo modo il nostro codice potrà essere eseguito solo all’interno del contesto del plugin.
Osserviamo quindi il codice mostrato di seguito:
if( isset( $_GET['pdf'] ) ) {
$pdf_base_url = 'http://sito.com/wp-content/uploads/pdf/';
$pdf_base_path = $_SERVER['DOCUMENT_ROOT'] . '/wp-content/uploads/pdf/';
$pdf = $_GET['pdf'];
$pdf_full_url = $pdf_base_url . $pdf;
$pdf_full_path = $pdf_base_path . $pdf;
if( preg_match( '/\.pdf$/', $pdf ) ) {
if( file_exists( $pdf_full_path ) ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mimetype = finfo_file( $finfo, $pdf_full_path );
finfo_close( $finfo );
if( $mimetype == 'application/pdf' ) {
header( 'Content-type: application/pdf' );
header( 'Content-Disposition: attachment; filename="' . $pdf . '"' );
readfile( $pdf_full_url );
}
}
}
}
Una variabile GET contiene il nome di un file PDF da scaricare. I PDF nel nostro esempio sono stati uploadati tramite FTP perché troppo pesanti per la Media Library. Nell'esempio abbiamo verificato che il nome del file sia valido in prima istanza. Quindi abbiamo controllato che il file richiesto sia effettivamente un file PDF verificando il suo MIME Type. Se non avessimo eseguito questa operazione, si sarebbe potuto scaricare qualsiasi tipo di file.
La verifica del MIME Type si rivela fondamentale anche nella gestione degli upload. In questo caso è inutile implementare un sistema di upload personalizzato, sarebbe meglio invece cercare di utilizzare sempre la Media Library e le sue API per la gestione degli allegati e dei file.
Ora, si ipotizzi il caso di un carrello per un e-commerce. Quando l'utente sceglie un prodotto, questo viene aggiunto all'array superglobale $_SESSION
. Ma come facciamo a sapere che sia sempre lo stesso utente ad effettuare tali operazioni?
In questo caso dobbiamo impostare un token di sessione:
if( !isset( $_SESSION['my-token'] ) ) {
$token = md5( $_SERVER['HTTP_USER_AGENT'] );
$_SESSION['my-token'] = $token;
}
Quindi ad ogni cambio pagina va verificato il token creato:
if( isset( $_SESSION['my-token'] ) ) {
$token = md5( $_SERVER['HTTP_USER_AGENT'] );
if( $_SESSION['my-token'] == $token ) {
//...
}
}
Alla fine della procedura d'acquisto la sessione dovrà essere distrutta e con essa il token di sessione.
Conclusione
Questa introduzione alla sicurezza dei plugin di WordPress è da ricollegarsi direttamente al tema più generale della sicurezza in PHP. Essere a conoscenza dei rischi è il primo passo per evitarli.