NAME
Catalyst::Plugin::CSRFToken - Robust CSRF protection plugin for Catalyst
SYNOPSIS
package MyApp;
use Catalyst;
# Enable CSRF protection; requires Session plugin
__PACKAGE__->setup(qw/
Session
Session::Store::... # your choice
Session::State::Cookie # Only sane state option
CSRFToken # Add this line
/);
# Configuration
__PACKAGE__->config(
'Plugin::CSRFToken' => {
'max_age' => 3600, # Token lifespan in seconds
'default_secret' => '...', # Optional, your default secret for HMAC signing
'param_key' => '...', # Optional, default is 'csrf_token'
'single_use_csrf_token' => ..., # Optional, default is 0
'auto_check' => ..., # Optional, default is 0
},
);
If not using 'auto_check' you can enable CSRF checks on a per-action basis:
sub some_action :Local EnableCSRF {
my ($self, $c) = @_;
# CSRF check is automatically performed
}
Or manually check the token:
if($c->req->method eq 'POST') {
Catalyst::Exception->throw(message => 'csrf_token failed validation')
unless $c->check_csrf_token;
}
In your templates, specify form IDs for multiple forms:
<form id="edit_profile" method="POST">
<input type="hidden" name="csrf_token" value="[% c.csrf_token(form_id=>'edit_profile') %]">
<!-- form fields here -->
</form>
Tokens can also be provided via the 'X-CSRF-Token' HTTP request header (useful for AJAX requests):
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha384-..."
crossorigin="anonymous"
></script>
<script>
$.ajax({
url: '/some/endpoint',
type: 'POST',
headers: {
'X-CSRF-Token': '[% c.csrf_token(form_id=>"your_form_id") %]'
},
data: {
// form data here
},
success: function(response) {
// handle response
}
});
</script>
DESCRIPTION
This creates a cryptographical token tied to a given web session used for CSRF protection. You can generate a token and pass it to your view layer where it should be added to the form you are trying to process, typically as a hidden field called 'csrf_token' (although you can change that in configuration if needed).
All POST, PUT, and PATCH requests are automatically checked for a valid CSRF token when 'auto_check_csrf_token' is enabled. If the check fails, a 403 Forbidden response is returned. The response can be customized by overriding the 'csrf_failure_response' method or as otherwise documented below.
If you leave this disabled, you will need to manually check the token using the 'check_csrf_token' method. Example:
if($c->req->method eq 'POST') {
Catalyst::Exception->throw(message => 'csrf_token failed validation')
unless $c->check_csrf_token;
}
Or you can enable CSRF checks on a per-action basis by adding the 'EnableCSRF' attribute to the action. Example:
sub some_action :Local EnableCSRF {
my ($self, $c) = @_;
# CSRF check is automatically performed
}
Version 1.001 Notes
Older versions of this plugin contained security and related bugs stemming from a mistake I made in the first release. Over the years I've tried to tweak it to make it more secure and robust. However, I've come to the conclusion that the best way to fix the issue required me to substantially rewrite the guts. I did my best to maintain the public API and as much of the private API as I could, but its possible this version break compatibility with older versions. Usually I try to avoid this, but in this case I felt it was necessary because I think the old versions are insecure and you should not use them in any case. Hit me with a bug report if you find something that doesn't work as expected and I will try to fix it, if I can without reintroducing the security issues.
This version also adds more debugging log output when Catalyst is run in debug mode. This should help you understand what is going on with the CSRF token generation and validation. But the log is more noisy.
CONFIGURATION
param_key =head2 token_param_key
Name of the request parameter used to carry the CSRF token. Defaults to 'csrf_token'.
max_age
Lifespan of a CSRF token in seconds. Defaults to 3600 (1 hour). After this time the token will be considered expired and a new one will be generated if requested, or will result in a 403 Forbidden response if the token is used in a request for validation.
auto_check_csrf_token
Boolean attribute controlling whether automatic CSRF checks on incoming requests are enabled. Defaults to 0 (disabled). Highly recommended to enable this feature. If you leave it off you will need to manually check the token using the 'check_csrf_token' method or you can enable on a per action basis by adding the 'EnableCSRF' attribute to the action. Examples:
sub some_action :Local EnableCSRF {
my ($self, $c) = @_;
# CSRF check is automatically performed
}
sub some_other_action :Local {
my ($self, $c) = @_;
Catalyst::Exception->throw(message => 'csrf_token failed validation')
unless $c->check_csrf_token;
}
default_secret
Optional secret key to enhance security by hashing tokens with HMAC. You can use this for example to force invalidation when restarting the service or just to add an extra layer of security. If you don't provide a secret, the token is stored in the session as is. If you provide a secret, the token is hashed with the secret before being stored in the session.
Using a secret can improve the security of your tokens and reduce the risk of playback attacks. But you should have a key rotation policy for this to be effective. If you don't provide a
session_key
Name of the session key used to store CSRF tokens. Defaults to '_csrf_token'. You can change this if it conflicts with another session key you are using.
single_use_csrf_token
Boolean attribute controlling whether CSRF tokens are single-use. Defaults to 0 (disabled). If enabled, the token is deleted from the session after the first validation. If disabled, the token can be used multiple times until it expires.
This is disabled by default because enabling it can lead to some tricky UI experiences, like if the user clicks the back button and resubmits the form, which then generated a CSRF token validation error. You can mitigate this issue and similar ones by setting you HTML form to not cache, or by using JavaScript to prevent the user from resubmitting, Example:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
But this can be browser dependent and not always work as expected. If you don't need the highest level of CSRF protection you can leave the default as zero, which will allow the token to be used multiple times until it expires. That way you don't break the 'back' button and similar actions that might cause a token to be reused. But this is less secure.
Even if youi leave this as zero, you can selectivly enable single use tokens by setting the ':SingleUseCSRF' attribute on the action. Example:
sub some_action :Local SingleUseCSRF {
my ($self, $c) = @_;
# CSRF check is automatically performed
}
You may wish to do this for particularly sensitive actions, like changing a password or making a payment or logging in.
METHODS
csrf_token(form_id=>$form_id)
Generates and returns a CSRF token for the given form ID. Defaults to 'default' if not provided. Calling this method will return a token you can embed in your form as a hidden field. If your webpage has multiple forms, you can generate a token for each form by providing a unique form ID.
Token is stored in the session and used to perform CSRF checks on form submissions. Please keep in mind that this session key is deleted once the token is used, so you can't use the same token twice. You should also deal with 'back' buttons and similar actions that might cause a token to be reused and return an error.
check_csrf_token
Checks the CSRF token in the request. If the token is missing, invalid, or expired, a 403 Forbidden response is returned. Returns 1 if the token is valid.
random_token($length)
Generates and returns a secure random token encoded in base64 format. Default length is 48 bytes. Useful when you just need a disposable token that is cryptographically secure.
csrf_failure_response
This is the method that is called when a CSRF token check fails. It first checks if the controller has a 'handle_failed_csrf_token_check' method and calls that if it does. If not it calls the 'handle_failed_csrf_token_check' method on the context object if that exists. If neither of those methods exist it creates a default response and finalizes it.
Override this method if you want to provide a custom response when a CSRF token check fails or implement one of the other two methods mentioned above.
SKIPPING AUTOMATIC CSRF CHECKS
You can skip automatic CSRF checks (when using the 'auto_check' configuration option) for specific actions by adding the 'NoCSRF' attribute to the action:
sub skip :Path(skip) DisableCSRF Args(0) {
my ($self, $c) = @_;
$c->res->body('ok');
}
CHAINING AND ACTION ATTRIBUTES
If using chained actions in your Catalyst application, you can apply the 'EnableCSRF', 'DisableCSRF', and 'SingleUseCSRF' attributes to alter how the CSRF token is checked. However you MUST apply the attribte to the final action in the chain for this to work. Example:
sub base :Chained('/') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_;
}
sub some_action :Chained('base') PathPart('some_action') Args(0) {
my ($self, $c) = @_;
}
sub final_action :Chained('base') PathPart('final_action') Args(0) EnableCSRF SingleUseCSRF {
my ($self, $c) = @_;
# CSRF check is automatically performed and token is deleted after use
}
If you don't place the attribute on the final action the plugin will not see it.
AUTHOR
John Napiorkowski <jnapiork@cpan.org>
COPYRIGHT
Copyright (c) 2025 the above named AUTHOR
LICENSE
You may distribute this code under the same terms as Perl itself.