J’ai déjà parlé sur ce blog des failles de sécurité de type CSRF (Cross-Site Request Forgery) : dans cet article et suite à ma conférence sur la sécurité dans les développements web.
En évitant de répéter ce que j’ai déjà écrit, je vais passer en revue trois techniques qui permettent d’éviter les attaques CSRF. Ces techniques s’utilisent conjointement, et sont plus simples que la méthode la plus utilisée depuis une dizaine d’années, à savoir l’ajout de jetons à tous les liens.
Ces techniques consistent à :
- vérifier que l’en-tête REFERER reçu correspond bien au site courant,
- n’accepter que des requêtes en POST,
- créer des cookies de session avec le paramètre
SameSite=Lax.
Je vais présenter leur mise en œuvre en PHP pur, et avec les frameworks Laravel, Symfony et Temma.
Vérification du REFERER
PHP pur
Voici le code d’une fonction qui vérifie si le REFERER correspond au site courant. Il faut appeler cette fonction aux bons endroits, en fonction de l’application web.
function refererIsSameDomain() : bool {
if (!isset($_SERVER['HTTP_REFERER']))
return false;
$referer = parse_url($_SERVER['HTTP_REFERER']);
$host = $_SERVER['HTTP_HOST'] ?? '';
return (($referer['host'] ?? '') == $host);
}Langage du code : PHP (php)
Laravel
Avec Laravel, on pourra créer un middleware, à déclarer dans app/Http/Kernel.php, et à utiliser dans la route.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VerifyRefererDomain {
public function handle(Request $request, Closure $next) {
$referer = $request->headers->get('referer');
$host = $request->getHost();
if (!$referer || parse_url($referer, PHP_URL_HOST) !== $host)
abort(403, 'Accès interdit : Referer invalide.');
return $next($request);
}
}Langage du code : PHP (php)
Symfony
Avec Symfony, on peut simplement mettre ce code dans l’action à protéger :
$referer = $request->headers->get('referer');
if (!$referer || parse_url($referer, PHP_URL_HOST) != $request->getHost()) {
throw new AccessDeniedHttpException('Referer invalide.');
}Langage du code : PHP (php)
Une autre solution serait de créer un attribut custom, puis de créer un RequestListener ou un ArgumentResolver. À mes yeux, c’est un poil complexe.
Temma
Avec Temma, on va utiliser un attribut qui est fait pour ça, et qu’on peut appliquer soit aux contrôleurs, soit aux actions.
use \Temma\Attributes\Referer as TµReferer;
#[TµReferer(true)]
class User extends \Temma\Web\Controller {
// ...
}Langage du code : PHP (php)
Plutôt simple et limpide.
Accepter les requêtes POST uniquement
PHP pur
Un peu comme pour le REFERER, voici une fonction qui permet de vérifier si la requête est bien en POST, à appeler là aussi aux endroits qui le nécessitent :
function isPostRequest() : bool {
return (($_SERVER['REQUEST_METHOD'] ?? '') == 'POST');
}Langage du code : PHP (php)
Laravel
Ici, c’est la configuration de la route qui sert à définir qu’on n’accepte que du POST :
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserController;
Route::post(
'/user/del/{id}',
[UserController::class, 'delete']
)->name('user.delete');Langage du code : PHP (php)
Symfony
Dans Symfony, c’est aussi dans la déclaration de la route qu’on va spécifier la méthode POST. À la différence de Laravel, la route ne se définit pas en configuration, mais dans un attribut.
use Symfony\Bundle\FrameworkBundle\Controller\Abstractcontroller;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends AbstractController {
#[Route(
'/user/del/{idUser}',
name: 'user_delete',
methods: ['POST'],
requirements: ['idUser' => '\d+']
)]
public function deleteUser() {
// ...
}
}Langage du code : PHP (php)
Temma
Avec Temma, on a là encore un attribut qui peut être appliqué à un contrôleur complet ou à une action.
use \Temma\Attributes\Methods\Post as TµPost;
class User extends \Temma\Web\Controller {
#[TµPost]
public function del(int $userId) {
// ...
}
}Langage du code : PHP (php)
Cookies SameSite=Lax
La dernière technique à utiliser pour contrecarrer les attaques CSRF, c’est de créer des cookies de session avec le paramètre SameSite à la valeur Lax (ou Strict). Mais ce n’est pas un sujet, tous les frameworks gèrent cela correctement.
En PHP pur, si on utilise les sessions natives de PHP, on pourra écrire :
$params = session_get_cookie_params();
session_set_cookie_params([
'lifetime' => $params['lifetime'],
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();Langage du code : PHP (php)
Si par contre vous avez votre propre système de sessions :
$options = [
'expires' => $timestamp,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
'domain' => $host,
];
setcookie($cookieName, $sessionId, $options);Langage du code : PHP (php)