Subject: [PATCH] Bug 14407 - Allow restricting SCO to IP or IP range Fix presence of tabs in files, and printslip refering to $query instead of $input Modified so that if invalid ranges or IPs are given, access is simply denied: it fails secure. Includes unit tests of the new in_ipset function. Sponsored-by: Perth Bible College

From: Nicholas van Oudtshoorn <[email-masked]>
Date: Mon, 30 Oct 2017 16:27:35 +0800

---
 C4/Auth.pm                                         | 26 ++++++++++++++-
 C4/Installer/PerlDependencies.pm                   |  5 +++
 ..._14407-add_selfcheckallowbyipranges_syspref.sql |  2 ++
 installer/data/mysql/sysprefs.sql                  |  1 +
 .../en/modules/admin/preferences/circulation.pref  |  4 +++
 opac/sco/help.pl                                   |  8 ++++-
 opac/sco/printslip.pl                              |  7 +++-
 opac/sco/sco-main.pl                               | 10 ++++--
 opac/sco/sco-patron-image.pl                       |  6 ++++
 t/Auth.t                                           | 37 ++++++++++++++++++++++
 10 files changed, 101 insertions(+), 5 deletions(-)
 create mode 100644 installer/data/mysql/atomicupdate/bug_14407-add_selfcheckallowbyipranges_syspref.sql
 create mode 100644 t/Auth.t

diff --git a/C4/Auth.pm b/C4/Auth.pm
index 11f57b2..bcaaaeb 100644
--- a/C4/Auth.pm
+++ b/C4/Auth.pm
@@ -39,6 +39,7 @@ use Koha::Patrons;
 use POSIX qw/strftime/;
 use List::MoreUtils qw/ any /;
 use Encode qw( encode is_utf8);
+use Net::CIDR;
 
 # use utf8;
 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug $ldap $cas $caslogout $shib $shib_login);
