View | Details | Raw Unified | Return to bug 36598
Collapse All | Expand All

(-)a/Koha/App/Intranet.pm (+2 lines)
Lines 38-43 sub startup { Link Here
38
    # FIXME This generates routes like this: /api/api/v1/...
38
    # FIXME This generates routes like this: /api/api/v1/...
39
    $self->plugin('RESTV1');
39
    $self->plugin('RESTV1');
40
40
41
    $self->plugin('CSRF');
42
41
    $self->hook(before_dispatch => \&_before_dispatch);
43
    $self->hook(before_dispatch => \&_before_dispatch);
42
    $self->hook(around_action => \&_around_action);
44
    $self->hook(around_action => \&_around_action);
43
45
(-)a/Koha/App/Opac.pm (+2 lines)
Lines 38-43 sub startup { Link Here
38
    # FIXME This generates routes like this: /api/api/v1/...
38
    # FIXME This generates routes like this: /api/api/v1/...
39
    $self->plugin('RESTV1');
39
    $self->plugin('RESTV1');
40
40
41
    $self->plugin('CSRF');
42
41
    $self->hook(before_dispatch => \&_before_dispatch);
43
    $self->hook(before_dispatch => \&_before_dispatch);
42
    $self->hook(around_action => \&_around_action);
44
    $self->hook(around_action => \&_around_action);
43
45
(-)a/Koha/App/Plugin/CSRF.pm (+100 lines)
Line 0 Link Here
1
package Koha::App::Plugin::CSRF;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
=head1 NAME
19
20
Koha::App::Plugin::CSRF
21
22
=head1 SYNOPSIS
23
24
    $app->plugin('CSRF');
25
26
=head1 DESCRIPTION
27
28
Enables CSRF protection in a Mojolicious app
29
30
=cut
31
32
use Modern::Perl;
33
34
use Mojo::Base 'Mojolicious::Plugin';
35
36
use Mojo::Message::Response;
37
38
use Koha::Token;
39
40
=head1 METHODS
41
42
=head2 register
43
44
Called by Mojolicious when the plugin is loaded.
45
46
Defines an `around_action` hook that will return a 403 response if CSRF token
47
is missing or invalid.
48
49
This verification occurs only for HTTP methods POST, PUT, DELETE and PATCH.
50
51
If CGISESSID cookie is missing, it means that we are not authenticated or we
52
are authenticated to the API by another method (HTTP basic or OAuth2). In this
53
case, no verification is done.
54
55
=cut
56
57
sub register {
58
    my ( $self, $app, $conf ) = @_;
59
60
    $app->hook(
61
        around_action => sub {
62
            my ( $next, $c, $action, $last ) = @_;
63
64
            my $method = $c->req->method;
65
            if ( $method eq 'POST' || $method eq 'PUT' || $method eq 'DELETE' || $method eq 'PATCH' ) {
66
                if ( $c->cookie('CGISESSID') && !$self->is_csrf_valid( $c->req ) ) {
67
                    return $c->reply->exception('Wrong CSRF token')->rendered(403);
68
                }
69
            }
70
71
            return $next->();
72
        }
73
    );
74
}
75
76
=head2 is_csrf_valid
77
78
Checks if a CSRF token exists and is valid
79
80
    $is_valid = $plugin->is_csrf_valid($req)
81
82
C<$req> must be a Mojo::Message::Request object
83
84
=cut
85
86
sub is_csrf_valid {
87
    my ( $self, $req ) = @_;
88
89
    my $csrf_token = $req->param('csrf_token') || $req->headers->header('CSRF_TOKEN');
90
    my $cookie     = $req->cookie('CGISESSID');
91
    if ( $csrf_token && $cookie ) {
92
        my $session_id = $cookie->value;
93
94
        return Koha::Token->new->check_csrf( { session_id => $session_id, token => $csrf_token } );
95
    }
96
97
    return 0;
98
}
99
100
1;
(-)a/t/db_dependent/mojo/csrf.t (-1 / +76 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use Test::More tests => 2;
6
use Test::Mojo;
7
8
use Koha::Database;
9
10
subtest 'CSRF - Intranet' => sub {
11
    plan tests => 4;
12
13
    my $t = Test::Mojo->new('Koha::App::Intranet');
14
15
    # Make sure we have a CGISESSID cookie
16
    $t->get_ok('/cgi-bin/koha/mainpage.pl');
17
18
    subtest 'Without a CSRF token' => sub {
19
        plan tests => 3;
20
21
        $t->post_ok('/cgi-bin/koha/mainpage.pl')->status_is(403)
22
            ->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
23
    };
24
25
    subtest 'With a wrong CSRF token' => sub {
26
        plan tests => 3;
27
28
        $t->post_ok( '/cgi-bin/koha/mainpage.pl', form => { csrf_token => 'BAD', op => 'cud-login' } )->status_is(403)
29
            ->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
30
    };
31
32
    subtest 'With a good CSRF token' => sub {
33
        plan tests => 4;
34
35
        $t->get_ok('/cgi-bin/koha/mainpage.pl');
36
37
        my $csrf_token = $t->tx->res->dom('input[name="csrf_token"]')->map( attr => 'value' )->first;
38
39
        $t->post_ok( '/cgi-bin/koha/mainpage.pl', form => { csrf_token => $csrf_token, op => 'cud-login' } )
40
            ->status_is(200)->content_like( qr/Please log in again/, 'Login failed but CSRF test passed' );
41
    };
42
};
43
44
subtest 'CSRF - OPAC' => sub {
45
    plan tests => 4;
46
47
    my $t = Test::Mojo->new('Koha::App::Opac');
48
49
    # Make sure we have a CGISESSID cookie
50
    $t->get_ok('/cgi-bin/koha/opac-user.pl');
51
52
    subtest 'Without a CSRF token' => sub {
53
        plan tests => 3;
54
55
        $t->post_ok('/cgi-bin/koha/opac-user.pl')->status_is(403)
56
            ->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
57
    };
58
59
    subtest 'With a wrong CSRF token' => sub {
60
        plan tests => 3;
61
62
        $t->post_ok( '/cgi-bin/koha/opac-user.pl', form => { csrf_token => 'BAD', op => 'cud-login' } )->status_is(403)
63
            ->content_like( qr/Wrong CSRF token/, 'Body contains "Wrong CSRF token"' );
64
    };
65
66
    subtest 'With a good CSRF token' => sub {
67
        plan tests => 4;
68
69
        $t->get_ok('/cgi-bin/koha/opac-user.pl');
70
71
        my $csrf_token = $t->tx->res->dom('input[name="csrf_token"]')->map( attr => 'value' )->first;
72
73
        $t->post_ok( '/cgi-bin/koha/opac-user.pl', form => { csrf_token => $csrf_token, op => 'cud-login' } )
74
            ->status_is(200)->content_like( qr/Log in to your account/, 'Login failed but CSRF test passed' );
75
    };
76
};

Return to bug 36598