WordPress: introduzione alla sicurezza dei temi

Short link

In questo articolo affronteremo il tema della sicurezza dei temi di WordPress. Si possono riassumere le pratiche consigliate per questo argomento in una sola espressione: siate paranoici.

Tutti sanno che usiamo WordPress: l’attacco

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 in WordPress.

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 è il 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. Se questi vincoli non esistono, il codice malevolo potrà propagarsi su più utenze e quindi su più siti.

A questo punto il danno è fatto. La soluzione come al solito è la prevenzione.

Prevenire l’input imprevisto

PHP gestisce i verbi 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 dobbiamo:

  • Filtrare le query SQL con il metodo prepare() della classe wpdb.
  • Filtrare ogni variabile 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 ecc. ).
  • Se si gestiscono file (upload, manipolazione, inclusione ecc.) verificare sempre che il file sia del tipo corretto e che l’operazione che l’utente vuole eseguire è consentita.
  • Se si usano le sessioni PHP, validare sempre la sessione corrente tramite token o autenticazione utente.

Guardiamo questo codice:

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;
  }
}

È sufficiente? Assolutamente no. Quindi usiamo wpdb::prepare():

$prepared = $wpdb->prepare( “SELECT * FROM reservations
WHERE id = %d”, array( $sane_res_id ) );
$reservation = $wpdb->get_results( $prepared );

Consideriamo ora quest’altro esempio:

$title = $_GET[‘title’];
echo ‘<h1>’ . $title . ‘</h1>’;

Questo è un esempio tipico in cui può verificarsi un attacco XSS, 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 temi e 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 tema o del plugin.

Osserviamo questo codice volutamente scritto in semplice PHP:


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 verificato che il file richiesto sia effettivamente un file PDF verificando il suo MIME Type. Se non avessimo fatto questa verifica, 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 custom: cercate di utilizzare sempre la Media Library e le sue API per la gestione degli allegati e dei file.

Ora immaginiamo il caso di un carrello di 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 le 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 deve essere distrutta e con essa il token di sessione.

Conclusione

Questa introduzione alla sicurezza dei temi di WordPress è da collegarsi direttamente al tema più generale della sicurezza in PHP. Essere a conoscenza dei rischi è il primo passo per evitarli.

L'autore

Gabriele Romanato, sviluppatore web full stack specializzato in siti, applicativi web ed e-commerce con Node.js e PHP.