Files
XC_VM/tests/Unit/HmacTokenTest.php
T
rootandClaude Opus 5 f2ad51ffd2 fix(auth): compare HMAC stream links strictly, in constant time
validateHMAC accepted a link when md5($genuine) == md5($given). PHP's loose
== reads two digests of the form 0e<digits> as the number 0 and so as equal:
for any request whose genuine HMAC has such an MD5 (about one in 3·10^8,
over parameters the requester chooses — identifier, expiry, max), a given
`hmac` like 240610708 passed as the key, and the stream was served under
that key's connection limits. The regression test carries a concrete case
found by search.

The HMAC is now compared with hash_equals against the given value itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuZvjFSdodqgpyXtaoH1Xt
2026-09-12 21:57:08 +00:00

65 lines
2.0 KiB
PHP

<?php
namespace XcVm\Tests\Unit;
use PHPUnit\Framework\TestCase;
use XcVm\Core\Auth\AuthService;
use XcVm\Core\Util\Encryption;
/**
* validateHMAC must accept the key's HMAC and nothing else.
*
* The fixture below was found by search: with this secret and these request
* parameters the genuine HMAC's MD5 is 0e554217211296920002813236859630 — a
* string PHP's == reads as the number 0. The old check compared
* md5($genuine) == md5($given), so any `hmac` whose MD5 also reads as 0 (such
* as 240610708) passed as the key, for any request whose HMAC happened to land
* there.
*/
class HmacTokenTest extends TestCase {
private const SECRET = 's3cret';
private const IDENTIFIER = '1690494838';
protected function setUp(): void {
if (!defined('OPENSSL_EXTRA')) {
define('OPENSSL_EXTRA', 'test-extra');
}
$GLOBALS['rSettings'] = ['enable_cache' => 0, 'live_streaming_pass' => 'streaming-pass'];
$rKey = Encryption::encrypt(self::SECRET, 'streaming-pass', OPENSSL_EXTRA);
$GLOBALS['db'] = new class($rKey) {
public function __construct(private string $rKey) {
}
public function query($rQuery) {
return true;
}
public function get_rows() {
return [['id' => 9, 'key' => $this->rKey]];
}
};
}
private function check(string $rHMAC) {
return AuthService::validateHMAC($rHMAC, null, 1, 'ts', '', '', self::IDENTIFIER, 0);
}
public function testTheKeysHmacIsAccepted(): void {
$rGenuine = hash_hmac('sha256', '1##ts######' . self::IDENTIFIER . '##0', self::SECRET);
$this->assertSame('0e554217211296920002813236859630', md5($rGenuine), 'fixture');
$this->assertSame(9, $this->check($rGenuine));
}
public function testAValueWhoseMd5ReadsAsZeroIsNotTheKey(): void {
$this->assertSame('0e462097431906509019562988736854', md5('240610708'), 'fixture');
$this->assertNull($this->check('240610708'));
}
public function testAnotherHmacIsRefused(): void {
$this->assertNull($this->check(hash_hmac('sha256', '1##ts######' . self::IDENTIFIER . '##0', 'other-secret')));
$this->assertNull($this->check(''));
}
}