diff --git a/Koha/REST/Plugin/CSRF.pm b/Koha/REST/Plugin/CSRF.pm deleted file mode 100644 index 3803a4c3ec..0000000000 --- a/Koha/REST/Plugin/CSRF.pm +++ /dev/null @@ -1,77 +0,0 @@ -package Koha::REST::Plugin::CSRF; - -# This file is part of Koha. -# -# Koha is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# Koha is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Koha; if not, see . - -use Modern::Perl; - -use Mojo::Base 'Mojolicious::Plugin'; - -use Koha::Token; - -=head1 NAME - -Koha::REST::Plugin::CSRF - CSRF protection for cookie-authenticated REST API requests - -=head1 DESCRIPTION - -This plugin enforces CSRF token validation for state-changing requests -(POST, PUT, DELETE, PATCH) when the request is authenticated via a session -cookie (CGISESSID). Requests using OAuth2 or HTTP Basic Auth are not affected. - -=head1 API - -=head2 Mojolicious::Plugin methods - -=head3 register - -=cut - -sub register { - my ( $self, $app, $conf ) = @_; - - my %safe_methods = map { $_ => 1 } qw(GET HEAD OPTIONS); - - $app->hook( - before_dispatch => sub { - my ($c) = @_; - - # Only check state-changing methods - return if $safe_methods{ uc( $c->req->method ) }; - - # Only enforce for cookie-authenticated requests - my $session_cookie = $c->req->cookie('CGISESSID'); - return unless $session_cookie; - - my $session_id = $session_cookie->value; - return unless $session_id; - - # Validate CSRF token from header - my $csrf_token = $c->req->headers->header('CSRF-TOKEN'); - - unless ( $csrf_token - && Koha::Token->new->check_csrf( { session_id => $session_id, token => $csrf_token } ) ) - { - $c->render( - status => 403, - json => { error => 'Wrong CSRF token' }, - ); - return; - } - } - ); -} - -1; diff --git a/Koha/REST/V1.pm b/Koha/REST/V1.pm index cae1ca2eb8..b1137e84b4 100644 --- a/Koha/REST/V1.pm +++ b/Koha/REST/V1.pm @@ -219,7 +219,6 @@ sub startup { }; $self->plugin('Koha::App::Plugin::Language'); - $self->plugin('Koha::REST::Plugin::CSRF'); $self->plugin('Koha::REST::Plugin::Pagination'); $self->plugin('Koha::REST::Plugin::Query'); $self->plugin('Koha::REST::Plugin::Objects'); diff --git a/t/db_dependent/Koha/REST/Plugin/CSRF.t b/t/db_dependent/Koha/REST/Plugin/CSRF.t deleted file mode 100755 index 84db011f5c..0000000000 --- a/t/db_dependent/Koha/REST/Plugin/CSRF.t +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/perl - -# This file is part of Koha. -# -# Koha is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# Koha is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Koha; if not, see . - -use Modern::Perl; - -use Mojolicious::Lite; - -use Koha::Database; -use Koha::Session; -use Koha::Token; - -use t::lib::Mocks; - -app->log->level('error'); - -plugin 'Koha::REST::Plugin::CSRF'; - -# Dummy routes for testing -get '/test' => sub { - my $c = shift; - $c->render( json => { ok => 1 }, status => 200 ); -}; - -post '/test' => sub { - my $c = shift; - $c->render( json => { ok => 1 }, status => 200 ); -}; - -put '/test' => sub { - my $c = shift; - $c->render( json => { ok => 1 }, status => 200 ); -}; - -del '/test' => sub { - my $c = shift; - $c->render( json => { ok => 1 }, status => 200 ); -}; - -patch '/test' => sub { - my $c = shift; - $c->render( json => { ok => 1 }, status => 200 ); -}; - -use Test::NoWarnings; -use Test::More tests => 8; -use Test::Mojo; - -my $schema = Koha::Database->new->schema; -my $t = Test::Mojo->new; - -t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); - -subtest 'GET requests bypass CSRF check' => sub { - - plan tests => 2; - - $schema->storage->txn_begin; - - $t->get_ok('/test')->status_is(200); - - $schema->storage->txn_rollback; -}; - -subtest 'HEAD requests bypass CSRF check' => sub { - - plan tests => 1; - - $schema->storage->txn_begin; - - my $tx = $t->ua->build_tx( HEAD => '/test' ); - $t->request_ok($tx); - - $schema->storage->txn_rollback; -}; - -subtest 'POST without CGISESSID cookie bypasses CSRF check' => sub { - - plan tests => 2; - - $schema->storage->txn_begin; - - $t->post_ok('/test')->status_is(200); - - $schema->storage->txn_rollback; -}; - -subtest 'POST with CGISESSID but no CSRF-TOKEN returns 403' => sub { - - plan tests => 3; - - $schema->storage->txn_begin; - - my $tx = $t->ua->build_tx( POST => '/test' ); - $tx->req->cookies( { name => 'CGISESSID', value => 'a_session_id' } ); - $t->request_ok($tx)->status_is(403)->json_is( '/error', 'Wrong CSRF token' ); - - $schema->storage->txn_rollback; -}; - -subtest 'POST with CGISESSID and invalid CSRF-TOKEN returns 403' => sub { - - plan tests => 3; - - $schema->storage->txn_begin; - - my $tx = $t->ua->build_tx( POST => '/test' ); - $tx->req->cookies( { name => 'CGISESSID', value => 'a_session_id' } ); - $tx->req->headers->header( 'CSRF-TOKEN', 'bogus_token' ); - $t->request_ok($tx)->status_is(403)->json_is( '/error', 'Wrong CSRF token' ); - - $schema->storage->txn_rollback; -}; - -subtest 'POST with CGISESSID and valid CSRF-TOKEN succeeds' => sub { - - plan tests => 3; - - $schema->storage->txn_begin; - - my $session = Koha::Session->get_session( {} ); - my $session_id = $session->id; - my $csrf_token = Koha::Token->new->generate_csrf( { session_id => $session_id } ); - - my $tx = $t->ua->build_tx( POST => '/test' ); - $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); - $tx->req->headers->header( 'CSRF-TOKEN', $csrf_token ); - $t->request_ok($tx)->status_is(200)->json_is( '/ok', 1 ); - - $schema->storage->txn_rollback; -}; - -subtest 'PUT and DELETE with valid CSRF-TOKEN succeed' => sub { - - plan tests => 6; - - $schema->storage->txn_begin; - - my $session = Koha::Session->get_session( {} ); - my $session_id = $session->id; - my $csrf_token = Koha::Token->new->generate_csrf( { session_id => $session_id } ); - - my $tx = $t->ua->build_tx( PUT => '/test' ); - $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); - $tx->req->headers->header( 'CSRF-TOKEN', $csrf_token ); - $t->request_ok($tx)->status_is(200)->json_is( '/ok', 1 ); - - $tx = $t->ua->build_tx( DELETE => '/test' ); - $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); - $tx->req->headers->header( 'CSRF-TOKEN', $csrf_token ); - $t->request_ok($tx)->status_is(200)->json_is( '/ok', 1 ); - - $schema->storage->txn_rollback; -}; diff --git a/t/db_dependent/api/v1/csrf.t b/t/db_dependent/api/v1/csrf.t deleted file mode 100755 index 67aa22bc62..0000000000 --- a/t/db_dependent/api/v1/csrf.t +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env perl - -# This file is part of Koha. -# -# Koha is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# Koha is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Koha; if not, see . - -use Modern::Perl; - -use Test::NoWarnings; -use Test::More tests => 5; -use Test::Mojo; - -use Koha::Database; -use Koha::Session; -use Koha::Token; - -use t::lib::Mocks; - -my $schema = Koha::Database->new->schema; -my $t = Test::Mojo->new('Koha::REST::V1'); - -t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); - -subtest 'GET with CGISESSID but no CSRF token is allowed' => sub { - - plan tests => 2; - - $schema->storage->txn_begin; - - my $tx = $t->ua->build_tx( GET => '/api/v1/patrons' ); - $tx->req->cookies( { name => 'CGISESSID', value => 'fake_session' } ); - $t->request_ok($tx); - isnt( $t->tx->res->code, 403, 'GET not blocked by CSRF check' ); - - $schema->storage->txn_rollback; -}; - -subtest 'POST without CGISESSID is not blocked by CSRF' => sub { - - plan tests => 2; - - $schema->storage->txn_begin; - - my $tx = $t->ua->build_tx( POST => '/api/v1/patrons' ); - $tx->req->headers->content_type('application/json'); - $tx->req->body('{}'); - $t->request_ok($tx); - isnt( $t->tx->res->code, 403, 'POST without cookie not blocked by CSRF' ); - - $schema->storage->txn_rollback; -}; - -subtest 'POST with CGISESSID but missing/invalid CSRF token returns 403' => sub { - - plan tests => 6; - - $schema->storage->txn_begin; - - # No token - my $tx = $t->ua->build_tx( POST => '/api/v1/patrons' ); - $tx->req->cookies( { name => 'CGISESSID', value => 'some_session' } ); - $tx->req->headers->content_type('application/json'); - $tx->req->body('{}'); - $t->request_ok($tx)->status_is(403)->json_is( '/error', 'Wrong CSRF token' ); - - # Invalid token - $tx = $t->ua->build_tx( POST => '/api/v1/patrons' ); - $tx->req->cookies( { name => 'CGISESSID', value => 'some_session' } ); - $tx->req->headers->header( 'CSRF-TOKEN', 'invalid' ); - $tx->req->headers->content_type('application/json'); - $tx->req->body('{}'); - $t->request_ok($tx)->status_is(403)->json_is( '/error', 'Wrong CSRF token' ); - - $schema->storage->txn_rollback; -}; - -subtest 'POST with CGISESSID and valid CSRF token passes CSRF check' => sub { - - plan tests => 2; - - $schema->storage->txn_begin; - - my $session = Koha::Session->get_session( {} ); - my $session_id = $session->id; - my $csrf_token = Koha::Token->new->generate_csrf( { session_id => $session_id } ); - - my $tx = $t->ua->build_tx( POST => '/api/v1/patrons' ); - $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); - $tx->req->headers->header( 'CSRF-TOKEN', $csrf_token ); - $tx->req->headers->content_type('application/json'); - $tx->req->body('{}'); - $t->request_ok($tx); - - # Should NOT be 403 - it passes CSRF and hits auth (401) or validation (400) - isnt( $t->tx->res->code, 403, 'Valid CSRF token passes the check' ); - - $schema->storage->txn_rollback; -};