Na przykładzie PHPiggy zobaczymy jak można podejść do zagadnienia CSRF przy pomocy middleware, bo poza tym to już zdaje się temat bardzo solidnie omówiliśmy. Do dzieła.

Na początek SessionMiddleware:

class SessionMiddleware implements MiddlewareInterface
{
  public function process(callable $next)
  {
    if (session_status() === PHP_SESSION_ACTIVE) {
      throw new SessionException("Session already active.");
    }

    if (headers_sent($filename, $line)) {
      throw new SessionException("Headers already sent. Consider enabling output buffering. Data outputted from {$filename} - Line: {$line}");
    }

    session_set_cookie_params([
      'secure' => $_ENV['APP_ENV'] === "production",
      'httponly' => true,
      'samesite' => 'lax'
    ]);

    session_start();

    $next();

    session_write_close();
  }
}

Już omawialiśmy co autor miał na myśli i jak to zaimplementować u siebie. Teraz tworzenie tokena w sesji:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Framework\Contracts\MiddlewareInterface;
use Framework\TemplateEngine;

class CsrfTokenMiddleware implements MiddlewareInterface
{
  public function __construct(private TemplateEngine $view)
  {
  }

  public function process(callable $next)
  {
    $_SESSION['token'] = $_SESSION['token'] ?? bin2hex(random_bytes(32));

    $this->view->addGlobal('csrfToken', $_SESSION['token']);

    $next();
  }
}

Ciekawa sprawa – jeżeli sesja jest tokena pozbawiona to on jest w sesji tworzony. Dopiero poprzez sesję jest wstrzykiwany przez TemplateEngine do formularza jako value pola input typu hidden.

Teraz sprawdzanie tokena:

class CsrfGuardMiddleware implements MiddlewareInterface
{
  public function process(callable $next)
  {
    $requestMethod = strtoupper($_SERVER['REQUEST_METHOD']);
    $validMethods = ['POST', 'PATCH', 'DELETE'];

    if (!in_array($requestMethod, $validMethods)) {
      $next();
      return;
    }

    if ($_SESSION['token'] !== $_POST['token']) {
      redirectTo('/');
    }

    unset($_SESSION['token']);

    $next();
  }
}

Jeżeli metoda nie wymaga tokena – idziemy dalej.

Jeżeli tokeny z „input type hidden token” się nie zgadzają z tym, co w sesji to ktoś tu majstruje zamiast „legitnie” wysyłać formularz z naszej strony – redirect zatem.

Jeżeli wszystko ok, to znaczy warunki „failowe” nie padły, to jeszcze zanim zawołamy next, usuwamy token, który już został użyty.