Skip to content

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.

Terminal window
composer require marko/security

Requires marko/session and marko/encryption for CSRF token management.

All three middleware classes read from config/security.php:

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'",
],
];

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

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.

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.

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.

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

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..."
use Marko\Security\Contracts\CsrfTokenManagerInterface;
public function get(): string; // Get token, generating one if none exists
public function validate(string $token): bool; // Validate against stored token
public function regenerate(): string; // Regenerate token, replacing the previous one
use Marko\Security\Middleware\CsrfMiddleware;
public function handle(Request $request, callable $next): Response;
use Marko\Security\Middleware\CorsMiddleware;
public function handle(Request $request, callable $next): Response;
use Marko\Security\Middleware\SecurityHeadersMiddleware;
public function handle(Request $request, callable $next): Response;
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;
use Marko\Security\Exceptions\SecurityException;
public function getContext(): string;
public function getSuggestion(): string;
use Marko\Security\Exceptions\CsrfTokenMismatchException;
public static function invalidToken(): self;