SECURITY MIDDLEWARE

Last updated: June 29th 2026

Overview

The framework provides a middleware pipeline for request filtering. Middleware can perform actions before or after the controller runs — CSRF validation, authentication checks, security headers, and rate limiting are all built in.

The Middleware System

Every middleware implements the Simple\Middleware\Middleware interface:

interface Middleware {
    public function handle(Request $request, Closure $next);
}

Call $next($request) to continue to the next middleware or controller. To short-circuit, throw an exception or return early.

Registering Middleware Aliases

Router::middlewareAlias('csrf', Csrf::class);
Router::middlewareAlias('auth', Auth::class);
Router::middlewareAlias('headers', SecurityHeaders::class);
Router::middlewareAlias('rate-limit', RateLimit::class);

Applying Middleware

On route groups (inherited by nested routes):

Router::group(['prefix' => 'admin', 'middleware' => ['auth']], function () {
    Router::get('dashboard', 'Admin@dashboard');
});

On individual routes (fluent):

Router::get('profile', 'User@profile')->middleware('auth');
Router::post('contact', 'Contact@send')->middleware(['csrf']);

Global middleware (runs on every request):

Router::globalMiddleware(['headers']);

Execution order: global → group → route-specific.

CSRF Protection

Protects POST/PUT/PATCH/DELETE requests from cross-site request forgery. Uses hash_equals() for timing-safe comparison.

Two steps required — CSRF is not automatic:

  1. Apply the csrf middleware to the route.
  2. Add {{ csrf_field() }} inside your <form> tag.
// Step 1: Apply middleware to route
Router::post('contact', 'Contact@send')->middleware('csrf');
<!-- Step 2: Add field inside your form -->
<form method="post" action="/contact">
    {{ csrf_field() }}
    <!-- other form fields -->
</form>

The token is auto-generated per session via Session::token(). You can also get the raw token:

{{ csrf_token() }}

The middleware also checks the X-CSRF-TOKEN header for AJAX requests. GET/HEAD/OPTIONS requests are skipped.

Authentication Middleware

Restricts routes to authenticated users. Throws a 401 exception if no user is in the session.

Router::get('dashboard', 'Admin@dashboard')->middleware('auth');

Router::group(['prefix' => 'admin', 'middleware' => ['auth']], function () {
    Router::get('users', 'Admin@users');
    Router::get('settings', 'Admin@settings');
});

Security Headers

Sets security response headers on every request:

HeaderValuePurpose
X-Frame-OptionsDENYClickjacking protection
X-Content-Type-OptionsnosniffMIME-sniffing prevention
Referrer-Policysame-originReferrer leakage prevention
Content-Security-PolicyConfigurable (default: default-src 'self')XSS prevention
Strict-Transport-Securitymax-age=31536000; includeSubDomains (HTTPS only)HSTS enforcement
Permissions-Policygeolocation=(), microphone=(), camera=()Feature restriction

Configure CSP by defining the CSP_POLICY constant in your config:

define('CSP_POLICY', "default-src 'self' https://fonts.googleapis.com");

Register as a global middleware in your route file:

Router::globalMiddleware(['security-headers']);

Or per-route:

Router::set('admin/{action}', [
    'controller' => 'AdminController',
    'middleware'  => ['auth', 'security-headers'],
]);

Rate Limiting

Limits the number of requests per IP + route. File-based storage.

Router::post('auth/authenticate', 'Auth@authenticate')->middleware(['csrf', 'rate-limit']);

Configuration via constants in your config:

define('RATE_LIMIT_MAX_ATTEMPTS', 5);   // requests per window
define('RATE_LIMIT_DECAY_SECONDS', 60); // window in seconds
define('RATE_LIMIT_STORAGE', '../app/storage/framework/rate-limit');

On successful login, call RateLimit::clear() to reset the counter.

Other Security Improvements

  • Session fixation: session_regenerate_id() runs after every successful login.
  • SameSite cookies: session.cookie_samesite = Lax is set on session start.
  • XSS protection: Error page escapes exception output with htmlspecialchars(). Twig enforces autoescape: html.
  • Open redirect: Request::redirect() rejects absolute URLs.
  • Host header injection: HTTP_HOST is validated against a strict regex before use.
  • File upload: Server-side MIME validation via finfo for all allowed extensions.
  • Debug mode: SHOW_ERRORS defaults to false in production.