VYPR
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.

PackageAffected versionsPatched versions
mautic/corePackagist
>= 4.4.0, < 4.4.174.4.17
mautic/corePackagist
>= 5.0.0-alpha, < 5.2.85.2.8
mautic/corePackagist
>= 6.0.0-alpha, < 6.0.56.0.5

Affected products

1

Patches

2
882c2c5be646

Merge commit from fork

https://github.com/mautic/mauticZdeno KuzmanySep 2, 2025via ghsa
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::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."
    
a310b1933de7

mst-75

https://github.com/mautic/mauticlenonleiteAug 8, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.