Skip to content

Commit 193e5e7

Browse files
committed
Merge branch '4.1' into 'main'
2 parents 8f5c5fa + ccd7683 commit 193e5e7

4 files changed

Lines changed: 61 additions & 5 deletions

File tree

phpmyfaq/admin/assets/src/content/editor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ export const renderEditor = () => {
266266
ajax: {
267267
url: './api/media-browser',
268268
contentType: 'application/json; charset=UTF-8',
269+
data: {
270+
csrfToken: (document.getElementById('pmf-csrf-token') as HTMLInputElement).value,
271+
},
269272
},
270273
createNewFolder: false,
271274
deleteFolder: false,

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/MediaBrowserController.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use phpMyFAQ\Core\Exception;
2525
use phpMyFAQ\Enums\PermissionType;
2626
use phpMyFAQ\Filter;
27+
use phpMyFAQ\Session\Token;
2728
use phpMyFAQ\Translation;
2829
use phpMyFAQ\Utils;
2930
use RecursiveDirectoryIterator;
@@ -58,11 +59,23 @@ public function index(Request $request): JsonResponse|Response
5859
$action = Filter::filterVar($data->action, FILTER_SANITIZE_SPECIAL_CHARS);
5960

6061
if ($action === 'fileRemove') {
61-
$file = Filter::filterVar($data->name, FILTER_SANITIZE_SPECIAL_CHARS);
62-
$file = PMF_CONTENT_DIR . '/user/images/' . $file;
62+
if (!Token::getInstance($this->container->get(id: 'session'))->verifyToken(
63+
'media-browser',
64+
$data->csrfToken,
65+
)) {
66+
return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
67+
}
68+
69+
$file = basename(Filter::filterVar($data->name, FILTER_SANITIZE_SPECIAL_CHARS));
70+
$allowedDir = realpath(PMF_CONTENT_DIR . '/user/images');
71+
$targetPath = realpath(PMF_CONTENT_DIR . '/user/images/' . $file);
72+
73+
if ($targetPath === false || !str_starts_with($targetPath, $allowedDir . DIRECTORY_SEPARATOR)) {
74+
return $this->json(['error' => 'Invalid file path'], Response::HTTP_BAD_REQUEST);
75+
}
6376

64-
if (file_exists($file)) {
65-
unlink($file);
77+
if (file_exists($targetPath)) {
78+
unlink($targetPath);
6679
}
6780

6881
$response = [

phpmyfaq/src/phpMyFAQ/Filter.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,16 @@ public static function removeAttributes(string $html = ''): string
171171
// remove broken stuff
172172
$html = str_replace(search: '
', replace: '', subject: $html);
173173

174-
preg_match_all(pattern: '/[a-z]+=".+"/iU', subject: $html, matches: $attributes);
174+
// Match attributes with double quotes, single quotes, or no quotes
175+
preg_match_all(
176+
pattern: '/[a-z]+\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/iU',
177+
subject: $html,
178+
matches: $attributes,
179+
);
175180

176181
foreach ($attributes[0] as $attribute) {
177182
$attributeName = stristr($attribute, needle: '=', before_needle: true);
183+
$attributeName = trim($attributeName);
178184
if (!self::isAttribute($attributeName)) {
179185
continue;
180186
}

tests/phpMyFAQ/FilterTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,40 @@ public function testComplexHtmlFiltering(): void
297297
$this->assertStringNotContainsString('onmouseover', $result);
298298
}
299299

300+
public function testRemoveAttributesWithUnquotedValues(): void
301+
{
302+
$html = '<img src=x onerror=alert(1)>';
303+
$result = Filter::removeAttributes($html);
304+
305+
$this->assertStringNotContainsString('onerror', $result);
306+
}
307+
308+
public function testRemoveAttributesWithSingleQuotedValues(): void
309+
{
310+
$html = "<img src='x' onerror='alert(1)'>";
311+
$result = Filter::removeAttributes($html);
312+
313+
$this->assertStringNotContainsString('onerror', $result);
314+
}
315+
316+
public function testRemoveAttributesWithSvgOnload(): void
317+
{
318+
$html = '<svg onload=alert(1)>';
319+
$result = Filter::removeAttributes($html);
320+
321+
$this->assertStringNotContainsString('onload', $result);
322+
}
323+
324+
public function testRemoveAttributesWithMixedQuoteStyles(): void
325+
{
326+
$html = '<div class="safe" onclick=alert(1) style=\'color:red\' onmouseover="steal()">';
327+
$result = Filter::removeAttributes($html);
328+
329+
$this->assertStringContainsString('class="safe"', $result);
330+
$this->assertStringNotContainsString('onclick', $result);
331+
$this->assertStringNotContainsString('onmouseover', $result);
332+
}
333+
300334
protected function tearDown(): void
301335
{
302336
unset($_GET['test_var'], $_GET['special_test'], $_POST['name'], $_POST['email']);

0 commit comments

Comments
 (0)