//nefariousplan

CVE-2026-42155: OpenMage's API Session ID Is the MD5 of When You Logged In

pattern

cve

proof of concept

OpenMage LTS is the community-maintained fork of Magento 1, the e-commerce platform Adobe end-of-lifed in 2020. Through version 20.17.0, its XML-RPC and SOAP APIs minted session tokens with one line of PHP:

$this->_currentSessId = md5(Carbon::now()->getTimestamp() . uniqid('', true) . $sessionName);

The first input is the Unix second. The second input is the PHP standard-library function whose own manual page warns it "does not generate cryptographically secure values, and must not be used for cryptographic purposes." The third input is null. The output is a thirty-two-character string that looks unguessable because every developer who has touched this method since 2009 has trusted that MD5 of three things must be unguessable. CVE-2026-42155 (CVSS 9.3, reported by @0x0OZ, fixed in 20.18.0) says it is not. The database schema disagreed first, and it disagreed in writing, in 2009.

OpenMage LTS is the community-maintained fork of Magento 1, the e-commerce platform Adobe end-of-lifed in 2020. Through version 20.17.0, its XML-RPC and SOAP APIs minted session tokens with one line of PHP:

$this->_currentSessId = md5(Carbon::now()->getTimestamp() . uniqid('', true) . $sessionName);

The first input is the Unix second. The second input is the PHP standard-library function whose own manual page warns it "does not generate cryptographically secure values, and must not be used for cryptographic purposes." The third input is null. The output is a thirty-two-character string that looks unguessable because every developer who has touched this method since 2009 has trusted that MD5 of three things must be unguessable. CVE-2026-42155 (CVSS 9.3, reported by @0x0OZ, fixed in 20.18.0) says it is not. The database schema disagreed first, and it disagreed in writing, in 2009.

The schema is testimony

The OpenMage install script for the api_session table is in app/code/core/Mage/Api/sql/api_setup/install-1.6.0.0.php. The column that stores the session token is declared this way:

->addColumn('sessid', Varien_Db_Ddl_Table::TYPE_TEXT, 40, [
], 'Sessioin id')

Forty characters. The misspelling in the comment is in the file. The width is what matters. Forty is the width of a SHA-1 digest. Whoever wrote the schema in 2009 sized the column for a hash and stopped, because the design that needed a column was a design that minted a hash.

The patch that resolves CVE-2026-42155 ships a new upgrade file, upgrade-1.6.0.3-1.6.0.4.php, whose entire job is to widen the same column:

$this->getConnection()->changeColumn(
    $table,
    'sessid',
    'sessid',
    [
        'type'      => Varien_Db_Ddl_Table::TYPE_TEXT,
        'length'    => 64,
        'nullable'  => false,
        'comment'   => 'Session ID',
    ],
);

Sixty-four is the hex width of random_bytes(32). The schema had to grow because the session ID stopped being a hash digest and started being thirty-two bytes of CSPRNG output. The schema was the part of the design that had a strong opinion about the token's size; nobody had read it as the part of the design that had a strong opinion about the token's source of randomness. Both opinions were the same number, and the column has carried the receipt since the table was first created.

"supress warnings"

The PR that lands CVE-2026-42155 is OpenMage/magento-lts#5501. Its title is Weak API Session ID. Its three component commits, in order, have these subjects:

chore: supress warnings
changed db colum length
cs

That is the entire commit-message corpus for a critical authentication fix. The squash merge took the first commit's subject, so the line in git log --oneline for the merge is:

