File Coverage

blib/lib/Apache/LoggedAuthDBI.pm
Criterion Covered Total %
statement 4 6 66.6
branch 1 2 50.0
condition n/a
subroutine 2 2 100.0
pod n/a
total 7 10 70.0


line stmt bran cond sub pod time code
1             package Apache::LoggedAuthDBI;
2              
3             $Apache::LoggedAuthDBI::VERSION = '0.12';
4              
5 1 50   1   844 use constant MP2 => $ENV{MOD_PERL_API_VERSION} == 2 ? 1 : 0;
  1         2  
  1         269  
6 1     1   1117 use Apache::AuthDBI;
  0            
  0            
7             use DBI;
8             use strict;
9              
10             use Exporter;
11             use vars qw(@ISA @EXPORT);
12              
13             @ISA = qw(Exporter);
14              
15              
16             BEGIN {
17             my @constants = qw( OK AUTH_REQUIRED FORBIDDEN DECLINED SERVER_ERROR );
18             if (MP2) {
19             require Apache2::Const;
20             import Apache2::Const @constants;
21             }
22             else {
23             require Apache::Constants;
24             import Apache::Constants @constants;
25             }
26             }
27              
28             # configuration attributes, defaults will be overwritten with values from .htaccess.
29              
30             my %CFG = (
31             'Auth_DBI_data_source' => '',
32             'Log_ADBI_table' => '',
33             'Log_ADBI_ip_field' => '',
34             'Log_ADBI_un_field' => '',
35             'Log_ADBI_status_field' => '',
36             'Log_ADBI_time_field' => ''
37             );
38             my $Attr = { };
39              
40              
41              
42             sub authen {
43             my ($r) = @_; # $r is the handler which allows direct access to Apache systems, DANGER!
44              
45             my $c = $r->connection;
46             my ($incomingIP) = $c->remote_ip;
47             my ($username) = $c->user;
48             my $s = $r->server;
49             my $serverName = $s->server_hostname;
50             my $client = &client($serverName);
51              
52             # $auth is what goes in the database
53             # $return_value is what the module returns
54             # $auth and $return_value do NOT have to be the same thing!
55              
56             my $auth = 'DECLINED'; # default it to declined. if its not bruteforce and login/pw are correct it'll be set to OK
57             my $return_value;
58              
59             my $errdocpath = $r->document_root;
60              
61             # get configuration
62             my ($key, $val);
63             while(($key, $val) = each %CFG) {
64             $val = $r->dir_config($key) || $val;
65             $key =~ s/^Log_ADBI_//;
66             $Attr->{$key} = $val;
67             }
68             $Attr->{data_source} = $r->dir_config('Auth_DBI_data_source');
69              
70              
71             # parse connect attributes, which may be tilde separated lists
72             my @data_sources = split(/~/, $Attr->{data_source});
73             my @usernames = split(/~/, $Attr->{username});
74             my @passwords = split(/~/, $Attr->{password});
75             $data_sources[0] = '' unless $data_sources[0]; # use ENV{DBI_DSN} if not defined
76              
77             # connect to database, use all data_sources until the connect succeeds
78             my $j;
79             my $dbh;
80             for ($j = 0; $j <= $#data_sources; $j++) {
81             last if ($dbh = DBI->connect($data_sources[$j], $usernames[$j], $passwords[$j]));
82             }
83             unless ($dbh) {
84             $r->log_reason("db connect error with data_source >$Attr->{data_source}<: $DBI::errstr", $r->uri);
85             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
86             }
87              
88             # connect to right database
89             #my $dbh = DBI->connect("DBI:mysql:$DB_CFG{$client.'dbname'}:$DB_CFG{$client.'dbhost'}", $DB_CFG{$client.'dblogin'}, $DB_CFG{$client.'dbpass'});
90              
91              
92              
93             #THE RULES
94             #
95             # configure the tolerance levels using the following 8 variables
96             #
97              
98             #autoreject if an IPaddress made X failed attempts in Y seconds
99             my $seconds_declined = 120;
100             my $times_declined = 5;
101              
102             #prevent brute force attacks, has an IPaddress made X attempts in Y seconds
103             my $seconds_brute_ip = 300;
104             my $times_brute_ip = 800;
105            
106             #prevent brute force attacks, has the same username been rejected X times in Y sec?
107             my $seconds_brute_username = 60;
108             my $times_brute_username = 3;
109              
110             #Prevent password sharing, has the same username accessed from X different IPs in Y sec
111             my $minutes_pw_shared = 180;
112             my $times_pw_shared = 30;
113              
114              
115             #SQL Queries to detect brute forcing and or pass sharing
116             # &get_count will return the number of entries that correspond to the query in $select. the result will be
117             # compared with the $times_... variable to detect a violation of our rules
118              
119             #autoreject if an IPaddress made X failed attempts in Y seconds
120             my $select = "SELECT id FROM ".$Attr->{table}." WHERE ".$Attr->{ip_field}."='$incomingIP' AND ".$Attr->{status_field}."<>'0' AND ".$Attr->{time_field}." > (DATE_SUB(NOW(), INTERVAL '$seconds_declined' SECOND))";
121             my $declined = &get_count($select, $dbh);
122              
123             #prevent brute force attacks, has an IPaddress made X attempts in Y seconds
124             $select = "SELECT id FROM ".$Attr->{table}." WHERE ".$Attr->{ip_field}."='$incomingIP' AND ".$Attr->{time_field}." > (DATE_SUB(NOW(), INTERVAL '$seconds_brute_ip' SECOND))";
125             my $brute_ip = &get_count($select, $dbh);
126              
127             #prevent brute force attacks, has the same username been rejected X times in Y sec?
128             $select = "SELECT id FROM ".$Attr->{table}." WHERE ".$Attr->{un_field}."='$username' AND ".$Attr->{status_field}."<>'0' AND ".$Attr->{time_field}." > (DATE_SUB(NOW(), INTERVAL '$seconds_brute_username' SECOND))";
129             my $brute_username = &get_count($select, $dbh);
130              
131             #Prevent password sharing, has the same username accessed from X different IPs in Y sec
132             $select = "SELECT distinct(".$Attr->{ip_field}.") ".$Attr->{table}." WHERE ".$Attr->{un_field}."='$username' AND ".$Attr->{time_field}. "> (DATE_SUB(NOW(), INTERVAL '$minutes_pw_shared' MINUTE))";
133             my $password_shared = &get_count($select, $dbh);
134              
135              
136              
137             #Take Action: in case of a detected violation beyond tolerance level send the user to an error page
138             if ($declined >= $times_declined) {
139             $r->filename($errdocpath . 'blocked.html');
140             $return_value = 'OK';
141             } elsif ($brute_ip >= $times_brute_ip || $brute_username >= $times_brute_username) {
142             $r->filename($errdocpath . 'brute_force.html');
143             $return_value = 'OK';
144             } elsif ($password_shared >= $times_pw_shared) {
145             $r->filename($errdocpath . 'pass_sharing.html');
146             $auth = 'PASS_SHARED';
147             $return_value = 'OK';
148              
149             #no brute force/pwsharing pass off to the main DBI authorization thingy...
150             } else {
151             $auth = Apache::AuthDBI::authen($r);
152             $return_value = $auth;
153             }
154              
155             #If this is the initial request log the attempt in the database. the ifcheck is necessary to screen out
156             #multiple entries caused by subrequests when everything goes through okay
157             if ($r->is_initial_req) {
158             my $sth = $dbh->prepare("INSERT INTO ".$Attr->{table}." VALUES ('', '$username', '$incomingIP', '$auth', NULL)");
159             $sth->execute;
160             }
161            
162             # disconnect from the database
163             $dbh->disconnect;
164              
165             # Return the $auth
166             return $return_value;
167             }
168              
169              
170             sub get_count {
171             my ($sql, $dbh) = @_;
172             my $sth = $dbh->prepare($sql);
173             my $rv = $sth->execute;
174             $rv = 0 if (!($rv > 0));
175             return $rv;
176             }
177              
178              
179             1;
180              
181             __END__