Medium severity5.5GHSA Advisory· Published Sep 3, 2025· Updated Apr 15, 2026
CVE-2025-9822
CVE-2025-9822
Description
SummaryA user with administrator rights can change the configuration of the mautic application and extract secrets that are not normally available.
ImpactAn administrator who usually does not have access to certain parameters, such as database credentials, can disclose them.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mautic/corePackagist | >= 4.4.0, < 4.4.17 | 4.4.17 |
mautic/corePackagist | >= 5.0.0-alpha, < 5.2.8 | 5.2.8 |
mautic/corePackagist | >= 6.0.0-alpha, < 6.0.5 | 6.0.5 |
Affected products
1Patches
24 files changed · +209 −1
app/bundles/CoreBundle/Form/Type/ConfigType.php+34 −0 modified@@ -25,8 +25,10 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; +use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -154,6 +156,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'message' => 'mautic.core.value.required', ] ), + new Callback([$this, 'validateImagePath']), ], ] ); @@ -691,6 +694,37 @@ function (FormEvent $event) use ($formModifier): void { ); } + // Validate $value to check ../ and denied system folders + public function validateImagePath(?string $value, ExecutionContextInterface $context): void + { + $isValid = true; + + $normalizedValue = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $value); + + if ( + empty($normalizedValue) + || str_contains($normalizedValue, '..') + || str_contains($normalizedValue, '.'.DIRECTORY_SEPARATOR) + || DIRECTORY_SEPARATOR === $normalizedValue + ) { + $isValid = false; + } + + $mediaFile = substr($value, 0, 6); + + if ('media/' !== $mediaFile) { + $isValid = false; + } + + if (!is_dir($value)) { + $isValid = false; + } + + if (!$isValid) { + $context->buildViolation('mautic.core.config.form.image.path.invalid')->atPath('image_path')->addViolation(); + } + } + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['ipLookupAttribution'] = (null !== $this->ipLookup) ? $this->ipLookup->getAttribution() : '';
app/bundles/CoreBundle/Tests/Functional/EventListener/ConfigSubscriberTest.php+171 −0 added@@ -0,0 +1,171 @@ +<?php + +declare(strict_types=1); + +namespace Mautic\CoreBundle\Tests\Functional\EventListener; + +use Mautic\CoreBundle\Test\MauticMysqlTestCase; +use PHPUnit\Framework\Assert; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpFoundation\Request; + +class ConfigSubscriberTest extends MauticMysqlTestCase +{ + protected $useCleanupRollback = false; + + protected string $prefix = ''; + + protected function setUp(): void + { + $this->configParams['config_allowed_parameters'] = [ + 'kernel.root_dir', + 'kernel.project_dir', + ]; + + $this->configParams['locale'] = 'en_US'; + + parent::setUp(); + + $this->prefix = MAUTIC_TABLE_PREFIX; + + $configPath = $this->getConfigPath(); + if (file_exists($configPath)) { + // backup original local.php + copy($configPath, $configPath.'.backup'); + } else { + // write a temporary local.php + file_put_contents($configPath, '<?php $parameters = [];'); + } + } + + protected function beforeTearDown(): void + { + if (file_exists($this->getConfigPath().'.backup')) { + // restore original local.php + rename($this->getConfigPath().'.backup', $this->getConfigPath()); + } else { + // local.php didn't exist to start with so delete + unlink($this->getConfigPath()); + } + } + + private function getConfigPath(): string + { + return self::getContainer()->get('kernel')->getLocalConfigFile(); + } + + public function testFailConfigMediaPathWithDots(): void + { + $crawler = $this->setImagePathRequest('media/..'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('..'); + Assert::assertStringContainsString('The image path is invalid', $crawler->text()); + + $crawler = $this->setImagePathRequest('...'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('./'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('./../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + } + + public function testFailConfigMediaPathWithSystemDirectories(): void + { + $crawler = $this->setImagePathRequest('app/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\..'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app/../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\..\\'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('bin'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('bin/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('themes'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + } + + public function testFoldersThatDontExist(): void + { + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist/this-folder-does-not-exist'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist/this-folder-does-not-exist/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + } + + public function testValidFolders(): void + { + $crawler = $this->setImagePathRequest('media/'); + Assert::assertStringNotContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/files/'); + Assert::assertStringNotContainsString('The image path is invalid.', $crawler->text()); + + $newFolder = $this->getContainer()->getParameter('mautic.image_path').'/../../media/newFolder'; + + $crawler = $this->setImagePathRequest('media/newFolder'); + + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + if (!file_exists($newFolder)) { + mkdir($newFolder, 0777, true); + } + + $crawler = $this->setImagePathRequest('media/newFolder'); + + Assert::assertStringNotContainsString('The image path is invalid.', $crawler->text()); + + if (is_dir($newFolder)) { + rmdir($newFolder); + } + } + + private function setImagePathRequest(string $value): Crawler + { + $crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit'); + Assert::assertTrue($this->client->getResponse()->isOk()); + + // Find save & close button + $buttonCrawler = $crawler->selectButton('config[buttons][save]'); + $form = $buttonCrawler->form(); + $form->setValues( + [ + 'config[coreconfig][site_url]' => 'https://mautic-community.local', // required + 'config[leadconfig][contact_columns]' => ['name', 'email', 'id'], + 'config[coreconfig][image_path]' => $value, + ] + ); + + $crawler = $this->client->submit($form); + Assert::assertSame(200, $this->client->getResponse()->getStatusCode()); + + return $crawler; + } +}
app/bundles/CoreBundle/Tests/Unit/Form/Type/ConfigTypeTest.php+1 −1 modified@@ -37,7 +37,7 @@ public function testSubmitValidData(): void 'site_url' => 'http://example.com', 'cache_path' => 'tmp', 'log_path' => '/var/log', - 'image_path' => '/tmp/sample-image.png', + 'image_path' => 'media/', 'cached_data_timeout' => 30000, 'date_format_full' => 'F j, Y g:i:s a T', 'date_format_short' => 'D, M d - g:i:s a',
app/bundles/CoreBundle/Translations/en_US/validators.ini+3 −0 modified@@ -19,3 +19,6 @@ mautic.core.invalid_file_type="Invalid file type {{ type }}. Use a file that mat mautic.core.invalid_file_encoding="The file is not encoded correctly into UTF-8." mautic.core.not.allowed.file.extension="%extension% is not an allowed file extension" mautic.core.regex.invalid="The regex syntax is invalid." +mautic.core.config.form.image.path.invalid="The image path is invalid." +mautic.core.config.form.image.path.invalid.not.media="The image path is invalid. It must begin with media/." +mautic.core.config.form.image.path.invalid.folder.not.exists="The image path is invalid. The folder does not exist."
4 files changed · +209 −1
app/bundles/CoreBundle/Form/Type/ConfigType.php+34 −0 modified@@ -25,8 +25,10 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; +use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -154,6 +156,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'message' => 'mautic.core.value.required', ] ), + new Callback([$this, 'validateImagePath']), ], ] ); @@ -691,6 +694,37 @@ function (FormEvent $event) use ($formModifier): void { ); } + // Validate $value to check ../ and denied system folders + public function validateImagePath(?string $value, ExecutionContextInterface $context): void + { + $isValid = true; + + $normalizedValue = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $value); + + if ( + empty($normalizedValue) + || str_contains($normalizedValue, '..') + || str_contains($normalizedValue, '.'.DIRECTORY_SEPARATOR) + || DIRECTORY_SEPARATOR === $normalizedValue + ) { + $isValid = false; + } + + $mediaFile = substr($value, 0, 6); + + if ('media/' !== $mediaFile) { + $isValid = false; + } + + if (!is_dir($value)) { + $isValid = false; + } + + if (!$isValid) { + $context->buildViolation('mautic.core.config.form.image.path.invalid')->atPath('image_path')->addViolation(); + } + } + public function buildView(FormView $view, FormInterface $form, array $options): void { $view->vars['ipLookupAttribution'] = (null !== $this->ipLookup) ? $this->ipLookup->getAttribution() : '';
app/bundles/CoreBundle/Tests/Functional/EventListener/ConfigSubscriberTest.php+171 −0 added@@ -0,0 +1,171 @@ +<?php + +declare(strict_types=1); + +namespace Mautic\CoreBundle\Tests\Functional\EventListener; + +use Mautic\CoreBundle\Test\MauticMysqlTestCase; +use PHPUnit\Framework\Assert; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpFoundation\Request; + +class ConfigSubscriberTest extends MauticMysqlTestCase +{ + protected $useCleanupRollback = false; + + protected string $prefix = ''; + + protected function setUp(): void + { + $this->configParams['config_allowed_parameters'] = [ + 'kernel.root_dir', + 'kernel.project_dir', + ]; + + $this->configParams['locale'] = 'en_US'; + + parent::setUp(); + + $this->prefix = MAUTIC_TABLE_PREFIX; + + $configPath = $this->getConfigPath(); + if (file_exists($configPath)) { + // backup original local.php + copy($configPath, $configPath.'.backup'); + } else { + // write a temporary local.php + file_put_contents($configPath, '<?php $parameters = [];'); + } + } + + protected function beforeTearDown(): void + { + if (file_exists($this->getConfigPath().'.backup')) { + // restore original local.php + rename($this->getConfigPath().'.backup', $this->getConfigPath()); + } else { + // local.php didn't exist to start with so delete + unlink($this->getConfigPath()); + } + } + + private function getConfigPath(): string + { + return self::$container->get('kernel')->getLocalConfigFile(); + } + + public function testFailConfigMediaPathWithDots(): void + { + $crawler = $this->setImagePathRequest('media/..'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('..'); + Assert::assertStringContainsString('The image path is invalid', $crawler->text()); + + $crawler = $this->setImagePathRequest('...'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('./'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('./../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + } + + public function testFailConfigMediaPathWithSystemDirectories(): void + { + $crawler = $this->setImagePathRequest('app/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\..'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app/../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\../'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('app\\..\\'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('bin'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('bin/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('themes'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + } + + public function testFoldersThatDontExist(): void + { + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist/this-folder-does-not-exist'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/this-folder-does-not-exist/this-folder-does-not-exist/'); + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + } + + public function testValidFolders(): void + { + $crawler = $this->setImagePathRequest('media/'); + Assert::assertStringNotContainsString('The image path is invalid.', $crawler->text()); + + $crawler = $this->setImagePathRequest('media/files/'); + Assert::assertStringNotContainsString('The image path is invalid.', $crawler->text()); + + $newFolder = $this->getContainer()->getParameter('mautic.image_path').'/../../media/newFolder'; + + $crawler = $this->setImagePathRequest('media/newFolder'); + + Assert::assertStringContainsString('The image path is invalid.', $crawler->text()); + + if (!file_exists($newFolder)) { + mkdir($newFolder, 0777, true); + } + + $crawler = $this->setImagePathRequest('media/newFolder'); + + Assert::assertStringNotContainsString('The image path is invalid.', $crawler->text()); + + if (is_dir($newFolder)) { + rmdir($newFolder); + } + } + + private function setImagePathRequest(string $value): Crawler + { + $crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit'); + Assert::assertTrue($this->client->getResponse()->isOk()); + + // Find save & close button + $buttonCrawler = $crawler->selectButton('config[buttons][save]'); + $form = $buttonCrawler->form(); + $form->setValues( + [ + 'config[coreconfig][site_url]' => 'https://mautic-community.local', // required + 'config[leadconfig][contact_columns]' => ['name', 'email', 'id'], + 'config[coreconfig][image_path]' => $value, + ] + ); + + $crawler = $this->client->submit($form); + Assert::assertSame(200, $this->client->getResponse()->getStatusCode()); + + return $crawler; + } +} \ No newline at end of file
app/bundles/CoreBundle/Tests/Unit/Form/Type/ConfigTypeTest.php+1 −1 modified@@ -37,7 +37,7 @@ public function testSubmitValidData(): void 'site_url' => 'http://example.com', 'cache_path' => 'tmp', 'log_path' => '/var/log', - 'image_path' => '/tmp/sample-image.png', + 'image_path' => 'media/', 'cached_data_timeout' => 30000, 'date_format_full' => 'F j, Y g:i:s a T', 'date_format_short' => 'D, M d - g:i:s a',
app/bundles/CoreBundle/Translations/en_US/validators.ini+3 −0 modified@@ -19,3 +19,6 @@ mautic.core.invalid_file_type="Invalid file type {{ type }}. Use a file that mat mautic.core.invalid_file_encoding="The file is not encoded correctly into UTF-8." mautic.core.not.allowed.file.extension="%extension% is not an allowed file extension" mautic.core.regex.invalid="The regex syntax is invalid." +mautic.core.config.form.image.path.invalid="The image path is invalid." +mautic.core.config.form.image.path.invalid.not.media="The image path is invalid. It must begin with media/." +mautic.core.config.form.image.path.invalid.folder.not.exists="The image path is invalid. The folder does not exist."
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-438m-6mhw-hq5wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-9822ghsaADVISORY
- github.com/mautic/mautic/commit/882c2c5be646e36f7b91e7c4b24f71aafa617cd5ghsaWEB
- github.com/mautic/mautic/commit/a310b1933de7cfefec03382a4d8c0d9dbbaa0600ghsaWEB
- github.com/mautic/mautic/security/advisories/GHSA-438m-6mhw-hq5wnvdWEB
News mentions
0No linked articles in our index yet.