Preconditions (*)
- Magento 2.4.3, with a lot of custom modules
- Session Cache Handler: Redis 6.2.4 (only Master)
- compression_threshold = '2048'
- compression_library = 'gzip'
- max_concurrency = '12'
- break_after_frontend = '5'
- break_after_adminhtml = '30'
- first_lifetime = '600'
- bot_first_lifetime = '60'
- bot_lifetime = '7200'
- disable_locking = '0'
- min_lifetime = '60'
- max_lifetime = '2592000'
- sentinel_master = ''
- sentinel_servers = ''
- sentinel_connect_retries = '5'
- sentinel_verify_master = '0'
- disabled varnish
- no sessions within local filesystem or database, all sessions are handled within REDIS (checked with redis-cli monitor)
- Session Validation is for every attribute true, but the system has empty values for x-forwarded-for, http-via and remote_addr, so the system only checks for User-Agent
- this is an architecture and configuration problem (PHP: variables_order), we will solve this today
Steps to reproduce (*)
- Currently not reproducible, we work on it, but maybe another user can relate and knows this situation and has some helpful informations
- Situations customers report seeing information from other customers:
- They see as logged in customer another customer name within the header (Welcome, Firstname Lastname).
- This Information is placed with knockout and CustomerData
<div class="text-center margin-bottom-15" data-bind="scope: 'customer'"><span class="text-small text-bold c-navyblue"><?= __('Login'); ?><br/><?= __('MyAccount'); ?></span><span class="c-denimblue"><?= __('logged in as '); ?><span data-bind="text: customer().fullname"></span></span></div>
- MyAccount Page shows another customer data
- this page is not cached -> first hint of a session hijacking situation
- Product Option Formular is prefilled with data of another customer
- we implemented a formular autofill feature with reload of CustomerData Section and a plugin which enhance the CustomerData
- look at Enhance CustomerData Plugin and Autofill JS
- the products have product options, these are autofilled with this feature
- this product was added to cart and also ordered
- the order was sent to the correct customer, also every other data within the order than the product options are correct
- => that gives the hint, that die hijacking of the session is a temporary event and the customer get his own session back
Conclusion
- i know it is a very specific problem and maybe not Magento2 core related
- we used standard functionality to enhance customer related data (plugin) and private content (CustomerData), which makes me think it could be Magento2 Core related.
- when we access customer session we only use SessionFactory, except at the plugin (CustomerData Plugin), there we use the CurrentCustomer Helper which uses the Session. Could this lead to the problem?
- the hijack is highly likely temporary (we are doing further checks actually)
Enhance CustomerData Plugin
di.xml
<type name="Magento\Customer\CustomerData\Customer">
<plugin name="Plugin_CustomerData" type="My\Core\Plugin\Customer\CustomerData" />
</type>
CustomerData.php
use Magento\Customer\Api\AddressRepositoryInterface;
use Magento\Customer\Helper\Session\CurrentCustomer;
use Magento\Directory\Model\CountryFactory;
class CustomerData
{
/**
* @var CurrentCustomer
*/
protected $currentCustomer;
/**
* @var AddressRepositoryInterface
*/
protected $addressRepository;
/**
* @var CountryFactory
*/
protected $countryFactory;
/**
* @var \Magento\Customer\Api\Data\AddressInterface
*/
private $billingAddress;
/**
* @var \Magento\Customer\Api\Data\AddressInterface
*/
private $shippingAddress;
/**
* CustomerData constructor.
* @param CurrentCustomer $currentCustomer
* @param AddressRepositoryInterface $addressRepository
* @param CountryFactory $countryFactory
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function __construct(
CurrentCustomer $currentCustomer,
AddressRepositoryInterface $addressRepository,
CountryFactory $countryFactory
) {
$this->currentCustomer = $currentCustomer;
$this->addressRepository = $addressRepository;
$this->countryFactory = $countryFactory;
if ($this->currentCustomer->getCustomerId()) {
$this->billingAddress = $this->addressRepository->getById($this->currentCustomer->getCustomer()->getDefaultBilling());
$this->shippingAddress = $this->addressRepository->getById($this->currentCustomer->getCustomer()->getDefaultShipping());
}
}
/**
* Add additional required attributes to customerData
*
* @param \Magento\Customer\CustomerData\Customer $subject
* @param $result
* @return mixed
*/
public function afterGetSectionData(\Magento\Customer\CustomerData\Customer $subject, $result)
{
if ($this->currentCustomer->getCustomerId()) {
$customer = $this->currentCustomer->getCustomer();
$result["prefix"] = __($customer->getPrefix());
$result["suffix"] = $customer->getSuffix();
$result["lastname"] = $customer->getLastname();
$result["pobox"] = $customer->getCustomAttribute('po_box') ? $customer->getCustomAttribute('po_box')->getValue() : '';
$result["email"] = $customer->getEmail();
$result["billingaddress"] = $this->getBillingAddress();
$result["shippingaddress"] = $this->getShippingAddress();
}
return $result;
}
/**
* @return array
*/
public function getBillingAddress()
{
$billingAddress["prefix"] = __($this->billingAddress->getPrefix());
$billingAddress["suffix"] = $this->billingAddress->getSuffix();
$billingAddress["firstname"] = $this->billingAddress->getFirstname();
$billingAddress["lastname"] = $this->billingAddress->getLastname();
$billingAddress["company"] = $this->billingAddress->getCompany();
$billingAddress["telephone"] = $this->billingAddress->getTelephone();
$billingAddress["street"] = implode(" ", $this->billingAddress->getStreet());
$billingAddress["postcode"] = $this->billingAddress->getPostcode();
$billingAddress["city"] = $this->shippingAddress->getCity();
$billingAddress["country"] = $this->getCountry($this->billingAddress->getCountryId());
return $billingAddress;
}
/**
* @return array
*/
public function getShippingAddress()
{
$shippingAddress = array();
$shippingAddress["prefix"] = __($this->shippingAddress->getPrefix());
$shippingAddress["suffix"] = $this->shippingAddress->getSuffix();
$shippingAddress["firstname"] = $this->shippingAddress->getFirstname();
$shippingAddress["lastname"] = $this->shippingAddress->getLastname();
$shippingAddress["company"] = $this->shippingAddress->getCompany();
$shippingAddress["telephone"] = $this->shippingAddress->getTelephone();
$shippingAddress["street"] = implode(" ", $this->shippingAddress->getStreet());
$shippingAddress["postcode"] = $this->shippingAddress->getPostcode();
$shippingAddress["city"] = $this->shippingAddress->getCity();
$shippingAddress["country"] = $this->getCountry($this->shippingAddress->getCountryId());
return $shippingAddress;
}
/**
* @param $countryId
* @return string
*/
public function getCountry($countryId)
{
$country = $this->countryFactory->create()->loadByCode($countryId);
return $country->getName();
}
}
```
### Autofill JS
```
define([
'uiComponent',
'Magento_Customer/js/customer-data',
'jquery'
], function (Component, customerData, $) {
'use strict';
return Component.extend({
/** @inheritdoc */
initialize: function () {
this._super();
customerData.reload(['customer'], true);
this.customer = customerData.get('customer'); //pass your custom section name
let self = this;
$(window).on('autofill', function () {
self.autofill();
});
$(window).trigger('autofill');
},
autofill: function () {
let self = this;
jQuery('[data-autofillid^=customer]').each(function (field) {
let name = jQuery(this).data('autofillid');
let attributeNames = name.split("_");
if (attributeNames.length === 3) {
if (self.customer().hasOwnProperty(attributeNames[1])) {
if (self.customer()[attributeNames[1]].hasOwnProperty(attributeNames[2])) {
jQuery(this).attr('data-value', self.customer()[attributeNames[1]][attributeNames[2]]);
jQuery(this).val(self.customer()[attributeNames[1]][attributeNames[2]]);
if (jQuery(this).prop('nodeName') === 'SPAN') {
jQuery(this).html(self.customer()[attributeNames[1]][attributeNames[2]]);
}
}
}
} else if (attributeNames.length === 2) {
if (self.customer().hasOwnProperty(attributeNames[1])) {
jQuery(this).attr('data-value', self.customer()[attributeNames[1]]);
jQuery(this).val(self.customer()[attributeNames[1]]);
if (jQuery(this).prop('nodeName') === 'SPAN') {
jQuery(this).html(self.customer()[attributeNames[1]]);
}
}
}
});
let event = document.createEvent('Event');
event.initEvent('rerender', true, true); //can bubble, and is cancellable
window.dispatchEvent(event);
/**
* fetch parameters from url
*/
let getUrlParameter = function getUrlParameter(sParam) {
let sPageURL = window.location.search.substring(1),
sURLVariables = sPageURL.split('&'),
sParameterName,
i;
for (i = 0; i < sURLVariables.length; i++) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1].replace(/\+/g, " "));
}
}
};
/**
* for typo3 created autofills we have to use a global variable
*/
if (window.hasOwnProperty('autofill')) {
let autofills = window.autofill;
autofills.forEach(function (autofill) {
let elements = $('[data-autofillid=' + autofill.key + ']');
if (autofill.key === 'seminar_dates') {
/**
* remove all existing options
*/
jQuery(elements).find('option').remove();
/**
* add options to select
*/
let options = typeof autofill.value === 'string' ? JSON.parse(autofill.value) : autofill.value;
options.forEach(function (option) {
jQuery(elements).append('<option value="'+option.value+'">'+option.label+'</option>');
});
} else {
elements.each(function (index, el) {
$(el).val(decodeURIComponent(autofill.value.replace(/\+/g, " ")));
$(el).html(decodeURIComponent(autofill.value.replace(/\+/g, " ")));
});
}
});
}
let parameters = getUrlParameter('autofill');
if (parameters) {
parameters.split("|").forEach(function (parameter) {
let keyValue = parameter.split(":");
if (keyValue.length > 1) {
jQuery('[data-autofillid=' + keyValue[0] + ']').each(function () {
if (keyValue[0] === 'profitcenter' && $(this).val().length) {
return;
}
let value = [];
for (let i = 1; i < keyValue.length; i++) {
value.push(keyValue[i]);
}
value = decodeURIComponent(value.join(':'));
if (jQuery(this).prop('nodeName') === 'SPAN') {
jQuery(this).html(value);
} else {
jQuery(this).val(value);
}
});
}
});
}
}
});
});
```
### Expected result (*)
1. Customers see only their information
### Actual result (*)
1. Customers can see data of other customers, also with private content (CustomerData) and not cached pages like the myaccount dashboard