Cross-Site Request Forgery (CSRF) protection is a critical security feature that helps prevent attackers from tricking users into performing unwanted actions on your application. Gimli provides built-in CSRF protection through the Csrf
class.
The CSRF implementation includes several security features:
random_bytes()
with 32 bytes of entropyTo protect your forms from CSRF attacks, add a hidden field with a CSRF token:
<?php
use Gimli\View\Csrf;
?>
<form method="post" action="/submit">
<input type="hidden" name="csrf_token" value="<?= Csrf::generate() ?>">
<!-- Form fields -->
<button type="submit">Submit</button>
</form>
When processing form submissions, validate the CSRF token:
<?php
use Gimli\View\Csrf;
use Gimli\Http\Response;
class FormController {
public function processForm(array $post_data): Response {
// Validate CSRF token
if (!Csrf::validateRequest($post_data)) {
return new Response("Invalid request", 403);
}
// Process form data
// ...
return new Response("Form processed successfully");
}
}
For AJAX requests, you can include the CSRF token in headers:
<?php
// In your view or JavaScript initialization code
$csrf_token = Csrf::getToken();
?>
<script>
// Add CSRF token to all AJAX requests
const csrfToken = '<?= $csrf_token ?>';
// Using fetch API
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
// Using jQuery
$.ajaxSetup({
headers: {
'X-CSRF-Token': csrfToken
}
});
</script>
On the server side, validate the token from the header:
<?php
use Gimli\View\Csrf;
class ApiController {
public function processApiRequest() {
$headers = getallheaders();
$token = $headers['X-CSRF-Token'] ?? '';
if (!Csrf::verify($token)) {
header('HTTP/1.1 403 Forbidden');
echo json_encode(['error' => 'CSRF validation failed']);
exit;
}
// Process API request
}
}
For SPAs that make multiple AJAX requests, you can reuse an existing token:
<?php
// In your initial page load
$csrf_token = Csrf::getToken();
?>
<script>
// Store the token in a JavaScript variable
const csrfToken = '<?= $csrf_token ?>';
// Function to make authenticated requests
function makeAuthenticatedRequest(url, method, data) {
return fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
}
</script>
Tokens are generated using cryptographically secure random bytes:
$token = bin2hex(random_bytes(self::TOKEN_LENGTH)); // 32 bytes = 64 hex characters
Tokens are stored in the session with expiration timestamps:
$tokens[$token] = time() + self::TOKEN_EXPIRY; // Default 900 seconds (15 minutes)
The verification process includes:
To prevent session bloat from too many tokens:
// Maximum of 10 tokens per session
if (count($tokens) >= self::MAX_TOKENS_PER_SESSION) {
// Remove oldest token
$oldest_key = array_key_first($tokens);
unset($tokens[$oldest_key]);
}
You can create a CSRF middleware to protect all routes that accept POST/PUT/PATCH/DELETE requests:
<?php
use Gimli\Middleware\Middleware_Interface;
use Gimli\Middleware\Middleware_Response;
use Gimli\Http\Request;
use Gimli\View\Csrf;
class CsrfMiddleware implements Middleware_Interface {
public function __construct(
protected Request $Request
) {}
public function process(): Middleware_Response {
// Skip CSRF check for GET and HEAD requests
if (in_array($this->Request->REQUEST_METHOD, ['GET', 'HEAD'])) {
return new Middleware_Response(true);
}
// Check for CSRF token in POST data
if ($this->Request->REQUEST_METHOD === 'POST' && isset($_POST['csrf_token'])) {
if (Csrf::verify($_POST['csrf_token'])) {
return new Middleware_Response(true);
}
}
// Check for CSRF token in headers (for AJAX/API)
$headers = getallheaders();
if (isset($headers['X-CSRF-Token'])) {
if (Csrf::verify($headers['X-CSRF-Token'])) {
return new Middleware_Response(true);
}
}
// CSRF validation failed
return new Middleware_Response(false, '/error/csrf');
}
}
Apply the middleware to routes:
<?php
use Gimli\Router\Route;
// Apply to all routes in a group
Route::group('/admin', function() {
Route::get('/dashboard', [AdminController::class, 'dashboard']);
Route::post('/settings', [AdminController::class, 'saveSettings']);
}, [CsrfMiddleware::class]);
// Or apply to specific routes
Route::post('/login', [AuthController::class, 'login'])->addMiddleware(CsrfMiddleware::class);
Csrf::generate()
- Creates a new token for formsCsrf::getToken()
- Gets or creates a token for AJAX requestsSameSite=Strict
by defaultToken Expiration: If users take too long to submit forms, tokens may expire. Consider extending the expiration time for longer forms.
Multiple Forms: Each form generates a unique token. If a user opens multiple tabs, ensure your application handles multiple valid tokens.
AJAX Polling: For applications that make frequent AJAX requests, use Csrf::getToken()
to reuse existing tokens when possible.
The CSRF protection is configured with sensible defaults:
These values are defined as constants in the Csrf
class and can be modified if needed for your specific application requirements.