CSRF Protection in Leaf MVC
MVC form security
Protect forms with middleware and a Blade directive.
Leaf MVC can install CSRF protection, wire the middleware, and let your views render tokens with @csrf so state-changing requests stay trusted.
A CSRF (Cross-Site Request Forgery) attack tricks a user into performing unwanted actions on your website without their knowledge. This can be done by sending a request to your website from another website the user is logged into.
How does CSRF work?
If you're not familiar with CSRF attacks, this amazing explanation from the Laravel documentation will help you understand how they work.
Imagine your application has a /user/email route that accepts a POST request to change the authenticated user's email address. Most likely, this route expects an email input field to contain the email address the user would like to begin using.
Without CSRF protection, a malicious website could create an HTML form that points to your application's /user/email route and submits the malicious user's own email address:
<form action="https://your-application.com/user/email" method="POST">
<input type="email" value="malicious-email@example.com">
</form>
<script>
document.forms[0].submit();
</script>If the malicious website automatically submits the form when the page is loaded, the malicious user only needs to lure an unsuspecting user of your application to visit their website and their email address will be changed in your application.
To prevent this vulnerability, we need to inspect every incoming
POST,PUT,PATCH, orDELETErequest for a secret session value that the malicious application is unable to access.
Setting up
To get started, install the CSRF module:
leaf install csrfcomposer require leafs/csrfOnce installed, Leaf MVC will automatically setup a CSRF middleware that will protect your application from CSRF attacks. This middleware will verify that all data coming from forms has a valid CSRF token.
Protecting your forms
To protect your forms, you need to add the CSRF token to your form data. You can do this by adding the @csrf directive to your form:
<form action="/user/email" method="POST">
@csrf
<input type="email" name="email" value="email">
<button type="submit">Submit</button>
</form>The @csrf directive will generate a hidden input field containing the CSRF token. This token will be validated by the CSRF middleware when the form is submitted.
@csrf directive
We recommend using the @csrf directive on all your forms, even if you do not have the CSRF middleware enabled. Leaf will only process the directive if the CSRF middleware is enabled, but you will have the added security of CSRF protection if you decide to enable it in the future simply by adding the middleware to your application.
X-CSRF-Token Header
Leaf MVC also checks for the X-CSRF-Token header in requests, allowing single-page applications and mobile apps to handle CSRF protection without relying on PHP forms. If the header is present, Leaf MVC will use its token to verify the request.
fetch('https://your-application.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': 'YOUR_CSRF_TOKEN',
},
body: JSON.stringify({ name: 'John Doe' }),
});final response = await http.post(
Uri.parse('https://your-application.com/submit'),
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': 'YOUR_CSRF_TOKEN',
},
body: jsonEncode({'name': 'John Doe'}),
);let url = URL(string: "https://your-application.com/submit")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("YOUR_CSRF_TOKEN", forHTTPHeaderField: "X-CSRF-Token")
request.httpBody = try? JSONSerialization.data(withJSONObject: ["name": "John Doe"])
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// handle response
}import requests
url = 'https://your-application.com/submit'
headers = {
'Content-Type': 'application/json',
'X-CSRF-Token': 'YOUR_CSRF_TOKEN',
}
data = {'name': 'John Doe'}
response = requests.post(url, headers=headers, json=data)This will send a POST request to /submit with the CSRF token in the X-CSRF-Token header. The CSRF module will automatically verify the token and allow the request to go through if the token is valid.
Single-page applications
If your frontend is an SPA, you usually don't need to pass the token around manually. When CSRF protection is enabled, Leaf sets a JavaScript-readable XSRF-TOKEN cookie (with SameSite=Lax, and marked secure on https). Clients like Axios and Inertia read this cookie automatically and echo it back as an X-XSRF-TOKEN header on every request, which Leaf accepts during verification. That means an Axios-based frontend gets CSRF protection with no extra setup.
If you'd rather not have the cookie set, you can turn it off with the cookie key in your published config:
'cookie' => false,Rotating tokens
By default, Leaf generates one CSRF token per session and keeps it for the lifetime of that session. This plays nicely with multiple open tabs, since every tab shares the same valid token.
If you want stricter protection, you can make tokens single-use with the rotate key in your published config. With rotation on, every verified request throws away the used token and issues a fresh one:
'rotate' => true,The trade-off: if a user has a form open in one tab and submits something in another, the first tab's token becomes stale and its submission will fail. Rotation is stricter, but per-session tokens are friendlier for multi-tab apps.
Separately from rotation, you can issue a fresh token yourself with regenerate(). We recommend doing this when a user logs in, so the token from the anonymous session doesn't carry over:
csrf()->regenerate(); // returns the new tokenDisplaying the generated token
The CSRF module also provides a token() method that returns the CSRF token. You can use this method to display the token in your views or to send the token to your frontend.
Keep your token private
Be careful not to expose the token to the public, as it can be used to bypass CSRF protection.
$csrfToken = csrf()->token();Configuring the CSRF module
By default, the CSRF module will be enabled for all your POST, PUT, PATCH, and DELETE requests. If you want to exclude certain methods or make some other relevant changes, you will need to publish the configuration file:
leaf config:publish csrfThis will create a config/csrf.php file in your application, which looks like this:
<?php
return [
/*
|----------------------------------------------------------------
| Enable/Disable CSRF Protection
|----------------------------------------------------------------
|
| Leaf will automatically generate a CSRF token for each active
| session. We still allow you to manually enable or disable
| CSRF protection for your app by uncommenting the line below.
|
*/
// 'enabled' => true,
/*
|----------------------------------------------------------------
| Secret
|----------------------------------------------------------------
|
| This is the secret key used to generate the CSRF token. It is
| combined with a random string to generate the token.
|
*/
'secret' => _env('APP_KEY', '@kor_leaf$0Secret!!_'),
/*
|----------------------------------------------------------------
| Secret Key
|----------------------------------------------------------------
|
| This is the key under which the token will be stored in the
| session. It can also be used to retrieve the token from the
| request headers.
|
*/
'secretKey' => 'X-Leaf-CSRF-Token',
/*
|----------------------------------------------------------------
| Route Exceptions
|----------------------------------------------------------------
|
| This is a list of routes that will be excluded from CSRF
| verification. This is useful for APIs that need to bypass
| the CSRF verification. You can use routes exactly as you
| defined them, eg: `/profile`, `/blog/(\d+)`, `/post/{id}`
|
*/
'except' => [],
/*
|----------------------------------------------------------------
| Configure allowed HTTP methods
|----------------------------------------------------------------
|
| This is a list of HTTP methods that are the CSRF module will
| be active on. All other methods will be ignored.
|
*/
'methods' => ['POST', 'PUT', 'PATCH', 'DELETE'],
/*
|----------------------------------------------------------------
| Rotate tokens
|----------------------------------------------------------------
|
| When set to true, tokens become single-use: every verified
| request discards the used token and issues a fresh one. The
| default (false) keeps one token per session, which is
| friendlier for apps with multiple open tabs.
|
*/
'rotate' => false,
/*
|----------------------------------------------------------------
| SPA cookie
|----------------------------------------------------------------
|
| When enabled, Leaf sets a JS-readable XSRF-TOKEN cookie that
| clients like Axios echo back as an X-XSRF-TOKEN header, so
| SPAs work without manual token handling. Set to false to
| disable the cookie.
|
*/
'cookie' => true,
/*
|----------------------------------------------------------------
| Configure missing token message
|----------------------------------------------------------------
|
| This is the message that will be returned when the CSRF token
| is not found in the request.
|
*/
'messages.tokenNotFound' => 'Token not found.',
/*
|----------------------------------------------------------------
| Configure invalid token message
|----------------------------------------------------------------
|
| This is the message that will be returned when the CSRF token
| is invalid.
|
*/
'messages.tokenInvalid' => 'Invalid token.',
/*
|----------------------------------------------------------------
| Configure error handler
|----------------------------------------------------------------
|
| By default, the CSRF module will return a built-in error page,
| however, you can configure a custom error handler to handle
| your own error pages.
|
| onError: function() {
| // Your custom error handler
| }
|
*/
'onError' => null,
];In this file, you can enable or disable CSRF protection, change the secret key, exclude routes from CSRF protection, turn on token rotation, disable the SPA cookie, and configure the allowed HTTP methods. You can also customize the messages shown when the CSRF token is not found or invalid. Route exceptions can include parameters, so /webhooks/{service} will match /webhooks/stripe and any other path in that shape.
Handling Failed CSRF Verification
By default, Leaf will output a built-in error page when a CSRF token is invalid. You can customize the messages shown on this page by updating your config.
'messages.tokenNotFound' => 'Token not found',
'messages.tokenInvalid' => 'Invalid token.',This will still display the default error page, just with your custom messages. If you want to handle the error yourself, you can use the onError key to pass an error handler to the CSRF module.
'onError' => function($error) {
if ($error === "tokenNotFound") {
// handle token not found error
} else {
// handle invalid token error
}
}What to read next
CSRF protection is just one of the many security features Leaf provides. Check out some of the other features of Leaf & Leaf MVC:
Authentication
Learn more about routing in Leaf MVC, dynamic routes, middleware and more.
Roles & Permissions
Learn how to process incoming requests, handle form submissions, and more.
Session Data
Save user data, flash messages, and more using Leaf's session module.
Frontend
Learn about SSR, SPA, and how to use Leaf with your favorite frontend framework.
