No matter how small your web app may be, security is essential! In this tutorial, you’ll learn how to add a CSRF token in forms used in Mezzio-based applications, to prevent attackers from being able to force your users to execute malicious actions.
:idseparator: -
:idprefix:
:experimental:
:source-highlighter: rouge
:rouge-style: pastie
:imagesdir: /images
Before we dive on in if you’re not sure with what CSRF (Cross-Site Request Forgery) is, here’s how https://owasp.org/www-community/attacks/csrf[OWASP (The Open Web Application Security Project) defines it]:
[quote,https://owasp.org/www-community/attacks/csrf]
CSRF is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated.
With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing.
If the victim is a normal user, a successful CSRF attack can force the user to perform state-changing requests like transferring funds, changing their email address, and so forth.
If the victim is an administrative account, CSRF can compromise the entire web application.
To defend against this kind of attack, a CSRF token (also known as an Anti-CSRF token) is included in all legitimate form submissions sent to a server.
After a form submission is received by the server, as part of validating the submission, the server checks the value of the CSRF token against the string that was generated when the form was first created.
If the two values are the same, you can be sure that the request is legitimate.
If they’re different, or if the token is missing, the request is rejected because it’s likely that some malicious actor has either intercepted the request and stripped the token, or has tricked a user into making a spoofed request.
Every time a form is created by the server, the CSRF token is created anew.
Secondly, often, though not always, whenever a CSRF token is validated, a new one is created.
Let’s assume that you have an existing form on your site, perhaps like this one on the landing page that I created for https://mezzioessentials.com[the Mezzio Essentials book].
The user sees a small, perfectly normal-looking form, with only the fields that they’re required to fill in.
However, if you look at the form’s source, then you’ll see that it contains a hidden field named __csrf
.
This field contains the CSRF token.
I’ve stripped out any unnecessary form content so that just the essential details are displayed.
[source,html,linenums]
----
== So, How Do You Use CSRF Tokens in Mezzio?
Conceptually, there isn’t that much to do.
Let’s step through the process of implementing it in Mezzio so that you learn how to do it.
The first thing that you need to do is to ensure that your configuration of PHP has the https://www.php.net/manual/en/intro.session.php[Sessions extension] installed and enabled.
By default it is, but depending on your PHP installation, cloud, or hosting provider, it might not be.
Always helps to be sure.
To check, run the following command.
If you see “session” output to the console, then it’s installed and enabled.
[source,bash]
grep session <(php -m)
Note: There are other ways to persist the CSRF token, but I’ll cover those in future posts.
=== Install the Required Packages
The next thing to do is to add the 3 required packages:
mezzio/mezzio-csrf
mezzio/mezzio-session
mezzio/mezzio-session-ext
To do that, run the following command in the terminal in the root directory of your project:
[source,bash,linenums]
composer require
mezzio/mezzio-csrf
mezzio/mezzio-session
mezzio/mezzio-session-ext
During the installation, you’ll see the following question:
[source,bash,linenums]
Please select which config file you wish to inject ‘Mezzio\Session\ConfigProvider’ into:
[0] Do not inject
[1] config/config.php
Make your selection (default is 1):
Accept the default value and continue with the package installation.
Then, when it asks the following question, accept the default of Y
as well.
[source,bash,linenums]
Remember this option for other packages of the same type? (Y/n)
=== Enable the Required Middleware
With those packages installed, we need to make use two pieces of middleware from them: SessionMiddleware
from mezzio-session, and CsrfMiddleware
from mezzio-csrf.
[quote, David McKay (@rawkode)]
when you generated a token server-side and pass it down to the page so that client-side requests submitting forms can pass it back to verify the request is legit.
We need to load the two Middleware classes for the form’s route — in the correct order.
SessionMiddleware
has to be loaded first, so that requests have a persistence layer to store generated CSRF tokens.
CsrfMiddleware
has to be loaded second, and provides the functionality for generating and validating CSRF tokens.
To do that, assuming that you have defined your routes in config/routes.php
, update the route’s handler to be an array, as in the highlighted example below.
.config/routes.php
[source,php,linenums,highlight=3..7]
$app->route(
‘/’,
[
\Mezzio\Session\SessionMiddleware::class,
\Mezzio\Csrf\CsrfMiddleware::class,
\App\Handler\HomePageHandler::class
],
[‘post’, ‘get’],
‘home’
);
The example above assumes that you only need them for one route.
If you need to have them available for every route in your application, add the following code to config/pipeline.php
:
.config/pipeline.php
[source,php,linenums]
$app->pipe(\Mezzio\Session\SessionMiddleware::class);
$app->pipe(\Mezzio\Router\Middleware\RouteMiddleware::class);
TIP: Register them before RouteMiddleware
is registered.
== Refactor the Route’s Handler Code to be CSRF-Aware
With the Middleware correctly loaded for the route, it’s time to add the required code in the route’s handler.
The handler that you put it in is up to your discretion.
For this article, let’s assume that it is in the default handler that comes with applications generated with https://github.com/mezzio/mezzio-skeleton[the Mezzio Skeleton], src/App/Handler/HomePageHandler.php
.
[source,php,linenums]
public function handle(ServerRequestInterface $request)
: ResponseInterface
{
$data = [];
$form = (new \Laminas\Form\Annotation\AnnotationBuilder())
->createForm(\App\Entity\PreviewBookDownload::class);
/** @var \Mezzio\Csrf\CsrfGuardInterface $guard */
$guard = $request->getAttribute(
\Mezzio\Csrf\CsrfMiddleware::GUARD_ATTRIBUTE
);
if ($request->getMethod() === 'GET') {
$data['__csrf'] = $guard->generateToken();
}
if ($request->getMethod() === 'POST') {
$data = $request->getParsedBody();
$form->setData($data);
$token = $data['__csrf'] ?? '';
if ($guard->validateToken($token) && $form->isValid()) {
// Handle a positive form submission
// ...
}
}
return new HtmlResponse(
$this->template->render(
'app::home-page', $data
)
);
}
The method first instantiates a https://docs.laminas.dev/laminas-form/quick-start/[Laminas Form] object using https://docs.laminas.dev/laminas-form/quick-start/#using-annotations[Laminas Form’s AnnotationBuilder
].
That it instantiates the form object in this way isn’t important.
It’s just the approach that I took in this scenario, as I like the functionality which Laminas Form provides for handling request filtering and validation.
[TIP]
If you want to use this approach, you’ll need to run the following command:
[source,bash,linenums]
composer require
laminas/laminas-form
doctrine/common
====
It then retrieves the CSRF guard, by retrieving the CsrfMiddleware::GUARD_ATTRIBUTE
attribute from the request, put there by CsrfMiddleware
, and uses it to initialise a new variable, aptly named $guard
.
It then checks if the HTTP request method was GET
.
If so, it calls $guard
’s generateToken
method to generate a new CSRF token and initialises the __csrf
element of the $data
array with it.
If the request method was POST
, it retrieves the body of the request, containing the form data, by calling $request
’s getParsedBody()
, and stores the retrieved array in $data
.
After doing that, it then passes $data
to a call to $form
’s setData
method, setting the value of the form’s fields with the values supplied in the request body.
After that, it attempts to retrieve the token from $data
’s __csrf
element, and initialise a new variable, named $token
with it.
If the element is not set or is empty, then $token
is set to an empty string.
Following that, it attempts to validate the token, by calling $guard
’s validateToken
method.
It also runs the form’s validation rules on the remainder of the form’s data.
If both method calls succeed, then the remaining code is executed.
If either fails, then the form is displayed again along with an appropriate error message.
I’ve not displayed this specific code, as it’s not essential to the understanding of integrating CSRF tokens into a Mezzio application.
After that, there’s one final aspect of code to cover, which is the returned https://docs.laminas.dev/laminas-diactoros/v2/custom-responses/#html-responses[HtmlResponse] object.
This renders the page’s template, after rendering the contents of $data
into it.
If it’s a GET request, then an empty form, containing the CSRF token will be displayed on the page.
If it’s a POST request, then the form will be rendered, with the CSRF token and any applicable error messages.
== Update The Form’s View Template
I’m not going to cover the handler’s view template in great depth, as there isn’t much to cover.
It’s a standard https://docs.laminas.dev/laminas-view/view-scripts/[laminas-view view script].
If you’re not familiar with laminas-view, I strongly encourage you to read https://docs.laminas.dev/laminas-view/quick-start/[the laminas-view Quick Start], which thoroughly covers them.
Here is the key line from the template file:
[source,php+html,linenums,,highlight=22..25]
----
When the form is rendered, because of the PHP code in the value element, the form’s value will be set to the CSRF token.
== In Conclusion
And that’s the essentials of integrating CSRF tokens into a Mezzio-based application.
We installed the required packages and saw how to enable them for a specific route, as well as globally for all routes.
We refactored a handler’s handle method to generate and validate a token based on whether the request method was GET or POST.
Finally, we saw how to update the view’s template to render the CSRF token in the __csrf
field.
Sure, there are many aspects of functionality that contribute to making it work.
However, I hope that the article’s identified what they are and shown you how they fit together.
That said, while using a CSRF token is a solid additional measure of security, more steps can and should be taken to fully secure your forms.
Check out https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html[OWASP's Cross-Site Request Forgery Prevention Cheat Sheet] if you’d like to learn more.
Finally, security is a fun subject and something that we all need to do to ensure that malicious actors don’t take advantage of our users.
Sure, it’s more work for us, but it’s a fantastic opportunity to learn, to grow our skills and help our users have a better experience with the apps that we create.
Join the discussion
comments powered by Disqus