Come prevenire attacchi di tipo CSRF in un sito già esistente, in PHP: parte 2
Nell'articolo precedente ci siamo occupati di come proteggere un sito dagli attacchi di tipo Cross-Site Request Forgery (CSRF), implementando un Token.
Se non l'hai già letto, fallo adesso perchè è alla base di quanto di dirò oggi e qui andrò solo ad integrare quanto ci occorre.
Nell'esempio avevamo un form, come questo
<form action="action1.php" method="POST">
<input type="hidden" id="param1" name="param1" value="pippo">
<input type="hidden" id="param2" name="param2" value="pluto">
<input type="submit" value="invia primo form">
</form>
ed una chiamata POST, tramite javascript ed in AJAX, che "posta" tutti i campi del form, e di seguito ne riporto un estratto:
<script>
.........
function handleforms(forms) {
.......
// url to call
var urlToCall = form.action
// params to post
var formData = new FormData(this);
// append csrf
formData.append("csrf", csrf);
// execute ajax call
$.ajax({
url: urlToCall,
type: "POST",
data: formData,
..................
});
.......
}
.........
</script>
Infatti "var formData = new FormData(this);" preleva tutti i campi del form con i relativi valori, e li posta così come sono.
Tuttavia capita spesso di non voler postare tutti i campi, o di postare i campi con nomi diversi dai nomi dei campi presenti nel form, o campi aggiuntivi rispetto a quelli presenti nel form.
L'articolo precedente non copre questa casistica, per cui eccoci nuovamente qui per integrare quanto già spiegato.
Facciamo un nuovo esempio: in questa pagina basica abbiamo un form e, incluso nel footer, un file javascript chiamato "index.js" che gestirà la post via ajax.
<!DOCTYPE html>
<html lang='it'>
<head>
<meta charset='utf-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>
</head>
<body>
<h2 style='text-align:center'>LA MIA PAGINA</p>
<br>
<form action="action1.php">
<input type="hidden" id="param1" name="param1" value="pippo">
<input type="hidden" id="param2" name="param2" value="pluto">
<input type="submit" value="invia primo form" class="bottone">
</form>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script type='text/javascript' src='index.js'></script>
</body>
</html>
Piccole note rispetto all'esempio dell'articolo precedente:
- nel form non ho indicato "method=POST".. tanto quel submit verrà intercettato da javascript e postato via ajax, per cui non ha senso indicarlo, ma nulla ti vieta di metterlo, è semplicemente inutile.
- ho aggiunto una classe "bottone" al pulsante sumbit del form, perchè nel file index.js voglio intercettare il click su questo pulsante.
Questo è il contenuto del file "index.js":
$(document).ready(function() {
// al click
$(document).on("click",".bottone",function(event) {
// blocco la submit
event.preventDefault();
// recupero la action url da chiamare
var form = $(this).closest('form');
var urlToCall= form.attr('action');
// recupero i valori dei campi da postare
var name = document.getElementById('param1').value;
var surname = document.getElementById('param2').value;
var id = 4;
// preparo i campi da postare via ajax
var form_data = new FormData();
form_data.append('name',name);
form_data.append('surname',surname);
form_data.append('id',id);
// effettuo la chiamata post
$.ajax({
url: urlToCall,
type: "POST",
data: form_data,
processData: false,
contentType: false,
async: true,
cache: false,
success: function(data)
{
alert(data)
},
error: function(xhr, ajaxOptions, thrownError){
alert("si è verificato un problema tecnico")
}
});
});
});
Al click sul pulsante di submit del form, il javascript intercetta il click, ed "event.preventDefault();" impedisce di eseguire la normale post, che prevederebbe il cambio di pagina: sarà ajax infatti a fare la chiamata e ricevere la risposta.
Come possiamo vedere, sto effettuando una chiamata POST, ma non passo "param1" bensì il campo "name", e non passo "param2" ma "surname", ed inoltre ho aggiunto un campo "id" che non era presente tra i campi del form.
Nell'articolo precedente avevamo visto come implementare un sistema che si applicasse automaticamente a tutti i form, ma questo prevedeva che postassimo tutti i campi del form, senza selezionarli, senza cambiarne il nome, senza aggiunta di nuovi parametri, come in questo esempio.
Da questo nasce l'esigenza, anche grazie alle vostre segnalazioni, di creare una seconda parte dell'articolo sul CSRF, per andare a coprire davvero tutti i casi, come questo.
In questo javascript "index.js", non dobbiamo fare alcuna modifica.
Quello che dobbiamo fare è, come fatto nel precedente articolo
- includere nella pagina la config.php che a sua volta chiama un metodo che crea (e verifica) il token
- includere nella pagina un meta tag con valore il token generato
- includere nel footer un javascript da usare globalmente, in tutte le pagine del sito, che chiamiamo "global.js": sarà questo a fare la magia e aggiungere a tutte le ajax post, automaticamente, il token, in qualunque pagina siano i form (potrei anche avere 100 form nella stessa pagina o in varie pagine del sito!)
Ci riportiamo quindi in una situazione simile a quella del precedente articolo
<?php
include "config.php";
?>
<!DOCTYPE html>
<html lang='it'>
<head>
<meta charset='utf-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>
<meta name="_csrf" content="<?=$token?>"/>
</head>
<body>
<h2 style='text-align:center'>LA MIA PAGINA</p>
<br>
<form action="action1.php">
<input type="hidden" id="param1" name="param1" value="pippo">
<input type="hidden" id="param2" name="param2" value="pluto">
<input type="submit" value="invia primo form" class="bottone">
</form>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script type='text/javascript' src='index.js'></script>
<script type='text/javascript' src='global.js'></script>
</body>
</html>
Vediamo adesso il contenuto del file "global.js" che varierà rispetto a quello dell'articolo precedente: in questo caso infatti non dobbiamo intercettare le post fatta dai tag <form>, bensì quelle fatte dalle chiamate ajax.
Usando jQuery:
$.ajaxSetup({
beforeSend: function(jqXHR, settings) {
// Check if the request is a POST request
if (settings.type === 'POST') {
// prelevo il csrf
const csrf=document.querySelector('meta[name="_csrf"]').getAttribute('content');
// if settings.data is a string, add the new parameter csrf to the data
if (typeof settings.data === 'string') {
settings.data += '&csrf='+csrf;
}
// if settings.data is an object, extend it with the new parameter csrf
else {
settings.data.append('csrf', csrf);
}
}
}
});
Lo script lavora così: prima di eseguire una qualunque chiamata AJAX ("beforeSend")
- viene verificata se la ajax è una chiamata di tipo POST
- se è una POST, viene prelevato il valore token CSRF dal meta tag ed aggiunto ai parametri della richiesta.
In questo modo possiamo automaticamente aggiungere ad ogni ajax post, in qualunque pagina venga effettuata, il nostro token!
Abbiamo così coperto tutte le possibili casistiche che l'articolo precedente non copriva.
Puoi scaricare il sorgente da GitHub. Il link subito sotto.