source: trunk/password.php @ 30353

Last change on this file since 30353 was 29111, checked in by plg, 10 years ago

bug 3050: increase security on reset password algorithm.

  • reset key has a 1-hour life
  • reset key is automatically deleted once used
  • reset key is stored as a hash

Thank you effigies for code suggestions

  • Property svn:eol-style set to LF
File size: 11.0 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2014 Piwigo Team                  http://piwigo.org |
6// | Copyright(C) 2003-2008 PhpWebGallery Team    http://phpwebgallery.net |
7// | Copyright(C) 2002-2003 Pierrick LE GALL   http://le-gall.net/pierrick |
8// +-----------------------------------------------------------------------+
9// | This program is free software; you can redistribute it and/or modify  |
10// | it under the terms of the GNU General Public License as published by  |
11// | the Free Software Foundation                                          |
12// |                                                                       |
13// | This program is distributed in the hope that it will be useful, but   |
14// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
15// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
16// | General Public License for more details.                              |
17// |                                                                       |
18// | You should have received a copy of the GNU General Public License     |
19// | along with this program; if not, write to the Free Software           |
20// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
21// | USA.                                                                  |
22// +-----------------------------------------------------------------------+
23
24// +-----------------------------------------------------------------------+
25// |                           initialization                              |
26// +-----------------------------------------------------------------------+
27
28define('PHPWG_ROOT_PATH','./');
29include_once( PHPWG_ROOT_PATH.'include/common.inc.php' );
30include_once(PHPWG_ROOT_PATH.'include/functions_mail.inc.php');
31
32// +-----------------------------------------------------------------------+
33// | Check Access and exit when user status is not ok                      |
34// +-----------------------------------------------------------------------+
35
36check_status(ACCESS_FREE);
37
38trigger_notify('loc_begin_password');
39
40// +-----------------------------------------------------------------------+
41// | Functions                                                             |
42// +-----------------------------------------------------------------------+
43
44/**
45 * checks the validity of input parameters, fills $page['errors'] and
46 * $page['infos'] and send an email with confirmation link
47 *
48 * @return bool (true if email was sent, false otherwise)
49 */
50function process_password_request()
51{
52  global $page, $conf;
53 
54  if (empty($_POST['username_or_email']))
55  {
56    $page['errors'][] = l10n('Invalid username or email');
57    return false;
58  }
59 
60  $user_id = get_userid_by_email($_POST['username_or_email']);
61   
62  if (!is_numeric($user_id))
63  {
64    $user_id = get_userid($_POST['username_or_email']);
65  }
66
67  if (!is_numeric($user_id))
68  {
69    $page['errors'][] = l10n('Invalid username or email');
70    return false;
71  }
72
73  $userdata = getuserdata($user_id, false);
74
75  // password request is not possible for guest/generic users
76  $status = $userdata['status'];
77  if (is_a_guest($status) or is_generic($status))
78  {
79    $page['errors'][] = l10n('Password reset is not allowed for this user');
80    return false;
81  }
82
83  if (empty($userdata['email']))
84  {
85    $page['errors'][] = l10n(
86      'User "%s" has no email address, password reset is not possible',
87      $userdata['username']
88      );
89    return false;
90  }
91
92  $activation_key = generate_key(20);
93
94  list($expire) = pwg_db_fetch_row(pwg_query('SELECT ADDDATE(NOW(), INTERVAL 1 HOUR)'));
95
96  single_update(
97    USER_INFOS_TABLE,
98    array(
99      'activation_key' => pwg_password_hash($activation_key),
100      'activation_key_expire' => $expire,
101      ),
102    array('user_id' => $user_id)
103    );
104 
105  $userdata['activation_key'] = $activation_key;
106
107  set_make_full_url();
108 
109  $message = l10n('Someone requested that the password be reset for the following user account:') . "\r\n\r\n";
110  $message.= l10n(
111    'Username "%s" on gallery %s',
112    $userdata['username'],
113    get_gallery_home_url()
114    );
115  $message.= "\r\n\r\n";
116  $message.= l10n('To reset your password, visit the following address:') . "\r\n";
117  $message.= get_gallery_home_url().'/password.php?key='.$activation_key.'-'.urlencode($userdata['email']);
118  $message.= "\r\n\r\n";
119  $message.= l10n('If this was a mistake, just ignore this email and nothing will happen.')."\r\n";
120
121  unset_make_full_url();
122
123  $message = trigger_change('render_lost_password_mail_content', $message);
124
125  $email_params = array(
126    'subject' => '['.$conf['gallery_title'].'] '.l10n('Password Reset'),
127    'content' => $message,
128    'email_format' => 'text/plain',
129    );
130
131  if (pwg_mail($userdata['email'], $email_params))
132  {
133    $page['infos'][] = l10n('Check your email for the confirmation link');
134    return true;
135  }
136  else
137  {
138    $page['errors'][] = l10n('Error sending email');
139    return false;
140  }
141}
142
143/**
144 *  checks the activation key: does it match the expected pattern? is it
145 *  linked to a user? is this user allowed to reset his password?
146 *
147 * @return mixed (user_id if OK, false otherwise)
148 */
149function check_password_reset_key($reset_key)
150{
151  global $page, $conf;
152
153  list($key, $email) = explode('-', $reset_key, 2);
154
155  if (!preg_match('/^[a-z0-9]{20}$/i', $key))
156  {
157    $page['errors'][] = l10n('Invalid key');
158    return false;
159  }
160
161  $user_ids = array();
162 
163  $query = '
164SELECT
165  '.$conf['user_fields']['id'].' AS id
166  FROM '.USERS_TABLE.'
167  WHERE '.$conf['user_fields']['email'].' = \''.pwg_db_real_escape_string($email).'\'
168;';
169  $user_ids = query2array($query, null, 'id');
170
171  if (count($user_ids) == 0)
172  {
173    $page['errors'][] = l10n('Invalid username or email');
174    return false;
175  }
176
177  $user_id = null;
178 
179  $query = '
180SELECT
181    user_id,
182    status,
183    activation_key,
184    activation_key_expire,
185    NOW() AS dbnow
186  FROM '.USER_INFOS_TABLE.'
187  WHERE user_id IN ('.implode(',', $user_ids).')
188;';
189  $result = pwg_query($query);
190  while ($row = pwg_db_fetch_assoc($result))
191  {
192    if (pwg_password_verify($key, $row['activation_key']))
193    {
194      if (strtotime($row['dbnow']) > strtotime($row['activation_key_expire']))
195      {
196        // key has expired
197        $page['errors'][] = l10n('Invalid key');
198        return false;
199      }
200
201      if (is_a_guest($row['status']) or is_generic($row['status']))
202      {
203        $page['errors'][] = l10n('Password reset is not allowed for this user');
204        return false;
205      }
206
207      $user_id = $row['user_id'];
208    }
209  }
210
211  if (empty($user_id))
212  {
213    $page['errors'][] = l10n('Invalid key');
214    return false;
215  }
216 
217  return $user_id;
218}
219
220/**
221 * checks the passwords, checks that user is allowed to reset his password,
222 * update password, fills $page['errors'] and $page['infos'].
223 *
224 * @return bool (true if password was reset, false otherwise)
225 */
226function reset_password()
227{
228  global $page, $conf;
229
230  if ($_POST['use_new_pwd'] != $_POST['passwordConf'])
231  {
232    $page['errors'][] = l10n('The passwords do not match');
233    return false;
234  }
235
236  if (!isset($_GET['key']))
237  {
238    $page['errors'][] = l10n('Invalid key');
239  }
240 
241  $user_id = check_password_reset_key($_GET['key']);
242 
243  if (!is_numeric($user_id))
244  {
245    return false;
246  }
247   
248  single_update(
249    USERS_TABLE,
250    array($conf['user_fields']['password'] => $conf['password_hash']($_POST['use_new_pwd'])),
251    array($conf['user_fields']['id'] => $user_id)
252    );
253
254  single_update(
255    USER_INFOS_TABLE,
256    array(
257      'activation_key' => null,
258      'activation_key_expire' => null,
259      ),
260    array('user_id' => $user_id)
261    );
262
263  $page['infos'][] = l10n('Your password has been reset');
264  $page['infos'][] = '<a href="'.get_root_url().'identification.php">'.l10n('Login').'</a>';
265
266  return true;
267}
268
269// +-----------------------------------------------------------------------+
270// | Process form                                                          |
271// +-----------------------------------------------------------------------+
272if (isset($_POST['submit']))
273{
274  check_pwg_token();
275 
276  if ('lost' == $_GET['action'])
277  {
278    if (process_password_request())
279    {
280      $page['action'] = 'none';
281    }
282  }
283
284  if ('reset' == $_GET['action'])
285  {
286    if (reset_password())
287    {
288      $page['action'] = 'none';
289    }
290  }
291}
292
293// +-----------------------------------------------------------------------+
294// | key and action                                                        |
295// +-----------------------------------------------------------------------+
296
297// a connected user can't reset the password from a mail
298if (isset($_GET['key']) and !is_a_guest())
299{
300  unset($_GET['key']);
301}
302
303if (isset($_GET['key']) and !isset($_POST['submit']))
304{
305  $user_id = check_password_reset_key($_GET['key']);
306  if (is_numeric($user_id))
307  {
308    $userdata = getuserdata($user_id, false);
309    $page['username'] = $userdata['username'];
310    $template->assign('key', $_GET['key']);
311
312    if (!isset($page['action']))
313    {
314      $page['action'] = 'reset';
315    }
316  }
317  else
318  {
319    $page['action'] = 'none';
320  }
321}
322
323if (!isset($page['action']))
324{
325  if (!isset($_GET['action']))
326  {
327    $page['action'] = 'lost';
328  }
329  elseif (in_array($_GET['action'], array('lost', 'reset', 'none')))
330  {
331    $page['action'] = $_GET['action'];
332  }
333}
334
335if ('reset' == $page['action'] and !isset($_GET['key']) and (is_a_guest() or is_generic()))
336{
337  redirect(get_gallery_home_url());
338}
339
340if ('lost' == $page['action'] and !is_a_guest())
341{
342  redirect(get_gallery_home_url());
343}
344
345// +-----------------------------------------------------------------------+
346// | template initialization                                               |
347// +-----------------------------------------------------------------------+
348
349$title = l10n('Password Reset');
350if ('lost' == $page['action'])
351{
352  $title = l10n('Forgot your password?');
353
354  if (isset($_POST['username_or_email']))
355  {
356    $template->assign('username_or_email', htmlspecialchars(stripslashes($_POST['username_or_email'])));
357  }
358}
359
360$page['body_id'] = 'thePasswordPage';
361
362$template->set_filenames(array('password'=>'password.tpl'));
363$template->assign(
364  array(
365    'title' => $title,
366    'form_action'=> get_root_url().'password.php',
367    'action' => $page['action'],
368    'username' => isset($page['username']) ? $page['username'] : $user['username'],
369    'PWG_TOKEN' => get_pwg_token(),
370    )
371  );
372
373
374// include menubar
375$themeconf = $template->get_template_vars('themeconf');
376if (!isset($themeconf['hide_menu_on']) OR !in_array('thePasswordPage', $themeconf['hide_menu_on']))
377{
378  include( PHPWG_ROOT_PATH.'include/menubar.inc.php');
379}
380
381// +-----------------------------------------------------------------------+
382// |                           html code display                           |
383// +-----------------------------------------------------------------------+
384
385include(PHPWG_ROOT_PATH.'include/page_header.php');
386trigger_notify('loc_end_password');
387flush_page_messages();
388$template->pparse('password');
389include(PHPWG_ROOT_PATH.'include/page_tail.php');
390
391?>
Note: See TracBrowser for help on using the repository browser.