##// END OF EJS Templates
Fixed: Redmine.pm potential security issue with cache credential enabled and subversion (#9567)....
Jean-Philippe Lang -
r7689:38089d9a5606
parent child
Show More
@@ -1,427 +1,429
1 1 package Apache::Authn::Redmine;
2 2
3 3 =head1 Apache::Authn::Redmine
4 4
5 5 Redmine - a mod_perl module to authenticate webdav subversion users
6 6 against redmine database
7 7
8 8 =head1 SYNOPSIS
9 9
10 10 This module allow anonymous users to browse public project and
11 11 registred users to browse and commit their project. Authentication is
12 12 done against the redmine database or the LDAP configured in redmine.
13 13
14 14 This method is far simpler than the one with pam_* and works with all
15 15 database without an hassle but you need to have apache/mod_perl on the
16 16 svn server.
17 17
18 18 =head1 INSTALLATION
19 19
20 20 For this to automagically work, you need to have a recent reposman.rb
21 21 (after r860) and if you already use reposman, read the last section to
22 22 migrate.
23 23
24 24 Sorry ruby users but you need some perl modules, at least mod_perl2,
25 25 DBI and DBD::mysql (or the DBD driver for you database as it should
26 26 work on allmost all databases).
27 27
28 28 On debian/ubuntu you must do :
29 29
30 30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
31 31
32 32 If your Redmine users use LDAP authentication, you will also need
33 33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
34 34
35 35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
36 36
37 37 =head1 CONFIGURATION
38 38
39 39 ## This module has to be in your perl path
40 40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
41 41 PerlLoadModule Apache::Authn::Redmine
42 42 <Location /svn>
43 43 DAV svn
44 44 SVNParentPath "/var/svn"
45 45
46 46 AuthType Basic
47 47 AuthName redmine
48 48 Require valid-user
49 49
50 50 PerlAccessHandler Apache::Authn::Redmine::access_handler
51 51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
52 52
53 53 ## for mysql
54 54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55 55 ## for postgres
56 56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57 57
58 58 RedmineDbUser "redmine"
59 59 RedmineDbPass "password"
60 60 ## Optional where clause (fulltext search would be slow and
61 61 ## database dependant).
62 62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
63 63 ## Optional credentials cache size
64 64 # RedmineCacheCredsMax 50
65 65 </Location>
66 66
67 67 To be able to browse repository inside redmine, you must add something
68 68 like that :
69 69
70 70 <Location /svn-private>
71 71 DAV svn
72 72 SVNParentPath "/var/svn"
73 73 Order deny,allow
74 74 Deny from all
75 75 # only allow reading orders
76 76 <Limit GET PROPFIND OPTIONS REPORT>
77 77 Allow from redmine.server.ip
78 78 </Limit>
79 79 </Location>
80 80
81 81 and you will have to use this reposman.rb command line to create repository :
82 82
83 83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
84 84
85 85 =head1 MIGRATION FROM OLDER RELEASES
86 86
87 87 If you use an older reposman.rb (r860 or before), you need to change
88 88 rights on repositories to allow the apache user to read and write
89 89 S<them :>
90 90
91 91 sudo chown -R www-data /var/svn/*
92 92 sudo chmod -R u+w /var/svn/*
93 93
94 94 And you need to upgrade at least reposman.rb (after r860).
95 95
96 96 =cut
97 97
98 98 use strict;
99 99 use warnings FATAL => 'all', NONFATAL => 'redefine';
100 100
101 101 use DBI;
102 102 use Digest::SHA1;
103 103 # optional module for LDAP authentication
104 104 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
105 105
106 106 use Apache2::Module;
107 107 use Apache2::Access;
108 108 use Apache2::ServerRec qw();
109 109 use Apache2::RequestRec qw();
110 110 use Apache2::RequestUtil qw();
111 111 use Apache2::Const qw(:common :override :cmd_how);
112 112 use APR::Pool ();
113 113 use APR::Table ();
114 114
115 115 # use Apache2::Directive qw();
116 116
117 117 my @directives = (
118 118 {
119 119 name => 'RedmineDSN',
120 120 req_override => OR_AUTHCFG,
121 121 args_how => TAKE1,
122 122 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
123 123 },
124 124 {
125 125 name => 'RedmineDbUser',
126 126 req_override => OR_AUTHCFG,
127 127 args_how => TAKE1,
128 128 },
129 129 {
130 130 name => 'RedmineDbPass',
131 131 req_override => OR_AUTHCFG,
132 132 args_how => TAKE1,
133 133 },
134 134 {
135 135 name => 'RedmineDbWhereClause',
136 136 req_override => OR_AUTHCFG,
137 137 args_how => TAKE1,
138 138 },
139 139 {
140 140 name => 'RedmineCacheCredsMax',
141 141 req_override => OR_AUTHCFG,
142 142 args_how => TAKE1,
143 143 errmsg => 'RedmineCacheCredsMax must be decimal number',
144 144 },
145 145 );
146 146
147 147 sub RedmineDSN {
148 148 my ($self, $parms, $arg) = @_;
149 149 $self->{RedmineDSN} = $arg;
150 150 my $query = "SELECT
151 151 hashed_password, salt, auth_source_id, permissions
152 152 FROM members, projects, users, roles, member_roles
153 153 WHERE
154 154 projects.id=members.project_id
155 155 AND member_roles.member_id=members.id
156 156 AND users.id=members.user_id
157 157 AND roles.id=member_roles.role_id
158 158 AND users.status=1
159 159 AND login=?
160 160 AND identifier=? ";
161 161 $self->{RedmineQuery} = trim($query);
162 162 }
163 163
164 164 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
165 165 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
166 166 sub RedmineDbWhereClause {
167 167 my ($self, $parms, $arg) = @_;
168 168 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
169 169 }
170 170
171 171 sub RedmineCacheCredsMax {
172 172 my ($self, $parms, $arg) = @_;
173 173 if ($arg) {
174 174 $self->{RedmineCachePool} = APR::Pool->new;
175 175 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
176 176 $self->{RedmineCacheCredsCount} = 0;
177 177 $self->{RedmineCacheCredsMax} = $arg;
178 178 }
179 179 }
180 180
181 181 sub trim {
182 182 my $string = shift;
183 183 $string =~ s/\s{2,}/ /g;
184 184 return $string;
185 185 }
186 186
187 187 sub set_val {
188 188 my ($key, $self, $parms, $arg) = @_;
189 189 $self->{$key} = $arg;
190 190 }
191 191
192 192 Apache2::Module::add(__PACKAGE__, \@directives);
193 193
194 194
195 195 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
196 196
197 197 sub access_handler {
198 198 my $r = shift;
199 199
200 200 unless ($r->some_auth_required) {
201 201 $r->log_reason("No authentication has been configured");
202 202 return FORBIDDEN;
203 203 }
204 204
205 205 my $method = $r->method;
206 206 return OK unless defined $read_only_methods{$method};
207 207
208 208 return OK if is_authentication_forced($r);
209 209
210 210 my $project_id = get_project_identifier($r);
211 211
212 212 $r->set_handlers(PerlAuthenHandler => [\&OK])
213 213 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
214 214
215 215 return OK
216 216 }
217 217
218 218 sub authen_handler {
219 219 my $r = shift;
220 220
221 221 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
222 222 return $res unless $res == OK;
223 223
224 224 my $project_id = get_project_identifier($r);
225 225 my $method = $r->method;
226 226 if (defined $read_only_methods{$method} && is_public_project($project_id, $r) && non_member_role_allows_browse_repository($r)) {
227 227 return OK;
228 228 }
229 229
230 230 if (is_member($r->user, $redmine_pass, $r)) {
231 231 return OK;
232 232 } else {
233 233 $r->note_auth_failure();
234 234 return AUTH_REQUIRED;
235 235 }
236 236 }
237 237
238 238 # check if authentication is forced
239 239 sub is_authentication_forced {
240 240 my $r = shift;
241 241
242 242 my $dbh = connect_database($r);
243 243 my $sth = $dbh->prepare(
244 244 "SELECT value FROM settings where settings.name = 'login_required';"
245 245 );
246 246
247 247 $sth->execute();
248 248 my $ret = 0;
249 249 if (my @row = $sth->fetchrow_array) {
250 250 if ($row[0] eq "1" || $row[0] eq "t") {
251 251 $ret = 1;
252 252 }
253 253 }
254 254 $sth->finish();
255 255 undef $sth;
256 256
257 257 $dbh->disconnect();
258 258 undef $dbh;
259 259
260 260 $ret;
261 261 }
262 262
263 263 sub is_public_project {
264 264 my $project_id = shift;
265 265 my $r = shift;
266 266
267 267 my $dbh = connect_database($r);
268 268 my $sth = $dbh->prepare(
269 269 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
270 270 );
271 271
272 272 $sth->execute($project_id);
273 273 my $ret = 0;
274 274 if (my @row = $sth->fetchrow_array) {
275 275 if ($row[0] eq "1" || $row[0] eq "t") {
276 276 $ret = 1;
277 277 }
278 278 }
279 279 $sth->finish();
280 280 undef $sth;
281 281 $dbh->disconnect();
282 282 undef $dbh;
283 283
284 284 $ret;
285 285 }
286 286
287 287 sub system_role_allows_browse_repository {
288 288 my $r = shift;
289 289 my $system_role = shift;
290 290
291 291 my $dbh = connect_database($r);
292 292 my $sth = $dbh->prepare(
293 293 "SELECT permissions FROM roles WHERE builtin = ?;"
294 294 );
295 295
296 296 $sth->execute($system_role);
297 297 my $ret = 0;
298 298 if (my @row = $sth->fetchrow_array) {
299 299 if ($row[0] =~ /:browse_repository/) {
300 300 $ret = 1;
301 301 }
302 302 }
303 303 $sth->finish();
304 304 undef $sth;
305 305 $dbh->disconnect();
306 306 undef $dbh;
307 307
308 308 $ret;
309 309 }
310 310
311 311 sub non_member_role_allows_browse_repository {
312 312 my $r = shift;
313 313 my $ret = system_role_allows_browse_repository($r, 1);
314 314 $ret;
315 315 }
316 316
317 317 sub anonymous_role_allows_browse_repository {
318 318 my $r = shift;
319 319 my $ret = system_role_allows_browse_repository($r, 2);
320 320 $ret;
321 321 }
322 322
323 323 # perhaps we should use repository right (other read right) to check public access.
324 324 # it could be faster BUT it doesn't work for the moment.
325 325 # sub is_public_project_by_file {
326 326 # my $project_id = shift;
327 327 # my $r = shift;
328 328
329 329 # my $tree = Apache2::Directive::conftree();
330 330 # my $node = $tree->lookup('Location', $r->location);
331 331 # my $hash = $node->as_hash;
332 332
333 333 # my $svnparentpath = $hash->{SVNParentPath};
334 334 # my $repos_path = $svnparentpath . "/" . $project_id;
335 335 # return 1 if (stat($repos_path))[2] & 00007;
336 336 # }
337 337
338 338 sub is_member {
339 339 my $redmine_user = shift;
340 340 my $redmine_pass = shift;
341 341 my $r = shift;
342 342
343 343 my $dbh = connect_database($r);
344 344 my $project_id = get_project_identifier($r);
345 345
346 346 my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
347 347
348 my $access_mode = defined $read_only_methods{$r->method} ? "R" : "W";
349
348 350 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
349 351 my $usrprojpass;
350 352 if ($cfg->{RedmineCacheCredsMax}) {
351 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
353 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
352 354 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
353 355 }
354 356 my $query = $cfg->{RedmineQuery};
355 357 my $sth = $dbh->prepare($query);
356 358 $sth->execute($redmine_user, $project_id);
357 359
358 360 my $ret;
359 361 while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
360 362
361 363 unless ($auth_source_id) {
362 364 my $method = $r->method;
363 365 my $salted_password = Digest::SHA1::sha1_hex($salt.$pass_digest);
364 if ($hashed_password eq $salted_password && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
366 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
365 367 $ret = 1;
366 368 last;
367 369 }
368 370 } elsif ($CanUseLDAPAuth) {
369 371 my $sthldap = $dbh->prepare(
370 372 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
371 373 );
372 374 $sthldap->execute($auth_source_id);
373 375 while (my @rowldap = $sthldap->fetchrow_array) {
374 376 my $ldap = Authen::Simple::LDAP->new(
375 377 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
376 378 port => $rowldap[1],
377 379 basedn => $rowldap[5],
378 380 binddn => $rowldap[3] ? $rowldap[3] : "",
379 381 bindpw => $rowldap[4] ? $rowldap[4] : "",
380 382 filter => "(".$rowldap[6]."=%s)"
381 383 );
382 384 my $method = $r->method;
383 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
385 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
384 386
385 387 }
386 388 $sthldap->finish();
387 389 undef $sthldap;
388 390 }
389 391 }
390 392 $sth->finish();
391 393 undef $sth;
392 394 $dbh->disconnect();
393 395 undef $dbh;
394 396
395 397 if ($cfg->{RedmineCacheCredsMax} and $ret) {
396 398 if (defined $usrprojpass) {
397 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
399 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
398 400 } else {
399 401 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
400 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
402 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
401 403 $cfg->{RedmineCacheCredsCount}++;
402 404 } else {
403 405 $cfg->{RedmineCacheCreds}->clear();
404 406 $cfg->{RedmineCacheCredsCount} = 0;
405 407 }
406 408 }
407 409 }
408 410
409 411 $ret;
410 412 }
411 413
412 414 sub get_project_identifier {
413 415 my $r = shift;
414 416
415 417 my $location = $r->location;
416 418 my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
417 419 $identifier;
418 420 }
419 421
420 422 sub connect_database {
421 423 my $r = shift;
422 424
423 425 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
424 426 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
425 427 }
426 428
427 429 1;
General Comments 0
You need to be logged in to leave comments. Login now