Skip to content

Commit 5998269

Browse files
committed
Security patch.
Changelog excerpt: - Improved the safeguards for getAssetPath.
1 parent 5bedfb6 commit 5998269

File tree

4 files changed

+40
-16
lines changed

4 files changed

+40
-16
lines changed

Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ __*Why "v3.0.0" instead of "v1.0.0?"*__ Prior to phpMussel v3, the "phpMussel Co
200200
- [2025.07.07]: The formatFilesize method wasn't accounting for negative numbers; Fixed.
201201
- [2025.07.26]: Some browsers, in some contexts, were raising errors during request inspection concerning the absence of any X-Content-Type-Options header declaration (though it isn't entirely clear whether this error had any actual effect); Fixed.
202202

203+
#### Security.
204+
- [2025.08.09]: Improved the safeguards for getAssetPath.
205+
203206
#### Other changes.
204207
- [2025.07.05]: Aesthetic patch.
205208
- [2025.07.08]: Added the ability to route all outbound requests through a proxy, and two new configuration directives, `request_proxy` and `request_proxyauth`.

assets/default/_logs.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
<tr>
33
<td class="nav{mod_class_nav}"><div class="big">
44
{nav} <hr /></div>
5-
<div class="ng1 idx">{LogFiles}</div>
5+
<div class="ng1 idx">{LogFiles} </div>
66
</td>
77
<td class="right{mod_class_right}">
8-
<div class="ng1 s flong mob">{LogFiles}</div>
8+
<div class="ng1 s flong mob">{LogFiles} </div>
99
<span class="s">{TextModeSwitchLink}{ProcessTime}</span>
1010
<hr />
1111
{logfileData}

src/FrontEnd.php

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* License: GNU/GPLv2
99
* @see LICENSE.txt
1010
*
11-
* This file: Front-end handler (last modified: 2025.08.08).
11+
* This file: Front-end handler (last modified: 2025.08.09).
1212
*/
1313

1414
namespace phpMussel\FrontEnd;
@@ -330,6 +330,11 @@ public function view(string $Page = ''): void
330330
'FormTarget' => $_POST['phpmussel-form-target'] ?? ''
331331
];
332332

333+
/** Fix for immediate display of freshly updated theme selection. */
334+
if (!empty($_POST['config_frontend_theme']) && !preg_match('~[^a-z]~', $_POST['config_frontend_theme']) && ($_POST['config_frontend_theme'] === 'default' ?: file_exists($this->AssetsPath . $_POST['config_frontend_theme'] . DIRECTORY_SEPARATOR . 'frontend.css'))) {
335+
$FE['theme'] = $_POST['config_frontend_theme'];
336+
}
337+
333338
/** Populated by [Home | Log Out] by default; Replaced by [Log Out] for some specific pages (e.g., the homepage). */
334339
$FE['bNav'] = $FE['HomeButton'] . $FE['LogoutButton'];
335340

@@ -388,6 +393,9 @@ public function view(string $Page = ''): void
388393
/** A simple passthru for the front-end CSS. */
389394
if ($Page === 'css') {
390395
$this->eTaggable('frontend.css', function ($AssetData) use (&$FE) {
396+
if (!empty($this->QueryVariables['theme-mode'])) {
397+
$FE['theme_mode_effects'] = $this->Loader->ConfigurationDefaults['frontend']['theme_mode']['effects'][$this->QueryVariables['theme-mode']] ?? '';
398+
}
391399
return $this->embedAssets($this->Loader->parse($FE, $AssetData, true));
392400
});
393401
}
@@ -882,6 +890,20 @@ private function filterByDefined(string $ChoiceKey): bool
882890
return defined($ChoiceKey);
883891
}
884892

