|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2017-11-07 11:13 UTC] dmitry dot yeskin at gmail dot com
Description:
------------
Hello!
We have an e-commerce website written with Yii2 framework.
We don't modify session ids ourself. We just use session_start();
So here is what happened: we have a good traffic like 12000 unique users in 3 hrs of mornings every day. And for last 2 weeks - we got a session id collision 2 times. I mean - 2 customers came to website exactly at the same second and got shared session id.
In result, they were able to see content of each other in their cart, until they started removing that content and even made a checkout, and then complained that ordered wrong item.
Here is data from nginx log:
--------
[ec2-user@scw ~]$ grep -r "73.111.163.135\|73.19.225.170" log.log
73.111.163.135 - - [26/Oct/2017:18:43:52 -0600] "GET /postcard?r=26oct2017 HTTP/2.0" 200
73.19.225.170 - - [26/Oct/2017:18:43:52 -0600] "GET /postcard?r=26oct2017 HTTP/2.0" 200
73.111.163.135 - - [26/Oct/2017:18:47:09 -0600] "POST /postcard?r=26oct2017 HTTP/2.0" 30
73.111.163.135 - - [26/Oct/2017:18:47:09 -0600] "GET /postcard?r=26oct2017 HTTP/2.0" 200
73.111.163.135 - - [26/Oct/2017:18:47:12 -0600] "GET /cart HTTP/2.0" 200
73.111.163.135 - - [26/Oct/2017:18:47:42 -0600] "POST /cart HTTP/2.0" 200
73.111.163.135 - - [26/Oct/2017:18:48:48 -0600] "GET /cart/checkout/ HTTP/2.0" 200
73.19.225.170 - - [26/Oct/2017:18:50:39 -0600] "POST /postcard?r=26oct2017 HTTP/2.0" 302
73.19.225.170 - - [26/Oct/2017:18:50:39 -0600] "GET /postcard?r=26oct2017 HTTP/2.0" 200
73.19.225.170 - - [26/Oct/2017:18:50:46 -0600] "GET /cart HTTP/2.0" 200
73.19.225.170 - - [26/Oct/2017:18:51:00 -0600] "GET /cart/remove/1 HTTP/2.0" 302
73.19.225.170 - - [26/Oct/2017:18:51:00 -0600] "GET /cart HTTP/2.0" 200
73.111.163.135 - - [26/Oct/2017:18:51:09 -0600] "POST /cart/checkout/ HTTP/2.0" 302
73.111.163.135 - - [26/Oct/2017:18:51:09 -0600] "GET /cart/checkout/complete HTTP/2.0" 200
73.19.225.170 - - [26/Oct/2017:18:51:17 -0600] "GET /cart/checkout/ HTTP/2.0" 200
------------
You may see that both customers came to website at the same moment to postcard page (then they clicked add to cart and proceeded to cart, and then one of users saw wrong item there and called /cart/remove/1, then another user completed checkout, but he bought a wrong item and then complained to us).
For 2 weeks we got 2 similar cases.
After the first case we changed a hash entropy php settings in php.ini so session_id is now 48 chars but it didn't help to prevent second case.
We solved this issue by adding user_agent to session var on controller init:
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_agent']) || empty($_SESSION['user_agent']) || $_SESSION['user_agent'] === Yii::$app->request->getUserAgent()) {
} else {
session_regenerate_id();
unset($_SESSION['user_agent']);
unset($_SESSION['cart']);
}
if (!isset($_SESSION['user_agent'])) {
$_SESSION['user_agent'] = Yii::$app->request->getUserAgent();
}
But still wondered why this happens? I read in github php source code that php must use IP address and random value for session_id generation so it must be unique and i also read that php has some mechanisms to prevent session_id collisions. But looks like it doesn't work correctly.
Again - everything great for thousands users. It only happened twice for last 2 weeks.
We dont modify session_id value ourself. We just use session_start and work with $_SESSION variables after that. What are we doing wrong?
Is it a bug?
We have PHP 7.0.21 working as php-fpm and nginx
PS. Doing PHP coding for 20 years, and see this for the first time.
Test script:
---------------
/**
* Process adding line-item to cart
*
* @return boolean the status of addition
*/
public function addToCart() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['cart'])) {
//$_SESSION['cart'] = array('items' => array(), 'coupon' => array(), 'subtotal' => '', 'discount' => '', 'shipping' => '', 'total' => '');
$_SESSION['cart'] = array('items' => array(), 'coupon' => array(), 'cost' => '', 'discount' => '', 'shipping' => '', 'total' => '');
}
$cost = ($this->price + 1 - 1); // Converting string to number
if (is_array($this->addons) && count($this->addons)>0) {
foreach ($this->addons as $addon) {
if (isset($addon['price']) && is_numeric($addon['price']) && !empty($addon['price'] + 1 - 1)) $cost += $addon['price'];
}
}
$shippingCost = 0;
if ($this->type == 'package' || $this->type == 'postcard' || $this->type == 'book') {
$data = $this->data;
if (isset($data['shipping']) && !empty($data['shipping']) && isset($data[$data['shipping']]) && is_numeric($data[$data['shipping']]) && !empty($data[$data['shipping']] + 1 - 1)) $shippingCost = $data[$data['shipping']];
}
$_SESSION['cart']['items'][] = array(
'id' => $this->id,
'product_id' => $this->product_id,
'sku' => $this->sku,
'title' => $this->title,
'type' => $this->type,
'description' => $this->description,
'price' => number_format((float)$this->price, 2, '.', ''),
'picture' => $this->picture,
'data' => $this->data,
'address' => $this->address,
'addons' => $this->addons,
'cost' => number_format((float)$cost, 2, '.', ''),
'shipping' => number_format((float)$shippingCost, 2, '.', ''),
'subtotal' => number_format((float)($cost + $shippingCost), 2, '.', ''),
'draft' => $this->draft,
);
$cost = 0;
$shipping = 0;
foreach ($_SESSION['cart']['items'] as $key => $item) {
$cost += $item['cost'];
$shipping += $item['shipping'];
}
$discount = (isset($_SESSION['cart']['discount']) && !empty($_SESSION['cart']['discount'])) ? $_SESSION['cart']['discount'] : 0;
$_SESSION['cart']['cost'] = number_format((float)$cost, 2, '.', '');
$_SESSION['cart']['shipping'] = number_format((float)$shipping, 2, '.', '');
$_SESSION['cart']['total'] = number_format((float)($cost + $shipping - $discount), 2, '.', '');
return true;
}
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Mon Oct 27 01:00:02 2025 UTC |
Thanks for comment nikic! It was a good idea but unfortunately looks like thats not a case. I tried this code to verify it: $fp = fopen('/dev/urandom','rb'); if ($fp !== FALSE) { $result = fread($fp, 100); fclose($fp); print $result; } else { die('Can not open /dev/urandom.'); } And I got random string in result. But few minutes ago I got one more collision! So 2 collisions only today!!! Yes traffic is not small - already 430 successful orders this morning. Only 2 collisions. But anyway - thats not normal! And I have all day today session.use_strict_mode enabled after Yasuo Ohgaki comment! Please help.In my controller in init() method I added this code for tracking: if (session_status() == PHP_SESSION_NONE) { session_start(); } if (isset($_SESSION['user_agent']) && !empty($_SESSION['user_agent']) && $_SESSION['user_agent'] != Yii::$app->request->getUserAgent()) { $data = 'Session ID: ' . session_id() . "\n" . 'User Agent: ' . Yii::$app->request->getUserAgent() . "\n" . 'IP: ' . Yii::$app->request->getUserIP() . "\n" . 'SESSION: ' . print_r($_SESSION, true); @mail('[MY EMAIL HERE]', 'Session ID Collision', $data); session_regenerate_id(); session_destroy(); session_start(); session_regenerate_id(); unset($_SESSION['user_agent']); $_SESSION['cart'] = array(); unset($_SESSION['cart']); } if (!isset($_SESSION['user_agent'])) { $_SESSION['user_agent'] = Yii::$app->request->getUserAgent(); $_SESSION['ip_address'] = Yii::$app->request->getUserIP(); } So it works like: if you dont have user_agent in your session - it adds it there. And if you have session started but your user_agent is not like one saved in your session (that is what happens when session shared), it send me email with data about "new session owner" and content of session from previous owner (his user_agent and IP) and trying to regenerate session (yes, I do it now 2 times + session_destroy in the middle to more guarantee). So I got today already 5 notifications about session collisions. Here is just example from one of emails: ------- Session ID: Y4gXw2l8fZZRHVgu0KoLamr5ywVyw5UauP0BOKdjpg0 User Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0_3 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Mobile/15A432 [FBAN/FBIOS;FBAV/147.0.0.46.81;FBBV/76961488;FBDV/iPhone9,2;FBMD/iPhone;FBSN/iOS;FBSV/11.0.3;FBSS/3;FBCR/Verizon;FBID/phone;FBLC/en_US;FBOP/5;FBRV/0] IP: 174.200.9.242 SESSION: Array ( [user_agent] => Mozilla/5.0 (Linux; Android 7.0; SM-N920T Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/148.0.0.51.62;] [ip_address] => 172.58.110.205 [__flash] => Array ( ) ) ------ So as described above in code, new customer with IP 174.200.9.242 assigned already existing session ID Y4gXw2l8fZZRHVgu0KoLamr5ywVyw5UauP0BOKdjpg0 which is owned and initiated by customer with IP 172.58.110.205. This can be confirmed with server nginx logs. That already happened 5 times today. I didn't track for collisions before and just had complaints from customers, but I believe that in the past days we had a much more collisions than we think. Right now, I can track every single collision but not sure what to do with this and what is the reason of it. Maybe some RAM memory overload? Maybe CPU overload? Maybe because PHP is working as php-fpm? What can I do more to help identify this issue?