Skip to content

Commit 29f823b

Browse files
committed
Improve validation of Theme directory names
1 parent 79f2a6d commit 29f823b

File tree

4 files changed

+62
-3
lines changed

4 files changed

+62
-3
lines changed

modules/cms/classes/Theme.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,14 @@ public function getPath(?string $dirName = null): string
9393

9494
/**
9595
* Sets the theme directory name.
96+
* @throws ApplicationException if the directory name is invalid.
9697
*/
9798
public function setDirName(string $dirName): void
9899
{
100+
if (!static::isValidDirName($dirName)) {
101+
throw new ApplicationException(Lang::get('cms::lang.theme.dir_name_invalid'));
102+
}
103+
99104
$this->dirName = $dirName;
100105
}
101106

@@ -107,6 +112,14 @@ public function getDirName(): string
107112
return $this->dirName;
108113
}
109114

115+
/**
116+
* Determines if the given directory name is valid.
117+
*/
118+
public static function isValidDirName(string $dirName): bool
119+
{
120+
return (bool) preg_match('/^[a-z0-9\_\-]+$/i', $dirName);
121+
}
122+
110123
/**
111124
* Helper for {{ theme.id }} twig vars
112125
* Returns a unique string for this theme.
@@ -237,9 +250,14 @@ public static function getActiveTheme(): self
237250
/**
238251
* Sets the active theme in the database.
239252
* The active theme code is stored in the database and overrides the configuration cms.activeTheme parameter.
253+
* @throws ApplicationException if the directory name is invalid.
240254
*/
241255
public static function setActiveTheme(string $code): void
242256
{
257+
if (!static::isValidDirName($code)) {
258+
throw new ApplicationException(Lang::get('cms::lang.theme.dir_name_invalid'));
259+
}
260+
243261
self::resetCache();
244262

245263
Parameter::set(self::ACTIVE_KEY, $code);

modules/cms/console/ThemeInstall.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function handle()
5050
}
5151

5252
if ($argDirName) {
53-
if (!preg_match('/^[a-z0-9\_\-]+$/i', $argDirName)) {
53+
if (!Theme::isValidDirName($argDirName)) {
5454
return $this->error('Invalid destination directory name.');
5555
}
5656

modules/cms/controllers/Themes.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public function index_onCreate()
149149
throw new ValidationException(['name' => Lang::get('cms::lang.theme.create_theme_required_name')]);
150150
}
151151

152-
if (!preg_match('/^[a-z0-9\_\-]+$/i', $newDirName)) {
152+
if (!CmsTheme::isValidDirName($newDirName)) {
153153
throw new ValidationException(['dir_name' => Lang::get('cms::lang.theme.dir_name_invalid')]);
154154
}
155155

@@ -233,7 +233,7 @@ public function index_onDuplicateTheme()
233233
$sourcePath = $theme->getPath();
234234
$destinationPath = themes_path().'/'.$newDirName;
235235

236-
if (!preg_match('/^[a-z0-9\_\-]+$/i', $newDirName)) {
236+
if (!CmsTheme::isValidDirName($newDirName)) {
237237
throw new ValidationException(['new_dir_name' => Lang::get('cms::lang.theme.dir_name_invalid')]);
238238
}
239239

modules/cms/tests/classes/ThemeTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Cms\Classes\Theme;
77
use Config;
88
use Event;
9+
use Winter\Storm\Exception\ApplicationException;
910

1011
class ThemeTest extends TestCase
1112
{
@@ -122,4 +123,44 @@ public function testChildThemeAssetUrl()
122123
$theme->assetUrl('assets/css/style2.css')
123124
);
124125
}
126+
127+
public function dirNameValidityProvider(): array
128+
{
129+
return [
130+
// Valid names
131+
'lowercase' => ['mytheme', true],
132+
'uppercase' => ['MYTHEME', true],
133+
'mixed case' => ['MyTheme', true],
134+
'with digits' => ['theme123', true],
135+
'with hyphens' => ['my-theme', true],
136+
'with underscores' => ['my_theme', true],
137+
'all allowed' => ['My-Theme_123', true],
138+
'single char' => ['a', true],
139+
140+
// Invalid names
141+
'empty string' => ['', false],
142+
'dot' => ['.', false],
143+
'dot dot' => ['..', false],
144+
'path traversal' => ['../etc', false],
145+
'forward slash' => ['foo/bar', false],
146+
'backslash' => ['foo\\bar', false],
147+
'spaces' => ['my theme', false],
148+
'special chars' => ['theme!@#', false],
149+
];
150+
}
151+
152+
/**
153+
* @dataProvider dirNameValidityProvider
154+
*/
155+
public function testIsValidDirName(string $dirName, bool $expected): void
156+
{
157+
$this->assertSame($expected, Theme::isValidDirName($dirName));
158+
}
159+
160+
public function testLoadRejectsPathTraversal()
161+
{
162+
$this->expectException(ApplicationException::class);
163+
164+
Theme::load('../../etc');
165+
}
125166
}

0 commit comments

Comments
 (0)