Various fixes for XSS and bad crypto. No claim to completeness.
[mirror/userdir-ldap-cgi.git] / update.cgi
1 #!/usr/bin/perl
2
3 # $Id: update.cgi,v 1.13 2006/12/28 02:44:02 rmurray Exp $
4 # (c) 1999 Randolph Chung. Licensed under the GPL. <tausq@debian.org>
5 # (c) 2006 Ryan Murray. Licensed under the GPL. <rmurray@debian.org>
6
7 use lib '.';
8 use strict vars;
9 #use Apache::Registry;
10 use CGI;
11 use Data::UUID;
12 use Digest::HMAC_SHA1 qw(hmac_sha1_hex);
13 use Digest::MD5 qw(md5_hex);
14 use Util;
15 use English;
16 use URI::Escape;
17 use Crypt::PasswdMD5;
18 use Net::LDAP qw(LDAP_SUCCESS LDAP_PROTOCOL_ERROR);
19 use Net::LDAP::Util qw(ldap_explode_dn);
20
21 my %config = &Util::ReadConfigFile;
22
23 my $query = new CGI;
24 my $proto = ($ENV{HTTPS} ? "https" : "http");
25
26 my $id = $query->param('id');
27 my $authtoken = $query->param('authtoken');
28
29 my $password = undef;
30
31 if ($authtoken || $id) {
32   $password = Util::TouchAuthToken($authtoken, $id);
33 } else {
34   $password = '';
35   $id = '';
36   $authtoken = '';
37 }
38
39 if ($proto eq "http" || !($id && $password)) {
40   print "Location: https://$ENV{SERVER_NAME}/$config{webloginhtml}\n\n";
41   exit;
42
43
44 my $ldap;
45
46 open (F, "<$config{passdir}/key-hmac-$UID") || &Util::HTMLError($!);
47 my $hmac_key = <F>;
48 close(F);
49 chomp($hmac_key);
50
51 sub DieHandler {
52   $ldap->unbind if (defined($ldap));
53 }
54
55 #$SIG{__DIE__} = \&DieHandler;
56 my $editdn = "uid=$id,$config{basedn}";
57
58 $ldap = Net::LDAP->new($config{ldaphost});
59 &Util::UpgradeConnection($ldap) unless $config{usessl} eq 'False';
60 my $auth = 0;
61 my $mesg;
62 $mesg = $ldap->bind($editdn, password => $password);
63 $mesg->sync;
64 $auth = ($mesg->code == LDAP_SUCCESS);
65
66 if (!$auth) {
67   $ldap->unbind;
68   &Util::HTMLError("You have not been authenticated. Please <a href=\"https://$ENV{SERVER_NAME}/$config{webloginhtml}\">Login</a>");
69 }
70
71 # Authenticated....
72 # Get our entry...
73 $mesg = $ldap->search(base   => $editdn,
74                       filter => "uid=*");
75 $mesg->code && &Util::HTMLError($mesg->error);
76   
77 my $entries = $mesg->as_struct;
78 if ($mesg->count != 1) {
79   # complain and quit
80 }
81
82 $mesg = $ldap->search(base  => $config{hostbasedn}, filter => 'host=*');
83 $mesg->code && &Util::HTMLError($mesg->error);
84 my $host_entries = $mesg->as_struct;
85
86 my @dns = keys(%$entries);
87 my $entry = $entries->{$dns[0]};
88
89 if (!($query->param('doupdate'))) {
90   # Not yet update, just fill in the form with the current values
91   my %data;
92   
93   # Fill in %data
94   # First do the easy stuff - this catches most of the cases
95   foreach (keys(%$entry)) {
96     $data{$_} = $entry->{$_}->[0];
97     $data{$_} = CGI::escapeHTML($data{$_}) if defined $data{$_};
98   }
99
100   $data{gender} = 9 if not $data{gender};
101
102   # Now we have to fill in the rest that needs some processing...
103   $data{id} = $id;
104   $data{authtoken} = $authtoken;
105   $data{editdn} = $editdn;
106   $data{staddress} = $entry->{postaladdress}->[0];
107   $data{staddress} =~ s/\$/\n/;
108   $data{staddress} = CGI::escapeHTML($data{staddress});
109   $data{countryname} = &Util::LookupCountry($data{c});
110
111   if ($data{mailgreylisting} eq "TRUE") {
112     $data{mailgreylisting} = " checked";
113   } else {
114     $data{mailgreylisting} = "";
115   }
116
117   if ($data{mailcallout} eq "TRUE") {
118     $data{mailcallout} = " checked";
119   } else {
120     $data{mailcallout} = "";
121   }
122
123   $data{maildefaultoptions} ||= 'TRUE';
124
125   if ($data{maildefaultoptions} eq "TRUE") {
126     $data{maildefaultoptions} = " checked";
127   } else {
128     $data{maildefaultoptions} = "";
129   }
130
131   $data{mailcontentinspectionaction} ||= 'reject';
132   
133   $data{email} = CGI::escapeHTML(join(", ", @{$entry->{emailforward}}));
134
135   my $genderselect = '<select name="gender">'
136                    . '<option value="9"'
137                    . ($data{gender} == 9 ? ' selected' : '')
138                    . '>unspecified'
139                    . '<option value="1"'
140                    . ($data{gender} == 1 ? ' selected' : '')
141                    . '>male<option value="2"'
142                    . ($data{gender} == 2 ? ' selected' : '')
143                    . '>female</select>';
144   my $mailcontentselect = '<select name="mailcontentinspection">'
145                         . '<option value="blackhole"'
146                         . ($data{mailcontentinspectionaction} eq 'blackhole' ? ' selected' : '')
147                         . '>blackhole'
148                         . '<option value="markup"'
149                         . ($data{mailcontentinspectionaction} eq 'markup' ? ' selected' : '')
150                         . '>markup'
151                         . '<option value="reject"'
152                         . ($data{mailcontentinspectionaction} eq 'reject' ? ' selected' : '')
153                         . '>reject';
154
155   my $confirmstring = '';
156   my $sudopassword = '';
157   for my $e(@{$entry->{'sudopassword'}}) {
158     my ($uuid, $status, $hosts, $crypted) = ($e =~ /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}) (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*-]+) ([^ ]+)$/);
159     unless (defined $uuid) {
160       $sudopassword .= "<tr><td>Unparseable line!</td></tr>\n";
161       next;
162     }
163     if ($status =~ /^confirmed:/) {
164       my $data = join(':', 'password-is-confirmed', 'sudo', $data{'uid'}, $uuid, $hosts, $crypted);
165       my $hmac = hmac_sha1_hex( $data, $hmac_key);
166       if ($status eq "confirmed:$hmac") {
167         $status = 'confirmed';
168       } else {
169         $status = 'INVALID';
170       }
171     }
172     my $e = "<tr><td>".CGI::escapeHTML($hosts)."</td>
173                  <td>".CGI::escapeHTML($status)."</td>
174                  <td><small>not shown</small></td>
175                  <!--<td><small><code>".CGI::escapeHTML($uuid)."</code></small></td>-->
176                  <td><input name=\"sudopassword-delete-".CGI::escapeHTML($uuid)."\" type=\"checkbox\" value=\"delete\"> (delete)</td></tr>\n";
177     $sudopassword .= $e;
178     if ($status eq 'unconfirmed') {
179       my $data = join(':', 'confirm-new-password', 'sudo', $data{'uid'}, $uuid, $hosts, $crypted);
180       my $hmac = hmac_sha1_hex( $data, $hmac_key);
181       $confirmstring .= CGI::escapeHTML("confirm sudopassword $uuid $hosts $hmac\n");
182     }
183   };
184   if ($confirmstring ne '') {
185     $confirmstring = "<br>To confirm your new sudo passwords send signed mail to changes\@db.debian.org with a signed body containing these lines:<br><pre>$confirmstring</pre>";
186   }
187
188   my $sudopasswordhosts = '<select name="newsudopass-host"> <option value="*">ALL';
189   for my $dn (sort {$host_entries->{$a}->{host}->[0] cmp $host_entries->{$b}->{host}->[0]} keys(%$host_entries)) {
190       my $data = $host_entries->{$dn};
191       my $host = $data->{'host'}->[0];
192       my $hostname = $data->{'hostname'}->[0];
193       $sudopasswordhosts .= "<option value=\"$hostname\">$host\n";
194   };
195   $sudopasswordhosts .= '</select>';
196
197   # finally we can send output...
198   my ($sub, $substr);
199   &Util::HTMLSendHeader;
200   open (F, "<$config{webupdatehtml}") || &Util::HTMLError($!);
201   while (<F>) {
202     s/~(.+?)~/$data{$1}/g;
203     s/<\?genderselect>/$genderselect/;
204     s/<\?mailcontentselect>/$mailcontentselect/;
205     s/<\?sudopassword>/$sudopassword/;
206     s/<\?sudopasswordhosts>/$sudopasswordhosts/;
207     s/<\?confirmstring>/$confirmstring/;
208     print;
209   }
210   close F;
211 } else {
212   my @ldapinfo_for_pwcheck;
213   for my $a (qw{cn sn md gecos uid}) {
214     for my $e (@{$entry->{$a}}) {
215       push @ldapinfo_for_pwcheck, $e;
216     }
217   }
218
219
220   # Actually update stuff...
221   my ($newpassword, $newstaddress, $newwebpassword, $newrtcpassword);
222   
223   # Good god, why would we want to do that here?  it breaks password setting
224   # etc, and it doesn't prevent people from setting eveil stuff in ldap
225   # directly.
226   # &Util::FixParams($query);
227
228   if (($query->param('labeleduri')) && 
229       ($query->param('labeleduri') !~ /^https?:\/\//i)) {
230     &Util::HTMLError("Your homepage URL is invalid");
231   }
232   
233   if ($query->param('newpass') && $query->param('newpassvrfy')) {
234     if ($query->param('newpass') ne $query->param('newpassvrfy')) {
235       # passwords don't match...
236       &Util::HTMLError("The passwords you specified do not match. Please go back and try again.");
237     }
238
239     my ($r, $msg) = &Util::checkPasswordQuality($query->param('newpass'), undef, [@ldapinfo_for_pwcheck]);
240     if ($r) {
241       &Util::HTMLError("Password check failed: $msg.  Please go back and try again.");
242     }
243
244     # create a md5 crypted password
245     $newpassword = '{crypt}'.crypt($query->param('newpass'), &Util::CreateCryptSalt(1));
246     
247     &Util::LDAPUpdate($ldap, $editdn, 'userPassword', $newpassword);
248     &Util::UpdateAuthToken($authtoken, $query->param('newpass'));
249   }  
250
251   if ($query->param('newwebpass') && $query->param('newwebpassvrfy')) {
252     if ($query->param('newwebpass') ne $query->param('newwebpassvrfy')) {
253       # passwords don't match...
254       &Util::HTMLError("The web-passwords you specified do not match. Please go back and try again.");
255     }
256
257     my ($r, $msg) = &Util::checkPasswordQuality($query->param('newwebpass'), undef, [@ldapinfo_for_pwcheck]);
258     if ($r) {
259       &Util::HTMLError("Password check failed for web-password: $msg.  Please go back and try again.");
260     }
261
262     # create a md5 crypted password
263     $newwebpassword = apache_md5_crypt($query->param('newwebpass'), &Util::CreateCryptSalt(1, 1));
264     
265     &Util::LDAPUpdate($ldap, $editdn, 'webPassword', $newwebpassword);
266   }  
267   if ($query->param('newrtcpass') && $query->param('newrtcpassvrfy')) {
268     if ($query->param('newrtcpass') ne $query->param('newrtcpassvrfy')) {
269       # passwords don't match...
270       &Util::HTMLError("The rtc-passwords you specified do not match. Please go back and try again.");
271     }
272
273     my ($r, $msg) = &Util::checkPasswordQuality($query->param('newrtcpass'), undef, [@ldapinfo_for_pwcheck]);
274     if ($r) {
275       &Util::HTMLError("Password check failed for rtc-password: $msg.  Please go back and try again.");
276     }
277     # create a md5 crypted password
278     $newrtcpassword = &md5_hex( ldap_explode_dn($editdn)->[0]{UID} . '@debian.org:rtc.debian.org:' . $query->param('newrtcpass') );
279
280     &Util::LDAPUpdate($ldap, $editdn, 'rtcPassword', $newrtcpassword);
281   }  
282
283   $newstaddress = $query->param('staddress');
284   $newstaddress =~ s/\n/\$/m;
285
286   my $gender = $query->param('gender');
287   if ($gender != 1 && $gender != 2) {
288     $gender = 9; # unspecified
289   }
290   
291   my ($bd_ok, $bd_yr, $bd_mo, $bd_day);
292
293   if ($query->param('birthdate') =~ /^([1-9][0-9]{3})([01][0-9])([0-3][0-9])$/) {
294     $bd_yr = $1; $bd_mo = $2; $bd_day = $3;
295     if ($bd_yr > 1850 and $bd_mo > 0 and $bd_mo <= 12 and $bd_day > 0) {
296       if ($bd_mo == 2) {
297          if ($bd_day == 29 and ($bd_yr % 4 == 0 && ($bd_yr % 100 != 0 || $bd_yr % 400 == 0))) {
298            $bd_ok = 1;
299          } elsif ($bd_day <= 28) {
300            $bd_ok = 1;
301          }
302       } elsif ($bd_mo == 4 or $bd_mo == 6 or $bd_mo == 9 or $bd_mo == 11) {
303         if ($bd_day <= 30) {
304           $bd_ok = 1;
305         }
306       } else {
307         if ($bd_day <= 31) {
308           $bd_ok = 1;
309         }
310       }
311     }
312   } elsif (not defined($query->param('birthdate')) or $query->param('birthdate') =~ /^\s*$/) {
313     $bd_ok = 1;
314   }
315   my ($lat, $long);
316   ($lat, $long) = &Util::CheckLatLong($query->param('latitude'), 
317                                       $query->param('longitude'));
318   my ($greylisting, $callout, $mailcontentinspection, $defaultoptions);
319
320   $greylisting = $query->param('mailgreylisting');
321   if (!$greylisting or $greylisting ne "TRUE") {
322      $greylisting = "FALSE";
323   }
324
325   $callout = $query->param('mailcallout');
326   if (!$callout or $callout ne "TRUE") {
327      $callout = "FALSE";
328   }
329
330   $mailcontentinspection = $query->param('mailcontentinspection');
331   if (!$mailcontentinspection or ($mailcontentinspection ne "blackhole" and $mailcontentinspection ne "markup")) {
332      $mailcontentinspection = "reject";
333   }
334
335   $defaultoptions = $query->param('maildefaultoptions');
336   if (!$defaultoptions or $defaultoptions ne "TRUE") {
337      $defaultoptions = "FALSE";
338   }
339
340   my $newsudo;
341   my $newsudo_hosts;
342   if ($query->param('newsudopass') && $query->param('newsudopassvrfy')) {
343     my $host = $query->param('newsudopass-host');
344     if ($host =~ /[^a-z0-9.-]/ and $host ne '*') {
345       &Util::HTMLError("The sudo host has weird characters '$host'.");
346     }
347
348     if ($query->param('newsudopass') ne $query->param('newsudopassvrfy')) {
349       &Util::HTMLError("The sudo passwords you specified do not match. Please go back and try again.");
350     }
351
352     my $ldappass = $password;
353     $ldappass = $query->param('newpass') if $query->param('newpass');
354     push @ldapinfo_for_pwcheck, $host, split(/\./, $host);
355     my ($r, $msg) = &Util::checkPasswordQuality($query->param('newsudopass'), $ldappass, [@ldapinfo_for_pwcheck]);
356     if ($r) {
357       &Util::HTMLError("Password check failed for new sudo pass: $msg.  Please go back and try again.");
358     }
359
360     # create a md5 crypted password
361     my $newsudopassword = crypt($query->param('newsudopass'), &Util::CreateCryptSalt(1));
362     my $ug = new Data::UUID;
363     my $uuid = $ug->create_str();
364
365     $newsudo = "$uuid unconfirmed $host $newsudopassword";
366     $newsudo_hosts = $host;
367   }
368
369   my %delete_uuids = map { s/^sudopassword-delete-//; $_ => 1} grep { $query->param($_) eq 'delete' } grep { /^sudopassword-delete-/ } $query->param;
370   my @keepsudo;
371   for my $entry (@{$entry->{'sudopassword'}}) {
372     my ($uuid, $status, $hosts, $crypted) = ($entry =~ /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}) (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$/);
373     next unless defined ($uuid);
374     next if (defined $delete_uuids{$uuid});
375     my %hosts = map { $_ => 1 } split(/,/, $hosts);
376     next if ($hosts{$newsudo_hosts});
377     push @keepsudo, $entry;
378   };
379   if ($newsudo ne '') {
380     push @keepsudo, $newsudo;
381   }
382
383   &Util::LDAPUpdate($ldap, $editdn, 'postalAddress', $newstaddress);
384   &Util::LDAPUpdate($ldap, $editdn, 'l', $query->param('l'));
385   &Util::LDAPUpdate($ldap, $editdn, 'latitude', $lat);
386   &Util::LDAPUpdate($ldap, $editdn, 'longitude', $long);
387   &Util::LDAPUpdate($ldap, $editdn, 'c', $query->param('country'));
388   &Util::LDAPUpdate($ldap, $editdn, 'postalCode', $query->param('postalcode'));
389   &Util::LDAPUpdate($ldap, $editdn, 'telephoneNumber', $query->param('telephonenumber'));
390   &Util::LDAPUpdate($ldap, $editdn, 'facsimileTelephoneNumber', $query->param('facsimiletelephonenumber'));
391   &Util::LDAPUpdate($ldap, $editdn, 'bATVToken', $query->param('bATVToken'));
392   &Util::LDAPUpdate($ldap, $editdn, 'VoIP', $query->param('VoIP'));
393   &Util::LDAPUpdate($ldap, $editdn, 'loginShell', $query->param('loginshell'));
394   &Util::LDAPUpdate($ldap, $editdn, 'emailForward', $query->param('email'));
395   &Util::LDAPUpdate($ldap, $editdn, 'privateSub', $query->param('privatesub'));
396   &Util::LDAPUpdate($ldap, $editdn, 'ircNick', $query->param('ircnick'));
397   &Util::LDAPUpdate($ldap, $editdn, 'icqUin', $query->param('icquin'));
398   &Util::LDAPUpdate($ldap, $editdn, 'jabberJID', $query->param('jabberjid'));
399   &Util::LDAPUpdate($ldap, $editdn, 'labeledURI', $query->param('labeleduri'));
400   &Util::LDAPUpdate($ldap, $editdn, 'onVacation', $query->param('onvacation'));
401   &Util::LDAPUpdate($ldap, $editdn, 'gender', $gender);
402   &Util::LDAPUpdate($ldap, $editdn, 'birthDate', $query->param('birthdate')) if $bd_ok;
403   &Util::LDAPUpdate($ldap, $editdn, 'mailDisableMessage', $query->param('maildisablemessage'));
404   &Util::LDAPUpdate($ldap, $editdn, 'mailCallout', $callout);
405   &Util::LDAPUpdate($ldap, $editdn, 'mailContentInspectionAction', $mailcontentinspection);
406   &Util::LDAPUpdate($ldap, $editdn, 'mailGreylisting', $greylisting);
407   &Util::LDAPUpdate($ldap, $editdn, 'mailDefaultOptions', $defaultoptions);
408   &Util::LDAPUpdate($ldap, $editdn, 'sudoPassword', \@keepsudo);
409
410   # when we are done, reload the page with the updated details.
411   my $url = "https://$ENV{SERVER_NAME}/$config{webupdateurl}?id=$id;authtoken=$authtoken";
412   print "Location: $url\n\n";  
413 }
414
415 $ldap->unbind;