package SrvMngr::Plugin::CSRFDefender; use strict; use warnings; use Carp; our $VERSION = '0.0.8-1'; use base qw(Mojolicious::Plugin Class::Accessor::Fast); __PACKAGE__->mk_accessors(qw( parameter_name session_key token_length error_status error_content error_template onetime get_token_param )); use String::Random; use Path::Class; sub register { my ($self, $app, $conf) = @_; # Plugin config $conf ||= {}; # setting $self->parameter_name($conf->{parameter_name} || 'csrftoken'); $self->session_key($conf->{session_key} || 'csrftoken'); $self->token_length($conf->{token_length} || 32); $self->error_status($conf->{error_status} || 403); $self->error_content($conf->{error_content} || 'Forbidden'); $self->onetime($conf->{onetime} || 0); if ($conf->{error_template}) { my $file = $app->home->rel_file($conf->{error_template}); $self->error_template($file); } $self->get_token_param($conf->{get_token_param} || 'CsrfDef=TOKEN'); # added for GET method # input check $app->hook(before_dispatch => sub { my ($c) = @_; unless ($self->_validate_csrf($c)) { my $content; if ($self->error_template) { my $file = file($self->error_template); $content = $file->slurp; } else { $content = $self->{error_content}, } $c->render( status => $self->{error_status}, text => $content, ); }; }); # output filter $app->hook(after_dispatch => sub { my ($c) = @_; my $token = $self->_get_csrf_token($c); my $p_name = $self->parameter_name; my $g_token = $self->get_token_param; my $body = $c->res->body; $body =~ s{(]*method=["']POST["'][^>]*>)}{$1\n}isg; $body =~ s{(\?$g_token)}{\?$p_name=$token}isg; # added for GET method $c->res->body($body); }); return $self; } sub _validate_csrf { my ($self, $c) = @_; my $p_name = $self->parameter_name; my $s_name = $self->session_key; my $request_token = $c->req->param($p_name); my $session_token = $c->session($s_name); # POST method or local GET with params. # if ( $c->req->method eq 'POST' or ( $c->req->method eq 'GET' && %{$c->req->params->to_hash} ) ) { # POST method or local GET with param csrftoken present if ( $c->req->method eq 'POST' or ( $c->req->method eq 'GET' && $request_token ) ) { return 0 unless $request_token; return 0 unless $session_token; return 0 unless $request_token eq $session_token; # onetime $c->session($self->{session_key} => '') if $self->onetime; } return 1; } sub _get_csrf_token { my ($self, $c) = @_; my $key = $self->session_key; my $token = $c->session($key); my $length = $self->token_length; return $token if $token; $token = String::Random::random_regex("[a-zA-Z0-9_]{$length}"); $c->session($key => $token); return $token; } 1; __END__ =head1 NAME Mojolicious::Plugin::CSRFDefender - Defend CSRF automatically in Mojolicious Application =head1 VERSION This document describes Mojolicious::Plugin::CSRFDefender. =head1 SYNOPSIS # Mojolicious $self->plugin('Mojolicious::Plugin::CSRFDefender'); # Mojolicious::Lite plugin 'Mojolicious::Plugin::CSRFDefender'; =head1 DESCRIPTION This plugin defends CSRF automatically in Mojolicious Application. Following is the strategy. =head2 output filter When the application response body contains form tags with method="post", this inserts hidden input tag that contains token string into forms in the response body. For example, the application response body is
this becomes
=head2 input check For every POST requests, this module checks input parameters contain the collect token parameter. If not found, throws 403 Forbidden. =head1 OPTIONS plugin 'Mojolicious::Plugin::CSRFDefender' => { parameter_name => 'param-csrftoken', session_key => 'session-csrftoken', token_length => 40, error_status => 400, error_template => 'public/400.html', }; =over 4 =item parameter_name(default:"csrftoken") Name of the input tag for the token. =item session_key(default:"csrftoken") Name of the session key for the token. =item token_length(default:32) Length of the token string. =item error_status(default:403) Status code when CSRF is detected. =item error_content(default:"Forbidden") Content body when CSRF is detected. =item error_template Return content of the specified file as content body when CSRF is detected. Specify the file path from the application home directory. =item onetime(default:0) If specified with 1, this plugin uses onetime token, that is, whenever client sent collect token and this middleware detect that, token string is regenerated. =back =head1 METHODS L inherits all methods from L and implements the following new ones. =head2 C $plugin->register; Register plugin in L application. =head1 SEE ALSO =over 4 =item * L =back =head1 REPOSITORY https://github.com/shibayu36/p5-Mojolicious-Plugin-CSRFDefender =head1 AUTHOR C<< >> =head1 LICENCE AND COPYRIGHT Copyright (c) 2011, Yuki Shibazaki C<< >>. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L.