bc177b58 *chore: supress warnings (#5501)

The asterisk is the only one in the file's recent commit history. The leading word is "chore." The third word is a typo. The PR title knew what the PR was for; the commit subjects coordinate to look like routine maintenance.

The 2026 disclosure timeline:

Date Event
2026-04-20 Patch commit authored, subject "chore: supress warnings"
2026-04-27 PR #5501 squash-merged into main
2026-05-04 GHSA-2cwr-gcf9-pvxr published, severity rated 9.3 critical
2026-05-15 CVE-2026-42155 published in NVD

Eight days between the public merge of a security fix to a deployed e-commerce platform's API authentication and the public advisory that names what the fix is for. Anyone polling git log for changes to Mage_Api_Model_Session::start during those eight days had the bug, the fix, and the affected versions in front of them. The merchants running the unpatched code did not.

The PHP manual has been warning about this since 2009

uniqid() is not a random number generator. PHP's source for ext/standard/uniqid.c is sixty lines long. The relevant branch:

if (more_entropy) {
    spprintf(&uniqid, 0, "%s%08x%05x%.8F",
             prefix, sec, usec, php_combined_lcg() * 10);
} else {
    spprintf(&uniqid, 0, "%s%08x%05x", prefix, sec, usec);
}

uniqid('', true) returns sprintf('%08x%05x%.8F', sec, usec, php_combined_lcg() * 10). The first eight hex characters are gettimeofday()'s seconds field. An attacker who knows the second a session was issued has them. The next five hex characters are the microsecond field, at most one million candidates. The trailing ten characters are the output of php_combined_lcg(), a Park-Miller-style combined linear congruential generator that PHP has shipped for thirty years; its seed is derived once per process from the wall clock and the process ID, and its sequence is recoverable from a few observed values.

The PHP manual page for uniqid() is direct, and has been for over a decade:

This function does not generate cryptographically secure values, and must not be used for cryptographic purposes, or purposes that require returned values to be unguessable.

That warning was in the documentation when Magento 1.7.0.0-rc1 shipped in March 2012 and added uniqid('', true) into the API session token's MD5 input. The git history of app/code/core/Mage/Api/Model/Session.php carries the two construction-changing commits side by side, fourteen years apart in the import stream:

2009-05-30  Import Magento Release 1.3.2
  $this->_currentSessId = md5(time() . $sessionName);

2012-03-31  Import Magento Release 1.7.0.0-rc1
  $this->_currentSessId = md5(time() . uniqid('', true) . $sessionName);

The change is the addition of the word uniqid. The intent was to add entropy. The function added is the one whose manual page says it does not provide entropy.

Directly above both of those lines, in every release from 2009 until July 2022, is one more line:

//        parent::start($sessionName=null);

parent::start is Mage_Core_Model_Session_Abstract::start, which calls PHP's native session_start(), which mints a session ID from PHP's CSPRNG-backed session module. The commented-out line in Mage_Api_Model_Session::start is the original implementation. Whoever shipped Magento 1.3.2 disabled the call that produced a cryptographically random session ID and replaced it with md5(time()). The comment marker on that line survived every commit on this file for thirteen years, then was removed in OpenMage/magento-lts#2339 ("Removed dead code") on 2022-07-30 by the same maintainer who shipped CVE-2026-42155's fix four years later.

Seven other places

The pattern is not isolated to the API session. A grep across app/code/core for md5 applied to time-derived inputs surfaces handlers that produce credential-equivalent strings:

Mage/Customer/Model/Customer.php:811
  public function getRandomConfirmationKey() {
      return md5(uniqid());
  }

Mage/Customer/Helper/Data.php:450
  public function generateResetPasswordLinkCustomerId($customerId) {
      return md5(uniqid($customerId . microtime() . mt_rand(), true));
  }

Mage/Core/Helper/Data.php:798
  public function uniqHash($prefix = '') {
      return $prefix . md5(uniqid(microtime() . mt_rand(), true));
  }

The first is the function whose return value is mailed to a new customer as their email-address confirmation token. It calls uniqid() with no prefix and more_entropy=false, which is the thirteen-hex-character timestamp form, no LCG bits added. The second is the function whose return value goes in the URL of a password-reset email; its inputs are the customer's primary key (enumerable), the wall clock in milliseconds, and mt_rand(), whose Mersenne Twister state has had public seed-recovery attacks since 2010. The third is the generic "random-looking string" helper other handlers reach for when they need a token. None of them call random_bytes. All of them are MD5 applied to time-derived inputs, wrapped in the same uniqid(...) whose manual page says it is not random.

CVE-2026-42155 names the one in Mage_Api_Model_Session. PR #5501 patches that one and only that one. The other three callers are unpatched in 20.18.0. They are the same shape. They will be other CVEs, when somebody writes the brute-forcer for them.

The brute force is the loop the manual wrote

The attack model from the GHSA is straightforward. The victim is the API account a Magento merchant has provisioned for a marketplace connector or an ERP integration, the account that logs in on a cron schedule because non-interactive integrations always do. The attacker observes one legitimate session being issued, captures the second from the HTTP response's Date: header (RFC 7231 says servers SHOULD set it, and Apache, Nginx, and every reverse proxy in front of Magento set it automatically), then enumerates the candidate token space.

The candidate space for one Unix second of one PHP-FPM worker is (usec_window) × (lcg_state). The usec window for a synchronously-served HTTP request is in the thousands to tens of thousands of microseconds, not the full million. The LCG state is recoverable from a small number of observed uniqid('', true) outputs, which the attacker collects by hitting /api/xmlrpc/ with login attempts under their own account. Each candidate is one MD5 (microseconds on a laptop), then one XML-RPC call to a method like resources that the session can call without further authorization. There is no rate limit on the XML-RPC endpoint. The GHSA reporter demonstrated this by showing five consecutive tokens minted within a single Unix second, which is the demonstration that the second is recoverable, which is the only ingredient still needed for the brute force to terminate.

The eventual session is treated as a fully authenticated API caller. _renewBySessId($sessId) in the same file does a loadBySessId against the api_session table; if the row exists and the timeout has not elapsed, the caller is bound to that row's user, the ACL is loaded, and the next XML-RPC method runs as that user. There is no second authenticator. The thirty-two-character sessid is the bearer credential.

Hash is not entropy

This is the pattern, and it has a slug in our catalog: hash-is-not-entropy. The canonical exhibit is Allegra's password-reset token that was the SHA-256 of its own expiration date. The shape is identical. Take a low-entropy public value, run it through a cryptographic hash, treat the output as a secret. The output's entropy is bounded by the input's entropy; the hash function only spreads the bits across a wider character set. SHA-256 of a guessable integer is a guessable token. MD5 of a guessable integer is a guessable token, faster.

OpenMage's version of the pattern adds two details the Allegra version did not have. The input is not just guessable; the input is observable, because the Date: header in the HTTP response that issued the session ID names the second to within a second. And the construction has been preserved across seventeen years and two refactors that each had the chance to replace it. The April 2026 ClockInterface refactor changed the call from time() to Carbon::now()->getTimestamp(). The result of that function is the same integer. The cleanup that removed the commented-out parent::start in 2022 deleted the in-code testimony that the original implementation had used PHP's CSPRNG. Both refactors touched the line. Neither rewrote it.

The fix that ships in 20.18.0 is one expression, bin2hex(random_bytes(32)), the call the PHP manual has pointed to since random_bytes shipped in PHP 7.0 in December 2015. The migration that widens the column is twenty-seven lines. The total diff that resolves CVE-2026-42155 is fifteen lines of code touching two files. The fifteen lines have been available to write for ten years. They were not written for this file in any of those years. They have not been written for the three other Customer- and Core-helper files that use the same construction in any of those years either. The version of the bug that has the CVE number is the version somebody wrote the brute-forcer for.

PoC and advisory: OpenMage/magento-lts GHSA-2cwr-gcf9-pvxr

The version of the bug that has the CVE number is the version somebody wrote the brute-forcer for.