marko/security
CSRF protection, CORS handling, and security headers middleware --- secure your routes with drop-in middleware. Three middleware classes cover the most common web security needs: CsrfMiddleware validates tokens on state-changing requests, CorsMiddleware handles preflight and cross-origin headers, and SecurityHeadersMiddleware adds protective response headers (HSTS, CSP, X-Frame-Options, etc.). All are configured via config/security.php.
Installation
Section titled “Installation”composer require marko/securityRequires marko/session and marko/encryption for CSRF token management.
Configuration
Section titled “Configuration”All three middleware classes read from config/security.php:
return [ 'csrf' => [ 'session_key' => '_csrf_token', ], 'cors' => [ 'allowed_origins' => ['https://example.com'], 'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'], 'allowed_headers' => ['Content-Type', 'Authorization'], 'max_age' => 3600, ], 'headers' => [ 'x_content_type_options' => 'nosniff', 'x_frame_options' => 'DENY', 'x_xss_protection' => '1; mode=block', 'strict_transport_security' => 'max-age=31536000; includeSubDomains', 'referrer_policy' => 'strict-origin-when-cross-origin', 'content_security_policy' => "default-src 'self'", ],];CSRF Protection
Section titled “CSRF Protection”Apply CsrfMiddleware to routes that accept form submissions:
use Marko\Routing\Attributes\Post;use Marko\Routing\Attributes\Middleware;use Marko\Security\Middleware\CsrfMiddleware;
class FormController{ #[Post('/contact')] #[Middleware(CsrfMiddleware::class)] public function submit(): Response { // Token validated automatically return new Response('Submitted'); }}The middleware checks _token in POST data or the X-CSRF-TOKEN header. Safe methods (GET, HEAD, OPTIONS) are skipped automatically.
Include the token in forms:
use Marko\Security\Contracts\CsrfTokenManagerInterface;
class ContactController{ public function __construct( private readonly CsrfTokenManagerInterface $csrfTokenManager, ) {}
public function form(): Response { $token = $this->csrfTokenManager->get(); // Render form with <input type="hidden" name="_token" value="$token"> }}CORS Middleware
Section titled “CORS Middleware”Handle cross-origin requests and preflight OPTIONS responses:
use Marko\Routing\Attributes\Get;use Marko\Routing\Attributes\Middleware;use Marko\Security\Middleware\CorsMiddleware;
class ApiController{ #[Get('/api/products')] #[Middleware(CorsMiddleware::class)] public function list(): Response { return new Response('Products'); }}Configure allowed origins, methods, and headers in config/security.php under the cors key (see Configuration above). When a request includes an Origin header that matches the allowed origins list, the middleware adds the appropriate CORS headers. For preflight OPTIONS requests, it short-circuits with a 204 response containing Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age headers. Use '*' in allowed_origins to permit any origin.
Security Headers Middleware
Section titled “Security Headers Middleware”Add protective HTTP headers to all responses:
use Marko\Routing\Attributes\Middleware;use Marko\Security\Middleware\SecurityHeadersMiddleware;
#[Middleware(SecurityHeadersMiddleware::class)]Headers are configured in config/security.php under the headers key (see Configuration above). Empty values are omitted from the response --- only headers with non-empty values are added.
Using the CSRF Token Manager Directly
Section titled “Using the CSRF Token Manager Directly”Regenerate tokens (e.g., after login):
$newToken = $this->csrfTokenManager->regenerate();Validate manually:
if (!$this->csrfTokenManager->validate($submittedToken)) { // Invalid token}Tokens are generated using 32 random bytes encrypted via marko/encryption and stored in the session. The validate() method uses timing-safe comparison (hash_equals) to prevent timing attacks.
Customization
Section titled “Customization”Replace CsrfTokenManager via Preferences to change token generation or storage:
use Marko\Core\Attributes\Preference;use Marko\Security\CsrfTokenManager;
#[Preference(replaces: CsrfTokenManager::class)]class MyCsrfTokenManager extends CsrfTokenManager{ public function get(): string { // Custom token retrieval logic }}Exceptions
Section titled “Exceptions”CsrfMiddleware throws CsrfTokenMismatchException when validation fails. This exception extends SecurityException, which provides rich context and suggestions --- consistent with Marko’s loud-errors principle:
use Marko\Security\Exceptions\CsrfTokenMismatchException;
// Thrown automatically by CsrfMiddleware:// message: "CSRF token validation failed."// context: "The submitted CSRF token does not match the token stored in the session..."// suggestion: "Ensure your form includes a valid CSRF token field (_token) or X-CSRF-TOKEN header..."API Reference
Section titled “API Reference”CsrfTokenManagerInterface
Section titled “CsrfTokenManagerInterface”use Marko\Security\Contracts\CsrfTokenManagerInterface;
public function get(): string; // Get token, generating one if none existspublic function validate(string $token): bool; // Validate against stored tokenpublic function regenerate(): string; // Regenerate token, replacing the previous oneCsrfMiddleware
Section titled “CsrfMiddleware”use Marko\Security\Middleware\CsrfMiddleware;
public function handle(Request $request, callable $next): Response;CorsMiddleware
Section titled “CorsMiddleware”use Marko\Security\Middleware\CorsMiddleware;
public function handle(Request $request, callable $next): Response;SecurityHeadersMiddleware
Section titled “SecurityHeadersMiddleware”use Marko\Security\Middleware\SecurityHeadersMiddleware;
public function handle(Request $request, callable $next): Response;SecurityConfig
Section titled “SecurityConfig”use Marko\Security\Config\SecurityConfig;
public function csrfSessionKey(): string;public function corsAllowedOrigins(): array;public function corsAllowedMethods(): array;public function corsAllowedHeaders(): array;public function corsMaxAge(): int;public function headerXContentTypeOptions(): string;public function headerXFrameOptions(): string;public function headerXXssProtection(): string;public function headerStrictTransportSecurity(): string;public function headerReferrerPolicy(): string;public function headerContentSecurityPolicy(): string;SecurityException
Section titled “SecurityException”use Marko\Security\Exceptions\SecurityException;
public function getContext(): string;public function getSuggestion(): string;CsrfTokenMismatchException
Section titled “CsrfTokenMismatchException”use Marko\Security\Exceptions\CsrfTokenMismatchException;
public static function invalidToken(): self;