CSRF protection in forms

Summary

This page adds CSRF protection to a form in three steps: a Csrf form element, a CSRF validator in the input filter, and the rendered hidden field in the template. It also shows how to test the protection and how the token timeout works.

Details

A Cross-Site Request Forgery (CSRF) attack is a type of security vulnerability that tricks a user into performing actions on a web application in which they are authenticated, without their knowledge or consent.

Web applications can protect users against these types of attacks by implementing CSRF tokens in their forms which are known only to the application that generated them and must be included when submitting forms. With each visit, a new CSRF token is added to the form so tokens are not reusable between forms. Failing to provide a valid CSRF token will result in a form validation error.

Implement CSRF protection

Implementing CSRF protection requires three steps:

Create field

Open the form's PHP class and append the following code to the method that initializes the fields (usually init):

$this->add(new \Laminas\Form\Element\Csrf('exampleCsrf', [
    'csrf_options' => [
        'timeout' => 3600,
        'session' => new \Laminas\Session\Container(),
    ],
]));

You should use a relevant name instead of exampleCsrf that best describes the purpose of the field (example: forgotPasswordCsrf).

Validate field

Open the InputFilter that validates the form fields and append the following code to the method that initializes the fields (usually init). If the form builds its input filter itself (for example ProfileDetailsForm), add it there instead:

$csrf = new \Laminas\InputFilter\Input('exampleCsrf');
$csrf->setRequired(true);
$csrf->getFilterChain()
    ->attachByName(\Laminas\Filter\StringTrim::class)
    ->attachByName(\Laminas\Filter\StripTags::class);
$csrf->getValidatorChain()
    ->attachByName(\Laminas\Validator\NotEmpty::class, [
        'message' => '<b>CSRF</b> is required and cannot be empty',
    ], true)
    ->attachByName(\Laminas\Session\Validator\Csrf::class, [
        'name'    => 'exampleCsrf',
        'message' => '<b>CSRF</b> is invalid',
        'session' => new \Laminas\Session\Container(),
    ], true);
$this->add($csrf);

Replace exampleCsrf with the CSRF field's name in the form.

Don't forget to modify both occurrences in this file.

Make sure that you validate the form using its isValid method in the handler/controller where it is submitted.

Render field

Open the template that renders your form and add the following code somewhere between the form's opening and closing tags:

{{ formElement(form.get('exampleCsrf')) }}

Test the implementation

Access your form from the browser and view its source. You should see a new hidden field, called exampleCsrf (or whatever name you used). After filling out the form, submitting it should work as before.

In order to make sure that the new CSRF field works as expected, you can inspect the form using your browser's Developer tools and modify its value. Submitting a filled out form should result in a validation error:

CSRF is invalid

Clearing the value instead results in:

CSRF is required and cannot be empty

Timeout

Note the timeout option in your PHP form's exampleCsrf field, with its default value set to 3600. This represents the value in seconds for how long the token is valid. Submitting a form that has been rendered for longer than this value will result in a validation error:

CSRF is invalid

You can modify the value of timeout in each form, but the default value should work in most cases.

FAQ

Q: Which forms are already protected?

A: Every shipped form: login, register, request password reset, reset password, profile details, avatar upload, change password, delete account and contact. Their fields are named after the form, for example userLoginCsrf and contactCsrf.

Q: How long is a token valid?

A: 3600 seconds (one hour), set by the timeout option of each form's Csrf element.

Q: Can two forms use the same CSRF field name?

A: Give each form its own name. Each name keeps its own token in the session, so forms that share a name also share, and overwrite, the same token.

Q: Which error appears when the token is wrong?

A: A missing token gives "CSRF is required and cannot be empty"; a modified or expired token gives "CSRF is invalid".