893+
/**
894+
* Traversal detection.
895+
*
896+
* @param string $Path The path to check for traversal.
897+
* @return bool True when the path is traversal-free. False when traversal has been detected.
898+
*/
899+
private function freeFromTraversal(string $Path): bool
900+
{
901+
return !preg_match(
902+
'~(?://|(?<![\da-z])\.\.(?![\da-z])|/\.(?![\da-z])|(?<![\da-z])\./|[\x01-\x1F\[-^`?*$])~i',
903+
str_replace('\\', '/', $Path)
904+
);
905+
}
906+
885907
/**
886908
* Get the appropriate path for a specified asset as per the defined theme.
887909
*
@@ -892,8 +914,8 @@ private function filterByDefined(string $ChoiceKey): bool
892914
*/
893915
private function getAssetPath(string $Asset, bool $CanFail = false): string
894916
{
895-
/** Guard against unsafe paths. */
896-
if (preg_match('~[^\da-z._]~i', $Asset)) {
917+
/** Guard against unsafe paths and traversal attacks. */
918+
if (preg_match('~[^\da-z._]~i', $Asset) || !$this->freeFromTraversal($Asset)) {
897919
return '';
898920
}
899921

@@ -905,20 +927,20 @@ private function getAssetPath(string $Asset, bool $CanFail = false): string
905927
}
906928
}
907929

908-
/** Non-default assets. */
930+
/** Non-default theme assets. */
909931
if (
910932
$this->Loader->Configuration['frontend']['theme'] !== 'default' &&
911933
is_readable($this->AssetsPath . $this->Loader->Configuration['frontend']['theme'] . DIRECTORY_SEPARATOR . $Asset)
912934
) {
913935
return $this->AssetsPath . $this->Loader->Configuration['frontend']['theme'] . DIRECTORY_SEPARATOR . $Asset;
914936
}
915937

916-
/** Default assets. */
938+
/** Default theme assets. */
917939
if (is_readable($this->AssetsPath . 'default' . DIRECTORY_SEPARATOR . $Asset)) {
918940
return $this->AssetsPath . 'default' . DIRECTORY_SEPARATOR . $Asset;
919941
}
920942

921-
/** Assets base directory. */
943+
/** Front-end assets base directory assets. */
922944
if (is_readable($this->AssetsPath . $Asset)) {
923945
return $this->AssetsPath . $Asset;
924946
}
@@ -1618,6 +1640,9 @@ private function eTaggable(string $Asset, ?callable $Callback = null): void
16181640
}
16191641
if ($Success) {
16201642
$AssetData = $this->Loader->readFile($ThisAsset);
1643+
if (is_callable($Callback)) {
1644+
$AssetData = $Callback($AssetData);
1645+
}
16211646
$OldETag = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
16221647
$NewETag = hash('sha256', $AssetData) . '-' . strlen($AssetData);
16231648
header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($ThisAsset)));
@@ -1633,9 +1658,6 @@ private function eTaggable(string $Asset, ?callable $Callback = null): void
16331658
if ($NoSniff) {
16341659
header('X-Content-Type-Options: nosniff');
16351660
}
1636-
if (is_callable($Callback)) {
1637-
$AssetData = $Callback($AssetData);
1638-
}
16391661
echo $AssetData;
16401662
die;
16411663
}

src/pages/logs.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* License: GNU/GPLv2
99
* @see LICENSE.txt
1010
*
11-
* This file: The logs page (last modified: 2025.04.24).
11+
* This file: The logs page (last modified: 2025.08.09).
1212
*/
1313

1414
namespace phpMussel\FrontEnd;
@@ -24,13 +24,12 @@
2424
$FE['FE_Content'] = $this->Loader->parse($FE, $this->Loader->readFile($this->getAssetPath('_logs.html')), true);
2525

2626
/** Initialise array for fetching logs data. */
27-
$FE['LogFiles'] = ['Files' => $this->logsRecursiveList(), 'Out' => ''];
27+
$FE['LogFiles'] = ['Files' => $this->logsRecursiveList(), 'Out' => "\n"];
2828

2929
/** Download a log file. */
3030
if (
31-
isset($this->QueryVariables['text-mode'], $this->QueryVariables['logfile']) &&
32-
$this->QueryVariables['text-mode'] === 'download' &&
33-
isset($FE['LogFiles']['Files'][$this->QueryVariables['logfile']])
31+
isset($this->QueryVariables['text-mode'], $this->QueryVariables['logfile'], $FE['LogFiles']['Files'][$this->QueryVariables['logfile']]) &&
32+
$this->QueryVariables['text-mode'] === 'download'
3433
) {
3534
header('Content-Type: application/octet-stream');
3635
header('Content-Transfer-Encoding: Binary');

0 commit comments

Comments
 (0)