@@ -55,7 +56,7 @@ BEGIN {
     @ISA       = qw(Exporter);
     @EXPORT    = qw(&checkauth &get_template_and_user &haspermission &get_user_subpermissions);
     @EXPORT_OK = qw(&check_api_auth &get_session &check_cookie_auth &checkpw &checkpw_internal &checkpw_hash
-      &get_all_subpermissions &get_user_subpermissions
+      &get_all_subpermissions &get_user_subpermissions &in_ipset
     );
     %EXPORT_TAGS = ( EditPermissions => [qw(get_all_subpermissions get_user_subpermissions)] );
     $ldap      = C4::Context->config('useldapserver') || 0;
@@ -2082,6 +2083,29 @@ sub haspermission {
     #FIXME - This fcn should return the failed permission so a suitable error msg can be delivered.
 }
 
+=head2 in_ipset
+
+  $flags = ($ipset);
+
+C<$ipset> A space separated string describing an IP set. Can include single IPs or ranges
+
+Returns 1 if the remote address is in the provided ipset, or 0 otherwise.
+
+=cut
+
+sub in_ipset {
+ my ($ipset) = @_;
+ my @allowedipranges = split(' ', $ipset);
+ if (scalar @allowedipranges > 0) {
+    my @rangelist;
+    eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
+    unless (Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist)) {
+      return 0;
+    }
+ }
+ return 1;
+}
+
 sub getborrowernumber {
     my ($userid) = @_;
     my $userenv = C4::Context->userenv;
diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm
index 3cacafc..0b08f34 100644
--- a/C4/Installer/PerlDependencies.pm
+++ b/C4/Installer/PerlDependencies.pm
@@ -802,6 +802,11 @@ our $PERL_DEPS = {
         'required' => '0',
         'min_ver'  => '0.56',
     },
+    'Net::CIDR'    => {
+        'usage'    => 'Core',
+        'required' => '1',
+        'min_ver'  => '0.15',
+    },
     'Net::SFTP::Foreign' => {
         'usage'    => 'Edifact',
         'required' => '0',
diff --git a/installer/data/mysql/atomicupdate/bug_14407-add_selfcheckallowbyipranges_syspref.sql b/installer/data/mysql/atomicupdate/bug_14407-add_selfcheckallowbyipranges_syspref.sql
new file mode 100644
index 0000000..4041a58
--- /dev/null
+++ b/installer/data/mysql/atomicupdate/bug_14407-add_selfcheckallowbyipranges_syspref.sql
@@ -0,0 +1,2 @@
+INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES
+	('SelfCheckAllowByIPRanges','',NULL,'(Leave blank if not used. Use ranges or simple ip addresses separated by spaces, like <code>192.168.1.1 192.168.0.0/24</code>.)','Short');
diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql
index f6e666b..b75f29c 100644
--- a/installer/data/mysql/sysprefs.sql
+++ b/installer/data/mysql/sysprefs.sql
@@ -475,6 +475,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
 ('SearchEngine','Zebra','Elasticsearch|Zebra','Search Engine','Choice'),
 ('SearchMyLibraryFirst','0',NULL,'If ON, OPAC searches return results limited by the user\'s library by default if they are logged in','YesNo'),
 ('SearchWithISBNVariations','0',NULL,'If enabled, search on all variations of the ISBN','YesNo'),
+('SelfCheckAllowByIPRanges','',NULL,'(Leave blank if not used. Use ranges or simple ip addresses separated by spaces, like <code>192.168.1.1 192.168.0.0/24</code>.)','Short'),
 ('SelfCheckHelpMessage','','70|10','Enter HTML to include under the basic Web-based Self Checkout instructions on the Help page','Textarea'),
 ('SelfCheckoutByLogin','1',NULL,'Have patrons login into the web-based self checkout system with their username/password or their cardnumber','YesNo'),
 ('SelfCheckReceiptPrompt','1','NULL','If ON, print receipt dialog pops up when self checkout is finished','YesNo'),
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref
index b77b373..e308290 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref
@@ -824,6 +824,10 @@ Circulation:
                   yes: Show
                   no: "Don't show"
             - "the print receipt popup dialog when self checkout is finished"
+        -
+            - pref: SelfCheckAllowByIPRanges
+              class: short
+            - (Leave blank if not used. Use ranges or simple ip addresses separated by spaces, like <code>192.168.1.1 192.168.0.0/24</code>.)
     Course Reserves:
         -
             - pref: UseCourseReserves
diff --git a/opac/sco/help.pl b/opac/sco/help.pl
index 412dc40..5c17cc6 100755
--- a/opac/sco/help.pl
+++ b/opac/sco/help.pl
@@ -25,10 +25,16 @@ use strict;
 use warnings;
 use CGI qw ( -utf8 );
 
-use C4::Auth   qw(get_template_and_user);
+use C4::Auth   qw(get_template_and_user in_ipset);
 use C4::Output qw(output_html_with_http_headers);
 
 my $query = new CGI;
+
+unless ( in_ipset(C4::Context->preference('SelfCheckAllowByIPRanges')) ) {
+    print $query->redirect("/cgi-bin/koha/opac-main.pl");
+    exit;
+}
+
 my ($template, $borrowernumber, $cookie) = get_template_and_user({
     template_name => "sco/help.tt",
     query => $query,
diff --git a/opac/sco/printslip.pl b/opac/sco/printslip.pl
index b85b8f5..98e8981 100755
--- a/opac/sco/printslip.pl
+++ b/opac/sco/printslip.pl
@@ -30,12 +30,17 @@ use strict;
 use warnings;
 use CGI qw ( -utf8 );
 use C4::Context;
-use C4::Auth qw/:DEFAULT get_session/;
+use C4::Auth qw/:DEFAULT get_session in_ipset/;
 use C4::Output;
 use C4::Members;
 use C4::Koha;
 
 my $input = new CGI;
+unless ( in_ipset(C4::Context->preference('SelfCheckAllowByIPRanges')) ) {
+    print $input->header(status => '403 Forbidden - functionality not available from your location');
+    exit;
+}
+
 my $sessionID = $input->cookie("CGISESSID");
 my $session = get_session($sessionID);
 
diff --git a/opac/sco/sco-main.pl b/opac/sco/sco-main.pl
index d6e4176..d93fc07 100755
--- a/opac/sco/sco-main.pl
+++ b/opac/sco/sco-main.pl
@@ -35,7 +35,7 @@ use Modern::Perl;
 
 use CGI qw ( -utf8 );
 
-use C4::Auth qw(get_template_and_user checkpw);
+use C4::Auth qw(get_template_and_user checkpw in_ipset);
 use C4::Koha;
 use C4::Circulation;
 use C4::Reserves;
@@ -58,7 +58,13 @@ unless (C4::Context->preference('WebBasedSelfCheck')) {
     exit;
 }
 
-if (C4::Context->preference('AutoSelfCheckAllowed')) 
+unless ( in_ipset(C4::Context->preference('SelfCheckAllowByIPRanges')) ) {
+    # redirect to OPAC home if self-checkout not permitted from current IP
+    print $query->redirect("/cgi-bin/koha/opac-main.pl");
+    exit;
+}
+
+if (C4::Context->preference('AutoSelfCheckAllowed'))
 {
     my $AutoSelfCheckID = C4::Context->preference('AutoSelfCheckID');
     my $AutoSelfCheckPass = C4::Context->preference('AutoSelfCheckPass');
diff --git a/opac/sco/sco-patron-image.pl b/opac/sco/sco-patron-image.pl
index dc6fbc8..6ee9a02 100755
--- a/opac/sco/sco-patron-image.pl
+++ b/opac/sco/sco-patron-image.pl
@@ -19,6 +19,7 @@
 
 use strict;
 use warnings;
+use C4::Auth qw(in_ipset);
 use C4::Service;
 use C4::Members;
 use Koha::Patron::Images;
@@ -36,6 +37,11 @@ unless (C4::Context->preference('ShowPatronImageInWebBasedSelfCheck')) {
     exit;
 }
 
+unless ( in_ipset(C4::Context->preference('SelfCheckAllowByIPRanges')) ) {
+    print $query->header(status => '403 Forbidden - functionality not available from your location');
+    exit;
+}
+
 my ($borrowernumber) = C4::Service->require_params('borrowernumber');
 my ($csrf_token) = C4::Service->require_params('csrf_token');
 
diff --git a/t/Auth.t b/t/Auth.t
new file mode 100644
index 0000000..739e3d9
--- /dev/null
+++ b/t/Auth.t
@@ -0,0 +1,37 @@
+# This file is part of Koha.
+#
+# Copyright (C) 2017 Nicholas van Oudtshoorn
+#
+# 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 <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+use Test::More tests => 10;
+
+use C4::Auth qw / in_ipset /;
+use warnings;
+
+$ENV{REMOTE_ADDR} = '192.168.1.30';
+my $ipset1 = "192.168.1.30";
+
+ok(in_ipset("192.168.1.30"), 'simple single ip matching remote ip');
+ok(!in_ipset("192.168.1.31"), 'simple single ip not match remote ip');
+ok(in_ipset("192.168.1.1/24"), 'simple ip range/24 with remote ip in it');
+ok(!in_ipset("192.168.2.1/24"), 'simple ip range/24 with remote ip not in it');
+ok(in_ipset("192.168.2.1/16"), 'simple ip range/16 with remote ip in it');
+ok(!in_ipset("192.168.1.10-30"), 'invalidly represented IP range with remote ip in it');
+ok(in_ipset("192.168.1.10-192.168.1.30"), 'validly represented ip range with remote ip in it');
+ok(in_ipset("127.0.0.1 192.168.1.30 192.168.2.10-192.168.2.25"), 'multiple ips and ranges, including the remote ip');
+ok(!in_ipset("127.0.0.1 8.8.8.8 192.168.2.1/24 192.168.3.1/24 192.168.1.1-192.168.1.29"), "multiple ip and ip ranges, with the remote ip in none of them");
+ok(in_ipset(""), "blank list given, no preference set - implies everything goes through.");
+
-- 
2.7.5