osCommerce Online Merchant v2.3.2 is a security improvement release which improves the customer password forgotten routine and generation of random strings.
Previously, the customer password forgotten routine would automatically generate a random password and e-mail it to the customer. The code was based on tep_create_random_value(), the PHP mt_rand() function, and a weak seeding of the random number generator. Now, tep_create_random_value() uses Phpass' stronger get_random_bytes() method to generate random strings, and the customer password forgotten routine e-mails a personal link to the customer and gives them 24 hours to change their password. If they do not, they can continue to use their existing password and their personal password reset link is discarded.
The customer password forgotten routine is also now protected with a new Customer Password Reset Action Recorder module which, by default, limits the generation of personal password reset links to once every 5 minutes.
The following database queries must be performed:
alter table customers_info add password_reset_key char(40);
alter table customers_info add password_reset_date datetime;
In addition, the Customer Password Reset Action Recorder module must be enabled as described below.
Files that have been modified in this release include:
Modified Files |
---|
admin/includes/classes/passwordhash.php |
admin/includes/functions/general.php |
includes/classes/passwordhash.php |
includes/filenames.php |
includes/functions/general.php |
includes/languages/english/password_forgotten.php |
includes/version.php |
login.php |
password_forgotten.php |
Files that have been added to this release include:
New Files |
---|
includes/languages/english/modules/action_recorder/ar_reset_password.php |
includes/languages/english/password_reset.php |
includes/modules/action_recorder/ar_reset_password.php |
password_reset.php |
The following API changes have been applied:
Title | Description |
---|---|
tep_create_random_value() | Now returns a mixed string if the $type parameter is not mixed, chars, or digits. Previously a boolean false value was returned. |
The following languages definitions have been modified:
Language File | Definitions | Status |
---|---|---|
modules/action_recorder/ar_reset_password.php |
MODULE_ACTION_RECORDER_RESET_PASSWORD_TITLE MODULE_ACTION_RECORDER_RESET_PASSWORD_DESCRIPTION |
New |
password_forgotten.php |
TEXT_MAIN TEXT_PASSWORD_RESET_INITIATED EMAIL_PASSWORD_REMINDER_SUBJECT EMAIL_PASSWORD_REMINDER_BODY EMAIL_PASSWORD_RESET_SUBJECT EMAIL_PASSWORD_RESET_BODY SUCCESS_PASSWORD_SENT ERROR_ACTION_RECORDER |
Changed New Deleted Deleted New New Deleted New |
password_reset.php |
NAVBAR_TITLE_1 NAVBAR_TITLE_2 HEADING_TITLE TEXT_MAIN TEXT_NO_RESET_LINK_FOUND TEXT_NO_EMAIL_ADDRESS_FOUND SUCCESS_PASSWORD_RESET |
New New New New New New New |
admin/includes/classes/passwordhash.php
includes/classes/passwordhash.php
Replace, in both files, the following class method:
function get_random_bytes($count)
{
$output = '';
if (@is_readable('/dev/urandom') &&
($fh = @fopen('/dev/urandom', 'rb'))) {
$output = fread($fh, $count);
fclose($fh);
}
if (strlen($output) < $count) {
$output = '';
for ($i = 0; $i < $count; $i += 16) {
$this->random_state =
md5(microtime() . $this->random_state);
$output .=
pack('H*', md5($this->random_state));
}
$output = substr($output, 0, $count);
}
return $output;
}
with:
function get_random_bytes($count)
{
$output = '';
if (@is_readable('/dev/urandom') &&
($fh = @fopen('/dev/urandom', 'rb'))) {
if (function_exists('stream_set_read_buffer')) {
stream_set_read_buffer($fh, 0);
}
$output = fread($fh, $count);
fclose($fh);
} elseif ( function_exists('openssl_random_pseudo_bytes') ) {
$output = openssl_random_pseudo_bytes($count, $orpb_secure);
if ( $orpb_secure != true ) {
$output = '';
}
} elseif (defined('MCRYPT_DEV_URANDOM')) {
$output = mcrypt_create_iv($count, MCRYPT_DEV_URANDOM);
}
if (strlen($output) < $count) {
$output = '';
for ($i = 0; $i < $count; $i += 16) {
$this->random_state =
md5(microtime() . $this->random_state);
$output .=
pack('H*', md5($this->random_state));
}
$output = substr($output, 0, $count);
}
return $output;
}
admin/includes/functions/general.php
Replace the following function:
function tep_rand($min = null, $max = null) {
static $seeded;
if (!$seeded) {
mt_srand((double)microtime()*1000000);
$seeded = true;
}
if (isset($min) && isset($max)) {
if ($min >= $max) {
return $min;
} else {
return mt_rand($min, $max);
}
} else {
return mt_rand();
}
}
with:
function tep_rand($min = null, $max = null) {
static $seeded;
if (!isset($seeded)) {
$seeded = true;
if ( (PHP_VERSION < '4.2.0') ) {
mt_srand((double)microtime()*1000000);
}
}
if (isset($min) && isset($max)) {
if ($min >= $max) {
return $min;
} else {
return mt_rand($min, $max);
}
} else {
return mt_rand();
}
}
includes/filenames.php
Add the following filename definition:
define('FILENAME_PASSWORD_RESET', 'password_reset.php');
includes/functions/general.php
Replace the following function:
function tep_create_random_value($length, $type = 'mixed') {
if ( ($type != 'mixed') && ($type != 'chars') && ($type != 'digits')) return false;
$rand_value = '';
while (strlen($rand_value) < $length) {
if ($type == 'digits') {
$char = tep_rand(0,9);
} else {
$char = chr(tep_rand(0,255));
}
if ($type == 'mixed') {
if (preg_match('/^[a-z0-9]$/i', $char)) $rand_value .= $char;
} elseif ($type == 'chars') {
if (preg_match('/^[a-z]$/i', $char)) $rand_value .= $char;
} elseif ($type == 'digits') {
if (preg_match('/^[0-9]$/i', $char)) $rand_value .= $char;
}
}
return $rand_value;
}
with:
function tep_create_random_value($length, $type = 'mixed') {
if ( ($type != 'mixed') && ($type != 'chars') && ($type != 'digits')) $type = 'mixed';
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$digits = '0123456789';
$base = '';
if ( ($type == 'mixed') || ($type == 'chars') ) {
$base .= $chars;
}
if ( ($type == 'mixed') || ($type == 'digits') ) {
$base .= $digits;
}
$value = '';
if (!class_exists('PasswordHash')) {
include(DIR_WS_CLASSES . 'passwordhash.php');
}
$hasher = new PasswordHash(10, true);
do {
$random = base64_encode($hasher->get_random_bytes($length));
for ($i = 0, $n = strlen($random); $i < $n; $i++) {
$char = substr($random, $i, 1);
if ( strpos($base, $char) !== false ) {
$value .= $char;
}
}
} while ( strlen($value) < $length );
if ( strlen($value) > $length ) {
$value = substr($value, 0, $length);
}
return $value;
}
Replace the following function:
function tep_rand($min = null, $max = null) {
static $seeded;
if (!isset($seeded)) {
mt_srand((double)microtime()*1000000);
$seeded = true;
}
if (isset($min) && isset($max)) {
if ($min >= $max) {
return $min;
} else {
return mt_rand($min, $max);
}
} else {
return mt_rand();
}
}
with:
function tep_rand($min = null, $max = null) {
static $seeded;
if (!isset($seeded)) {
$seeded = true;
if ( (PHP_VERSION < '4.2.0') ) {
mt_srand((double)microtime()*1000000);
}
}
if (isset($min) && isset($max)) {
if ($min >= $max) {
return $min;
} else {
return mt_rand($min, $max);
}
} else {
return mt_rand();
}
}
includes/languages/english/modules/action_recorder/ar_reset_password.php
Copy new file to destination.
includes/languages/english/password_forgotten.php
Replace the definition of TEXT_MAIN:
define('TEXT_MAIN', 'If you\'ve forgotten your password, enter your e-mail address below and we\'ll send you an e-mail message containing your new password.');
with:
define('TEXT_MAIN', 'If you\'ve forgotten your password, enter your e-mail address below and we\'ll send you instructions on how to securely change your password.');
Delete the following definitions:
define('EMAIL_PASSWORD_REMINDER_SUBJECT', STORE_NAME . ' - New Password');
define('EMAIL_PASSWORD_REMINDER_BODY', 'A new password was requested from ' . tep_get_ip_address() . '.' . "\n\n" . 'Your new password to \'' . STORE_NAME . '\' is:' . "\n\n" . ' %s' . "\n\n");
define('SUCCESS_PASSWORD_SENT', 'Success: A new password has been sent to your e-mail address.');
Add the following definitions:
define('TEXT_PASSWORD_RESET_INITIATED', 'Please check your e-mail for instructions on how to change your password. The instructions contain a link that is valid only for 24 hours or until your password has been updated.');
define('EMAIL_PASSWORD_RESET_SUBJECT', STORE_NAME . ' - New Password');
define('EMAIL_PASSWORD_RESET_BODY', 'A new password has been requested for your account at ' . STORE_NAME . '.' . "\n\n" . 'Please follow this personal link to securely change your password:' . "\n\n" . '%s' . "\n\n" . 'This link will be automatically discarded after 24 hours or after your password has been changed.' . "\n\n" . 'For help with any of our online services, please email the store-owner: ' . STORE_OWNER_EMAIL_ADDRESS . '.' . "\n\n");
define('ERROR_ACTION_RECORDER', 'Error: A password reset link has already been sent. Please try again in %s minutes.');
includes/languages/english/password_reset.php
Copy new file to destination.
includes/modules/action_recorder/ar_reset_password.php
Copy new file to destination.
includes/version.php
Replace the content:
2.3.1
with:
2.3.2
login.php
Replace the following line:
tep_db_query("update " . TABLE_CUSTOMERS_INFO . " set customers_info_date_of_last_logon = now(), customers_info_number_of_logons = customers_info_number_of_logons+1 where customers_info_id = '" . (int)$customer_id . "'");
with:
tep_db_query("update " . TABLE_CUSTOMERS_INFO . " set customers_info_date_of_last_logon = now(), customers_info_number_of_logons = customers_info_number_of_logons+1, password_reset_key = null, password_reset_date = null where customers_info_id = '" . (int)$customer_id . "'");
password_forgotten.php
Replace the following content:
if (isset($HTTP_GET_VARS['action']) && ($HTTP_GET_VARS['action'] == 'process') && isset($HTTP_POST_VARS['formid']) && ($HTTP_POST_VARS['formid'] == $sessiontoken)) {
$email_address = tep_db_prepare_input($HTTP_POST_VARS['email_address']);
$check_customer_query = tep_db_query("select customers_firstname, customers_lastname, customers_password, customers_id from " . TABLE_CUSTOMERS . " where customers_email_address = '" . tep_db_input($email_address) . "'");
if (tep_db_num_rows($check_customer_query)) {
$check_customer = tep_db_fetch_array($check_customer_query);
$new_password = tep_create_random_value(ENTRY_PASSWORD_MIN_LENGTH);
$crypted_password = tep_encrypt_password($new_password);
tep_db_query("update " . TABLE_CUSTOMERS . " set customers_password = '" . tep_db_input($crypted_password) . "' where customers_id = '" . (int)$check_customer['customers_id'] . "'");
tep_mail($check_customer['customers_firstname'] . ' ' . $check_customer['customers_lastname'], $email_address, EMAIL_PASSWORD_REMINDER_SUBJECT, sprintf(EMAIL_PASSWORD_REMINDER_BODY, $new_password), STORE_OWNER, STORE_OWNER_EMAIL_ADDRESS);
$messageStack->add_session('login', SUCCESS_PASSWORD_SENT, 'success');
tep_redirect(tep_href_link(FILENAME_LOGIN, '', 'SSL'));
} else {
$messageStack->add('password_forgotten', TEXT_NO_EMAIL_ADDRESS_FOUND);
}
}
with:
$password_reset_initiated = false;
if (isset($HTTP_GET_VARS['action']) && ($HTTP_GET_VARS['action'] == 'process') && isset($HTTP_POST_VARS['formid']) && ($HTTP_POST_VARS['formid'] == $sessiontoken)) {
$email_address = tep_db_prepare_input($HTTP_POST_VARS['email_address']);
$check_customer_query = tep_db_query("select customers_firstname, customers_lastname, customers_id from " . TABLE_CUSTOMERS . " where customers_email_address = '" . tep_db_input($email_address) . "'");
if (tep_db_num_rows($check_customer_query)) {
$check_customer = tep_db_fetch_array($check_customer_query);
$actionRecorder = new actionRecorder('ar_reset_password', $check_customer['customers_id'], $email_address);
if ($actionRecorder->canPerform()) {
$actionRecorder->record();
$reset_key = tep_create_random_value(40);
tep_db_query("update " . TABLE_CUSTOMERS_INFO . " set password_reset_key = '" . tep_db_input($reset_key) . "', password_reset_date = now() where customers_info_id = '" . (int)$check_customer['customers_id'] . "'");
$reset_key_url = tep_href_link(FILENAME_PASSWORD_RESET, 'account=' . urlencode($email_address) . '&key=' . $reset_key, 'SSL', false);
if ( strpos($reset_key_url, '&') !== false ) {
$reset_key_url = str_replace('&', '&', $reset_key_url);
}
tep_mail($check_customer['customers_firstname'] . ' ' . $check_customer['customers_lastname'], $email_address, EMAIL_PASSWORD_RESET_SUBJECT, sprintf(EMAIL_PASSWORD_RESET_BODY, $reset_key_url), STORE_OWNER, STORE_OWNER_EMAIL_ADDRESS);
$password_reset_initiated = true;
} else {
$actionRecorder->record(false);
$messageStack->add('password_forgotten', sprintf(ERROR_ACTION_RECORDER, (defined('MODULE_ACTION_RECORDER_RESET_PASSWORD_MINUTES') ? (int)MODULE_ACTION_RECORDER_RESET_PASSWORD_MINUTES : 5)));
}
} else {
$messageStack->add('password_forgotten', TEXT_NO_EMAIL_ADDRESS_FOUND);
}
}
Replace the following code:
<?php echo tep_draw_form('password_forgotten', tep_href_link(FILENAME_PASSWORD_FORGOTTEN, 'action=process', 'SSL'), 'post', '', true); ?>
<div class="contentContainer">
<div class="contentText">
<div><?php echo TEXT_MAIN; ?></div>
<table border="0" width="100%" cellspacing="0" cellpadding="2">
<tr>
<td class="fieldKey"><?php echo ENTRY_EMAIL_ADDRESS; ?></td>
<td class="fieldValue"><?php echo tep_draw_input_field('email_address'); ?></td>
</tr>
</table>
</div>
<div class="buttonSet">
<span class="buttonAction"><?php echo tep_draw_button(IMAGE_BUTTON_CONTINUE, 'triangle-1-e', null, 'primary'); ?></span>
<?php echo tep_draw_button(IMAGE_BUTTON_BACK, 'triangle-1-w', tep_href_link(FILENAME_LOGIN, '', 'SSL')); ?>
</div>
</div>
</form>
with:
<?php
if ($password_reset_initiated == true) {
?>
<div class="contentContainer">
<div class="contentText">
<?php echo TEXT_PASSWORD_RESET_INITIATED; ?>
</div>
</div>
<?php
} else {
?>
<?php echo tep_draw_form('password_forgotten', tep_href_link(FILENAME_PASSWORD_FORGOTTEN, 'action=process', 'SSL'), 'post', '', true); ?>
<div class="contentContainer">
<div class="contentText">
<div><?php echo TEXT_MAIN; ?></div>
<table border="0" width="100%" cellspacing="0" cellpadding="2">
<tr>
<td class="fieldKey"><?php echo ENTRY_EMAIL_ADDRESS; ?></td>
<td class="fieldValue"><?php echo tep_draw_input_field('email_address'); ?></td>
</tr>
</table>
</div>
<div class="buttonSet">
<span class="buttonAction"><?php echo tep_draw_button(IMAGE_BUTTON_CONTINUE, 'triangle-1-e', null, 'primary'); ?></span>
<?php echo tep_draw_button(IMAGE_BUTTON_BACK, 'triangle-1-w', tep_href_link(FILENAME_LOGIN, '', 'SSL')); ?>
</div>
</div>
</form>
<?php
}
?>
password_reset.php
Copy new file to destination.
The Customer Password Reset Action Recorder module must be enabled at the following page:
Administration Tool -> Modules -> Action Recorder -> Install Module -> Customer Password Reset -> Install Module
A full list of source code changes can be seen at:
https://github.com/osCommerce/oscommerce2/compare/v2.3.1...upgrade232
We'd like to thank Gary Burton and George Zarkadas for testing and reviewing this upgrade guide, and George Argyros and Aggelos Kiayias for bringing the issue of insecure random number generators to our attention.
George and Aggelos are presenting a talk about insecure random number generators at Black Hat USA on July 25th 2012.
Stefan Esser, an independent Security Consultant, has published an article on insecure random number generators.