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.
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.
Router::middlewareAlias('csrf', Csrf::class);
Router::middlewareAlias('auth', Auth::class);
Router::middlewareAlias('headers', SecurityHeaders::class);
Router::middlewareAlias('rate-limit', RateLimit::class);
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.
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:
csrf middleware to the route.{{ 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.
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');
});
Sets security response headers on every request:
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options | DENY | Clickjacking protection |
X-Content-Type-Options | nosniff | MIME-sniffing prevention |
Referrer-Policy | same-origin | Referrer leakage prevention |
Content-Security-Policy | Configurable (default: default-src 'self') | XSS prevention |
Strict-Transport-Security | max-age=31536000; includeSubDomains (HTTPS only) | HSTS enforcement |
Permissions-Policy | geolocation=(), 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'],
]);
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.
session_regenerate_id() runs after every successful login.session.cookie_samesite = Lax is set on session start.htmlspecialchars(). Twig enforces autoescape: html.Request::redirect() rejects absolute URLs.HTTP_HOST is validated against a strict regex before use.finfo for all allowed extensions.SHOW_ERRORS defaults to false in production.