Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ All notable changes of the phpMyAdmin 6.0 release series are documented in this
* [#19795](https://github.com/phpmyadmin/phpmyadmin/pull/19795): Use `additional-methods.min.js` instead of `additional-methods.js`
* [#19564](https://github.com/phpmyadmin/phpmyadmin/pull/19564): Update the table comment field to a textarea
* [#19947](https://github.com/phpmyadmin/phpmyadmin/pull/19947): Export ODS column headers by default
* [#15993](https://github.com/phpmyadmin/phpmyadmin/issues/15993): Do not expose the phpMyAdmin version in the cache-busting `?v=` query string of CSS and JS assets

### Removed

Expand Down
8 changes: 4 additions & 4 deletions resources/js/modules/ajax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,10 +716,10 @@ const AJAX = {
// Clear loaded scripts if they are from another version of phpMyAdmin.
// Depends on common params being set before loading scripts in responseHandler
if (self.scriptsVersion === null) {
self.scriptsVersion = CommonParams.get('version');
} else if (self.scriptsVersion !== CommonParams.get('version')) {
self.scriptsVersion = CommonParams.get('asset_version');
} else if (self.scriptsVersion !== CommonParams.get('asset_version')) {
self.scripts = [];
self.scriptsVersion = CommonParams.get('version');
self.scriptsVersion = CommonParams.get('asset_version');
}

self.scriptsCompleted = false;
Expand Down Expand Up @@ -786,7 +786,7 @@ const AJAX = {
const script = document.createElement('script');
const self = this;

script.src = 'js/' + name + '?' + 'v=' + encodeURIComponent(CommonParams.get('version'));
script.src = 'js/' + name + '?' + 'v=' + encodeURIComponent(CommonParams.get('asset_version'));
script.async = false;
script.onload = function () {
self.done(name, callback);
Expand Down
8 changes: 4 additions & 4 deletions resources/templates/base.twig
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" type="text/css" href="{{ header.theme_path }}/jquery/jquery-ui.css">
{% if header.codemirror_enable -%}
<link rel="stylesheet" type="text/css" href="js/vendor/codemirror/lib/codemirror.css?v={{ pma.version|url_encode }}">
<link rel="stylesheet" type="text/css" href="js/vendor/codemirror/addon/hint/show-hint.css?v={{ pma.version|url_encode }}">
<link rel="stylesheet" type="text/css" href="js/vendor/codemirror/lib/codemirror.css?v={{ header.asset_version|url_encode }}">
<link rel="stylesheet" type="text/css" href="js/vendor/codemirror/addon/hint/show-hint.css?v={{ header.asset_version|url_encode }}">
{% if header.lint_enable -%}
<link rel="stylesheet" type="text/css" href="js/vendor/codemirror/addon/lint/lint.css?v={{ pma.version|url_encode }}">
<link rel="stylesheet" type="text/css" href="js/vendor/codemirror/addon/lint/lint.css?v={{ header.asset_version|url_encode }}">
{% endif %}
{% endif %}
<link rel="stylesheet" type="text/css" href="{{ header.theme_path }}/css/theme{{ pma.text_dir == 'rtl' ? '.rtl' }}.css?v={{ pma.version|url_encode }}">
<link rel="stylesheet" type="text/css" href="{{ header.theme_path }}/css/theme{{ pma.text_dir == 'rtl' ? '.rtl' }}.css?v={{ header.asset_version|url_encode }}">
<title>{{ header.title }}</title>
{{ header.scripts|raw }}
<noscript><style>html{display:block}</style></noscript>
Expand Down
2 changes: 1 addition & 1 deletion resources/templates/scripts.twig
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% for file in files %}
<script data-cfasync="false" src="{{ file.filename starts with 'index.php' ? file.filename : 'js/' ~ file.filename -}}
{{- '.php' in file.filename ? get_common(file.params|merge({'v': pma.version})) : '?v=' ~ pma.version|url_encode }}"></script>
{{- '.php' in file.filename ? get_common(file.params|merge({'v': asset_version})) : '?v=' ~ asset_version|url_encode }}"></script>
{% endfor %}

<script data-cfasync="false">
Expand Down
41 changes: 41 additions & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use function array_key_last;
use function array_shift;
use function array_slice;
use function bin2hex;
use function count;
use function defined;
use function explode;
Expand All @@ -26,6 +27,7 @@
use function fread;
use function function_exists;
use function get_defined_constants;
use function hash_hmac;
use function implode;
use function in_array;
use function ini_get;
Expand All @@ -41,13 +43,15 @@
use function ob_end_clean;
use function ob_start;
use function parse_url;
use function random_bytes;
use function realpath;
use function rtrim;
use function setcookie;
use function sprintf;
use function str_ends_with;
use function stripos;
use function strtolower;
use function substr;
use function sys_get_temp_dir;
use function time;
use function trim;
Expand Down Expand Up @@ -742,4 +746,41 @@ public function getChangeLogFilePath(): string
{
return CHANGELOG_FILE;
}

/**
* Returns an opaque token that changes with every phpMyAdmin version.
*
* It is used as the cache-busting query string of CSS and JS assets instead of the
* plain version number, so that the version is not exposed to unauthenticated
* visitors (e.g. on the login page). It is keyed with the blowfish secret so it
* cannot be mapped back to a version with a lookup table.
*/
public function getAssetVersion(): string
{
return substr(hash_hmac('sha256', Version::VERSION, $this->getAssetVersionKey()), 0, 12);
}

/**
* Returns the key used to derive the asset version token.
*
* When no blowfish secret is configured, a random per-session key is used instead,
* so that the token does not degrade to a publicly computable hash of the version.
*/
private function getAssetVersionKey(): string
{
if ($this->config->blowfish_secret !== '') {
return $this->config->blowfish_secret;
}

/** @var mixed $key */
$key = $_SESSION['asset_version_key'] ?? null;
if (is_string($key) && $key !== '') {
return $key;
}

$key = bin2hex(random_bytes(16));
$_SESSION['asset_version_key'] = $key;

return $key;
}
}
2 changes: 1 addition & 1 deletion src/Footer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Footer

public function __construct(Template $template, private readonly Config $config)
{
$this->scripts = new Scripts($template);
$this->scripts = new Scripts($template, $config);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function getScripts(): Scripts
return $this->scripts;
}

$this->scripts = new Scripts($this->template);
$this->scripts = new Scripts($this->template, $this->config);

$this->scripts->addFile('runtime.js');
$this->scripts->addFile('vendor/jquery/jquery.min.js');
Expand Down Expand Up @@ -134,7 +134,7 @@ public function getJsParams(): array
'is_https' => $this->config->isHttps(),
'rootPath' => $this->config->getRootPath(),
'arg_separator' => Url::getArgSeparator(),
'version' => Version::VERSION,
'asset_version' => $this->config->getAssetVersion(),
];
if ($this->config->hasSelectedServer()) {
$params['auth_type'] = $this->config->selectedServer['auth_type'];
Expand Down Expand Up @@ -283,6 +283,7 @@ public function getDisplay(ResponseRenderer $responseRenderer): array
'codemirror_enable' => $this->config->config->CodemirrorEnable,
'lint_enable' => $this->config->config->LintEnable,
'theme_path' => $theme->getPath(),
'asset_version' => $this->config->getAssetVersion(),
'server' => Current::$server,
'title' => $this->getPageTitle(),
'scripts' => $scripts->getDisplay(),
Expand Down
8 changes: 6 additions & 2 deletions src/Scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Scripts
/**
* Generates new Scripts objects
*/
public function __construct(private readonly Template $template)
public function __construct(private readonly Template $template, private readonly Config $config)
{
}

Expand Down Expand Up @@ -119,6 +119,10 @@ public function getFiles(): array
*/
public function getDisplay(): string
{
return $this->template->render('scripts', ['files' => $this->files, 'code' => $this->code]);
return $this->template->render('scripts', [
'files' => $this->files,
'code' => $this->code,
'asset_version' => $this->config->getAssetVersion(),
]);
}
}
33 changes: 33 additions & 0 deletions tests/unit/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PhpMyAdmin\Config\Settings\Server;
use PhpMyAdmin\Dbal\ConnectionType;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\Version;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Depends;
Expand All @@ -18,9 +19,11 @@

use function file_put_contents;
use function get_defined_constants;
use function hash_hmac;
use function md5;
use function realpath;
use function stristr;
use function substr;
use function sys_get_temp_dir;
use function tempnam;
use function unlink;
Expand Down Expand Up @@ -647,4 +650,34 @@ public function testGetChangeLogFilePath(): void
{
self::assertSame(CHANGELOG_FILE, (new Config())->getChangeLogFilePath());
}

public function testGetAssetVersion(): void
{
$config = new Config();
$config->config = new Settings(['blowfish_secret' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']);
$assetVersion = $config->getAssetVersion();

self::assertMatchesRegularExpression('/^[0-9a-f]{12}$/', $assetVersion);
self::assertSame($assetVersion, $config->getAssetVersion());
self::assertStringNotContainsString(Version::VERSION, $assetVersion);
self::assertSame(
substr(hash_hmac('sha256', Version::VERSION, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), 0, 12),
$assetVersion,
);

$config->config = new Settings(['blowfish_secret' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb']);
self::assertNotSame($assetVersion, $config->getAssetVersion());
}

public function testGetAssetVersionWithoutBlowfishSecret(): void
{
$config = new Config();
$config->config = new Settings(['blowfish_secret' => '']);
$assetVersion = $config->getAssetVersion();

self::assertMatchesRegularExpression('/^[0-9a-f]{12}$/', $assetVersion);
self::assertSame($assetVersion, $config->getAssetVersion());
// The token must not be a publicly computable hash of the version.
self::assertNotSame(substr(hash_hmac('sha256', Version::VERSION, ''), 0, 12), $assetVersion);
}
}
11 changes: 11 additions & 0 deletions tests/unit/HeaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PhpMyAdmin\Tests\Clock\MockClock;
use PhpMyAdmin\Tests\Stubs\ResponseRenderer;
use PhpMyAdmin\Theme\ThemeManager;
use PhpMyAdmin\Version;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Medium;
Expand Down Expand Up @@ -108,6 +109,7 @@ public function testEnable(): void
'codemirror_enable' => true,
'lint_enable' => true,
'theme_path' => '',
'asset_version' => $config->getAssetVersion(),
'server' => 0,
'title' => 'phpMyAdmin',
'scripts' => $header->getScripts()->getDisplay(),
Expand Down Expand Up @@ -143,6 +145,15 @@ public function testGetJsParams(): void
);
}

public function testGetJsParamsDoesNotExposeVersion(): void
{
$header = $this->getNewHeaderInstance();
$params = $header->getJsParams();
self::assertArrayHasKey('asset_version', $params);
self::assertArrayNotHasKey('version', $params);
self::assertStringNotContainsString(Version::VERSION, $header->getJsParamsCode());
}

public function testGetJsParamsCode(): void
{
$header = $this->getNewHeaderInstance();
Expand Down
1 change: 1 addition & 0 deletions tests/unit/ResponseRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
'codemirror_enable' => true,
'lint_enable' => true,
'theme_path' => '',
'asset_version' => Config::getInstance()->getAssetVersion(),

Check failure on line 107 in tests/unit/ResponseRendererTest.php

View workflow job for this annotation

GitHub Actions / analyse-php (8.2)

DeprecatedMethod

tests/unit/ResponseRendererTest.php:107:40: DeprecatedMethod: The method PhpMyAdmin\Config::getInstance has been marked as deprecated (see https://psalm.dev/001)

Check failure on line 107 in tests/unit/ResponseRendererTest.php

View workflow job for this annotation

GitHub Actions / analyse-php (8.2)

Call to deprecated method getInstance() of class PhpMyAdmin\Config: Use dependency injection instead.
'server' => 0,
'title' => 'phpMyAdmin',
'scripts' => $header->getScripts()->getDisplay(),
Expand Down
16 changes: 10 additions & 6 deletions tests/unit/ScriptsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace PhpMyAdmin\Tests;

use PhpMyAdmin\Config;
use PhpMyAdmin\Config\Settings;
use PhpMyAdmin\Scripts;
use PhpMyAdmin\Template;
use PhpMyAdmin\Version;
Expand All @@ -18,6 +19,8 @@ class ScriptsTest extends AbstractTestCase
{
protected Scripts $object;

private string $assetVersion;

/**
* Sets up the fixture, for example, opens a network connection.
* This method is called before a test is executed.
Expand All @@ -26,7 +29,10 @@ protected function setUp(): void
{
parent::setUp();

$this->object = new Scripts(new Template(new Config()));
$config = new Config();
$config->config = new Settings(['blowfish_secret' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']);
$this->object = new Scripts(new Template($config), $config);
$this->assetVersion = $config->getAssetVersion();
}

/**
Expand All @@ -40,14 +46,12 @@ public function testGetDisplay(): void

$actual = $this->object->getDisplay();

self::assertStringContainsString('src="js/common.js?v=' . $this->assetVersion . '"', $actual);
self::assertStringContainsString(
'src="js/common.js?v=' . rawurlencode(Version::VERSION) . '"',
$actual,
);
self::assertStringContainsString(
'src="index.php?route=%2Fmessages&l=en&v=' . rawurlencode(Version::VERSION) . '&lang=en"',
'src="index.php?route=%2Fmessages&l=en&v=' . $this->assetVersion . '&lang=en"',
$actual,
);
self::assertStringNotContainsString(rawurlencode(Version::VERSION), $actual);
self::assertStringContainsString(
'window.AJAX.scriptHandler.add(\'vendor\/codemirror\/lib\/codemirror.js\', false);',
$actual,
Expand Down
Loading