Mojolicious-Plugin-CSRFDefender

 view release on metacpan or  search on metacpan

lib/Mojolicious/Plugin/CSRFDefender.pm  view on Meta::CPAN

package Mojolicious::Plugin::CSRFDefender;

use strict;
use warnings;
use Carp;

our $VERSION = '0.0.8';

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
));

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);
    }

    # 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 $body = $c->res->body;
        $body =~ s{(<form\s*[^>]*method=["']POST["'][^>]*>)}{$1\n<input type="hidden" name="$p_name" value="$token" />}isg;
        $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);

    if ($c->req->method eq 'POST') {
        return 0 unless $request_token;
        return 0 unless $session_token;
        return 0 unless $request_token eq $session_token;
    }

    # onetime
    if ($c->req->method eq 'POST' && $self->onetime) {
        $c->session($self->{session_key} => '');
    }

    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.



( run in 1.330 second using v1.01-cache-2.11-cpan-39bf76dae61 )