VYPR
High severityNVD Advisory· Published Apr 9, 2024· Updated Aug 2, 2024

Contao possible cookie sharing with external domains while checking protected pages for broken links

CVE-2024-28235

Description

Contao is an open source content management system. Starting in version 4.9.0 and prior to versions 4.13.40 and 5.3.4, when checking for broken links on protected pages, Contao sends the cookie header to external urls as well, the passed options for the http client are used for all requests. Contao versions 4.13.40 and 5.3.4 have a patch for this issue. As a workaround, disable crawling protected pages.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Contao CMS leaks session cookies to external URLs during broken link checking on protected pages, exposing authenticated sessions.

Vulnerability

Overview

CVE-2024-28235 is a session cookie disclosure vulnerability in the Contao CMS, affecting versions from 4.9.0 up to (but not including) 4.13.40 and 5.3.4 [1][2]. The flaw resides in the broken link checker (crawler), which, when configured to crawl protected pages, reuses the same HTTP client options—including the Cookie header—for all requests [1][2]. As a result, the user's authentication cookies are transmitted to external URLs that are checked for broken links, potentially exposing active session tokens to third-party servers [2].

Attack

Vector and Prerequisites

An attacker can exploit this vulnerability by crafting a malicious external link that is checked by the Contao crawler. No authentication is required on the attacker's part, but the Contao installation must have the broken link checker enabled and configured to include protected pages in its scans [2]. The victim is the Contao administrator or user who triggers the broken link check (either manually or via a scheduled task). The cookie leakage occurs silently in the background without user interaction beyond initiating the scan.

Impact

If an attacker controls an external URL that the crawler visits, the attacker's server can capture the Contao session cookie from the HTTP request headers. This cookie can then be used to impersonate the user (likely an administrator) whose session was active, enabling unauthorized access to the Contao backend [2]. The impact is high, as it can lead to full compromise of the CMS instance, including data theft, content manipulation, and privilege escalation.

Mitigation

Status

Contao has released patches in versions 4.13.40 and 5.3.4 that fix the issue by introducing a ScopingHttpClient to restrict cookie sending to the appropriate scope [1][3]. Administrators should upgrade to these versions immediately. As a workaround, users can disable crawling of protected pages until a patch can be applied [2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
contao/core-bundlePackagist
>= 4.9.0, < 4.13.404.13.40
contao/core-bundlePackagist
>= 5.0.0-RC1, < 5.3.45.3.4

Affected products

2

Patches

2
73a2770e2d35

Merge pull request from GHSA-9jh5-qf84-x6pr

https://github.com/contao/contaoYanick WitschiApr 9, 2024via ghsa
6 files changed · +174 22
  • core-bundle/src/Crawl/Escargot/Factory.php+81 11 modified
    @@ -17,7 +17,10 @@
     use Contao\PageModel;
     use Doctrine\DBAL\Connection;
     use Nyholm\Psr7\Uri;
    +use Psr\Http\Message\UriInterface;
     use Symfony\Component\HttpClient\HttpClient;
    +use Symfony\Component\HttpClient\ScopingHttpClient;
    +use Symfony\Component\HttpFoundation\RequestStack;
     use Symfony\Component\Uid\Uuid;
     use Symfony\Contracts\HttpClient\HttpClientInterface;
     use Terminal42\Escargot\BaseUriCollection;
    @@ -36,6 +39,7 @@ class Factory
     
         private Connection $connection;
         private ContaoFramework $framework;
    +    private RequestStack $requestStack;
         private array $defaultHttpClientOptions;
     
         /**
    @@ -48,12 +52,19 @@ class Factory
          */
         private array $subscribers = [];
     
    -    public function __construct(Connection $connection, ContaoFramework $framework, array $additionalUris = [], array $defaultHttpClientOptions = [])
    +    /**
    +     * @var \Closure(array<string, mixed>): HttpClientInterface|null
    +     */
    +    private ?\Closure $httpClientFactory;
    +
    +    public function __construct(Connection $connection, ContaoFramework $framework, RequestStack $requestStack, array $additionalUris = [], array $defaultHttpClientOptions = [], \Closure $httpClientFactory = null)
         {
             $this->connection = $connection;
             $this->framework = $framework;
    +        $this->requestStack = $requestStack;
             $this->additionalUris = $additionalUris;
             $this->defaultHttpClientOptions = $defaultHttpClientOptions;
    +        $this->httpClientFactory = $httpClientFactory ?? static fn (array $defaultOptions) => HttpClient::create($defaultOptions);
         }
     
         public function addSubscriber(EscargotSubscriberInterface $subscriber): self
    @@ -163,18 +174,77 @@ public function createFromJobId(string $jobId, QueueInterface $queue, array $sel
     
         private function createHttpClient(array $options = []): HttpClientInterface
         {
    -        return HttpClient::create(
    -            array_merge_recursive(
    -                [
    -                    'headers' => [
    -                        'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    -                        'user-agent' => self::USER_AGENT,
    -                    ],
    -                    'max_duration' => 10, // Ignore requests that take longer than 10 seconds
    +        $options = array_merge_recursive(
    +            [
    +                'headers' => [
    +                    'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    +                    'user-agent' => self::USER_AGENT,
                     ],
    -                array_merge_recursive($this->getDefaultHttpClientOptions(), $options)
    -            )
    +                'max_duration' => 10, // Ignore requests that take longer than 10 seconds
    +            ],
    +            array_merge_recursive($this->getDefaultHttpClientOptions(), $options)
             );
    +
    +        // Make sure confidential headers force a scoped client so external domains do not leak data
    +        $cleanOptions = $this->cleanOptionsFromConfidentialData($options);
    +
    +        if ($options === $cleanOptions) {
    +            return ($this->httpClientFactory)($options);
    +        }
    +
    +        $scopedOptionsByRegex = [];
    +
    +        // All options including the confidential headers for our root page collection
    +        foreach ($this->getRootPageUriCollection()->all() as $rootPageUri) {
    +            $scopedOptionsByRegex[preg_quote($this->getOriginFromUri($rootPageUri))] = $options;
    +        }
    +
    +        // Closing the session is necessary here as otherwise we might run into our own session lock
    +        $request = $this->requestStack->getMainRequest();
    +
    +        if ($request && $request->hasSession()) {
    +            $request->getSession()->save();
    +        }
    +
    +        return new ScopingHttpClient(($this->httpClientFactory)($cleanOptions), $scopedOptionsByRegex);
    +    }
    +
    +    private function getOriginFromUri(UriInterface $uri): string
    +    {
    +        $origin = $uri->getScheme().'://'.$uri->getHost();
    +
    +        if ($uri->getPort()) {
    +            $origin .= ':'.$uri->getPort();
    +        }
    +
    +        return $origin.'/';
    +    }
    +
    +    private function cleanOptionsFromConfidentialData(array $options): array
    +    {
    +        $cleanOptions = [];
    +
    +        foreach ($options as $k => $v) {
    +            if ('headers' === $k) {
    +                foreach ($v as $header => $value) {
    +                    if (\in_array(strtolower($header), ['authorization', 'cookie'], true)) {
    +                        continue;
    +                    }
    +
    +                    $cleanOptions['headers'][$header] = $value;
    +                }
    +
    +                continue;
    +            }
    +
    +            if ('basic_auth' === $k || 'bearer_auth' === $k) {
    +                continue;
    +            }
    +
    +            $cleanOptions[$k] = $v;
    +        }
    +
    +        return $cleanOptions;
         }
     
         private function registerDefaultSubscribers(Escargot $escargot): void
    
  • core-bundle/src/DependencyInjection/ContaoCoreExtension.php+2 2 modified
    @@ -277,8 +277,8 @@ private function handleCrawlConfig(array $config, ContainerBuilder $container):
             }
     
             $factory = $container->getDefinition('contao.crawl.escargot.factory');
    -        $factory->setArgument(2, $config['crawl']['additional_uris']);
    -        $factory->setArgument(3, $config['crawl']['default_http_client_options']);
    +        $factory->setArgument(3, $config['crawl']['additional_uris']);
    +        $factory->setArgument(4, $config['crawl']['default_http_client_options']);
         }
     
         /**
    
  • core-bundle/src/Resources/config/services.yml+1 0 modified
    @@ -82,6 +82,7 @@ services:
             arguments:
                 - '@database_connection'
                 - '@contao.framework'
    +            - '@request_stack'
     
         contao.crawl.escargot.search_index_subscriber:
             class: Contao\CoreBundle\Crawl\Escargot\Subscriber\SearchIndexSubscriber
    
  • core-bundle/src/Resources/contao/classes/Crawl.php+1 4 modified
    @@ -125,12 +125,9 @@ public function run()
     			}
     			else
     			{
    +				// TODO: we need a way to authenticate with a token instead of our own cookie
     				$session = System::getContainer()->get('session');
     				$clientOptions = array('headers' => array('Cookie' => sprintf('%s=%s', $session->getName(), $session->getId())));
    -
    -				// Closing the session is necessary here as otherwise we run into our own session lock
    -				// TODO: we need a way to authenticate with a token instead of our own cookie
    -				$session->save();
     			}
     		}
     		else
    
  • core-bundle/tests/Crawl/Escargot/FactoryTest.php+87 3 modified
    @@ -18,6 +18,10 @@
     use Contao\PageModel;
     use Doctrine\DBAL\Connection;
     use Nyholm\Psr7\Uri;
    +use Symfony\Component\HttpClient\MockHttpClient;
    +use Symfony\Component\HttpClient\Response\MockResponse;
    +use Symfony\Component\HttpClient\ScopingHttpClient;
    +use Symfony\Component\HttpFoundation\RequestStack;
     use Terminal42\Escargot\BaseUriCollection;
     use Terminal42\Escargot\Queue\InMemoryQueue;
     
    @@ -37,7 +41,7 @@ public function testHandlesSubscribersCorrectly(): void
                 ->willReturn('subscriber-2')
             ;
     
    -        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework());
    +        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), new RequestStack());
             $factory->addSubscriber($subscriber1);
             $factory->addSubscriber($subscriber2);
     
    @@ -65,6 +69,7 @@ public function testBuildsUriCollectionsCorrectly(): void
             $factory = new Factory(
                 $this->createMock(Connection::class),
                 $this->mockContaoFramework([PageModel::class => $pageModelAdapter]),
    +            new RequestStack(),
                 ['https://example.com']
             );
     
    @@ -87,7 +92,7 @@ public function testCreatesEscargotCorrectlyWithNewJobId(): void
                 ->willReturn('subscriber-1')
             ;
     
    -        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework());
    +        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), new RequestStack());
             $factory->addSubscriber($subscriber1);
     
             $uriCollection = new BaseUriCollection([new Uri('https://contao.org')]);
    @@ -110,7 +115,7 @@ public function testCreatesEscargotCorrectlyWithExistingJobId(): void
                 ->willReturn('subscriber-1')
             ;
     
    -        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework());
    +        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), new RequestStack());
             $factory->addSubscriber($subscriber1);
     
             $queue = new InMemoryQueue();
    @@ -126,4 +131,83 @@ public function testCreatesEscargotCorrectlyWithExistingJobId(): void
             $escargot = $factory->createFromJobId($jobId, $queue, ['subscriber-8']);
             $this->assertSame(Factory::USER_AGENT, $escargot->getUserAgent());
         }
    +
    +    public function testScopesConfidentialHeadersAutomatically(): void
    +    {
    +        $expectedRequests = [
    +            function (string $method, string $url, array $options): MockResponse {
    +                $this->assertSame('GET', $method);
    +                $this->assertSame('https://contao.org/robots.txt', $url);
    +                $this->assertContains('Cookie: Confidential', $options['headers']);
    +                $this->assertContains('Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $options['headers']);
    +
    +                return new MockResponse();
    +            },
    +            function (string $method, string $url, array $options): MockResponse {
    +                $this->assertSame('GET', $method);
    +                $this->assertSame('https://contao.de/robots.txt', $url);
    +                $this->assertContains('Cookie: Confidential', $options['headers']);
    +                $this->assertContains('Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $options['headers']);
    +
    +                return new MockResponse();
    +            },
    +            function (string $method, string $url, array $options): MockResponse {
    +                $this->assertSame('GET', $method);
    +                $this->assertSame('https://www.foreign-domain.com/robots.txt', $url);
    +                $this->assertNotContains('Cookie: Confidential', $options['headers']);
    +                $this->assertNotContains('Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $options['headers']);
    +
    +                return new MockResponse();
    +            },
    +        ];
    +
    +        $mockClient = new MockHttpClient($expectedRequests);
    +        $clientFactory = static fn (array $defaultOptions) => $mockClient;
    +
    +        $rootPage1 = $this->createMock(PageModel::class);
    +        $rootPage1
    +            ->method('getAbsoluteUrl')
    +            ->willReturn('https://contao.org')
    +        ;
    +
    +        $rootPage2 = $this->createMock(PageModel::class);
    +        $rootPage2
    +            ->method('getAbsoluteUrl')
    +            ->willReturn('https://contao.de')
    +        ;
    +
    +        $pageModelAdapter = $this->mockAdapter(['findPublishedRootPages']);
    +        $pageModelAdapter
    +            ->method('findPublishedRootPages')
    +            ->willReturn([$rootPage1, $rootPage2])
    +        ;
    +
    +        $subscriber1 = $this->createMock(EscargotSubscriberInterface::class);
    +        $subscriber1
    +            ->method('getName')
    +            ->willReturn('subscriber-1')
    +        ;
    +
    +        $factory = new Factory(
    +            $this->createMock(Connection::class),
    +            $this->mockContaoFramework([PageModel::class => $pageModelAdapter]),
    +            new RequestStack(),
    +            ['https://www.foreign-domain.com'],
    +            [
    +                'headers' => [
    +                    'Cookie' => 'Confidential',
    +                ],
    +                'auth_basic' => 'username:password',
    +            ],
    +            $clientFactory
    +        );
    +
    +        $factory->addSubscriber($subscriber1);
    +
    +        $escargot = $factory->create($factory->getCrawlUriCollection(), new InMemoryQueue(), ['subscriber-1']);
    +        $escargot->crawl();
    +
    +        $this->assertSame(3, $mockClient->getRequestsCount());
    +        $this->assertInstanceOf(ScopingHttpClient::class, $escargot->getClient());
    +    }
     }
    
  • core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php+2 2 modified
    @@ -338,8 +338,8 @@ public function testSetsTheCrawlOptionsOnTheEscargotFactory(): void
     
             $definition = $container->getDefinition('contao.crawl.escargot.factory');
     
    -        $this->assertSame(['https://example.com'], $definition->getArgument(2));
    -        $this->assertSame(['proxy' => 'http://localhost:7080', 'headers' => ['Foo' => 'Bar']], $definition->getArgument(3));
    +        $this->assertSame(['https://example.com'], $definition->getArgument(3));
    +        $this->assertSame(['proxy' => 'http://localhost:7080', 'headers' => ['Foo' => 'Bar']], $definition->getArgument(4));
         }
     
         public function testConfiguresTheBackupManagerCorrectly(): void
    
79b7620d01ce

Merge pull request from GHSA-9jh5-qf84-x6pr

https://github.com/contao/contaoYanick WitschiApr 9, 2024via ghsa
6 files changed · +173 22
  • core-bundle/config/services.yaml+1 0 modified
    @@ -76,6 +76,7 @@ services:
                 - '@database_connection'
                 - '@contao.framework'
                 - '@contao.routing.content_url_generator'
    +            - '@request_stack'
     
         contao.crawl.escargot.search_index_subscriber:
             class: Contao\CoreBundle\Crawl\Escargot\Subscriber\SearchIndexSubscriber
    
  • core-bundle/contao/classes/Crawl.php+1 4 modified
    @@ -128,12 +128,9 @@ public function run()
     			}
     			else
     			{
    +				// TODO: we need a way to authenticate with a token instead of our own cookie
     				$session = System::getContainer()->get('request_stack')->getSession();
     				$clientOptions = array('headers' => array('Cookie' => sprintf('%s=%s', $session->getName(), $session->getId())));
    -
    -				// Closing the session is necessary here as otherwise we run into our own session lock
    -				// TODO: we need a way to authenticate with a token instead of our own cookie
    -				$session->save();
     			}
     		}
     		else
    
  • core-bundle/src/Crawl/Escargot/Factory.php+82 11 modified
    @@ -18,7 +18,10 @@
     use Contao\PageModel;
     use Doctrine\DBAL\Connection;
     use Nyholm\Psr7\Uri;
    +use Psr\Http\Message\UriInterface;
     use Symfony\Component\HttpClient\HttpClient;
    +use Symfony\Component\HttpClient\ScopingHttpClient;
    +use Symfony\Component\HttpFoundation\RequestStack;
     use Symfony\Component\Routing\Exception\ExceptionInterface;
     use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
     use Symfony\Component\Uid\Uuid;
    @@ -43,15 +46,24 @@ class Factory
         private array $subscribers = [];
     
         /**
    -     * @param array<string> $additionalUris
    +     * @var \Closure(array<string, mixed>): HttpClientInterface|null
    +     */
    +    private readonly \Closure|null $httpClientFactory;
    +
    +    /**
    +     * @param array<string>                                            $additionalUris
    +     * @param \Closure(array<string, mixed>): HttpClientInterface|null $httpClientFactory
          */
         public function __construct(
             private readonly Connection $connection,
             private readonly ContaoFramework $framework,
             private readonly ContentUrlGenerator $urlGenerator,
    +        private readonly RequestStack $requestStack,
             private readonly array $additionalUris = [],
             private readonly array $defaultHttpClientOptions = [],
    +        \Closure|null $httpClientFactory = null,
         ) {
    +        $this->httpClientFactory = $httpClientFactory ?? static fn (array $defaultOptions) => HttpClient::create($defaultOptions);
         }
     
         public function addSubscriber(EscargotSubscriberInterface $subscriber): self
    @@ -162,18 +174,77 @@ public function createFromJobId(string $jobId, QueueInterface $queue, array $sel
     
         private function createHttpClient(array $options = []): HttpClientInterface
         {
    -        return HttpClient::create(
    -            array_merge_recursive(
    -                [
    -                    'headers' => [
    -                        'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    -                        'user-agent' => self::USER_AGENT,
    -                    ],
    -                    'max_duration' => 10, // Ignore requests that take longer than 10 seconds
    +        $options = array_merge_recursive(
    +            [
    +                'headers' => [
    +                    'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    +                    'user-agent' => self::USER_AGENT,
                     ],
    -                array_merge_recursive($this->getDefaultHttpClientOptions(), $options),
    -            ),
    +                'max_duration' => 10, // Ignore requests that take longer than 10 seconds
    +            ],
    +            array_merge_recursive($this->getDefaultHttpClientOptions(), $options),
             );
    +
    +        // Make sure confidential headers force a scoped client so external domains do
    +        // not leak data
    +        $cleanOptions = $this->cleanOptionsFromConfidentialData($options);
    +
    +        if ($options === $cleanOptions) {
    +            return ($this->httpClientFactory)($options);
    +        }
    +
    +        $scopedOptionsByRegex = [];
    +
    +        // All options including the confidential headers for our root page collection
    +        foreach ($this->getRootPageUriCollection()->all() as $rootPageUri) {
    +            $scopedOptionsByRegex[preg_quote($this->getOriginFromUri($rootPageUri))] = $options;
    +        }
    +
    +        // Closing the session is necessary here as otherwise we might run into our own
    +        // session lock
    +        if ($this->requestStack->getMainRequest()?->hasSession()) {
    +            $this->requestStack->getMainRequest()->getSession()->save();
    +        }
    +
    +        return new ScopingHttpClient(($this->httpClientFactory)($cleanOptions), $scopedOptionsByRegex);
    +    }
    +
    +    private function getOriginFromUri(UriInterface $uri): string
    +    {
    +        $origin = $uri->getScheme().'://'.$uri->getHost();
    +
    +        if ($uri->getPort()) {
    +            $origin .= ':'.$uri->getPort();
    +        }
    +
    +        return $origin.'/';
    +    }
    +
    +    private function cleanOptionsFromConfidentialData(array $options): array
    +    {
    +        $cleanOptions = [];
    +
    +        foreach ($options as $k => $v) {
    +            if ('headers' === $k) {
    +                foreach ($v as $header => $value) {
    +                    if (\in_array(strtolower($header), ['authorization', 'cookie'], true)) {
    +                        continue;
    +                    }
    +
    +                    $cleanOptions['headers'][$header] = $value;
    +                }
    +
    +                continue;
    +            }
    +
    +            if ('basic_auth' === $k || 'bearer_auth' === $k) {
    +                continue;
    +            }
    +
    +            $cleanOptions[$k] = $v;
    +        }
    +
    +        return $cleanOptions;
         }
     
         private function registerDefaultSubscribers(Escargot $escargot): void
    
  • core-bundle/src/DependencyInjection/ContaoCoreExtension.php+2 2 modified
    @@ -322,8 +322,8 @@ private function handleCrawlConfig(array $config, ContainerBuilder $container):
             }
     
             $factory = $container->getDefinition('contao.crawl.escargot.factory');
    -        $factory->setArgument(3, $config['crawl']['additional_uris']);
    -        $factory->setArgument(4, $config['crawl']['default_http_client_options']);
    +        $factory->setArgument(4, $config['crawl']['additional_uris']);
    +        $factory->setArgument(5, $config['crawl']['default_http_client_options']);
         }
     
         /**
    
  • core-bundle/tests/Crawl/Escargot/FactoryTest.php+85 3 modified
    @@ -19,6 +19,10 @@
     use Contao\PageModel;
     use Doctrine\DBAL\Connection;
     use Nyholm\Psr7\Uri;
    +use Symfony\Component\HttpClient\MockHttpClient;
    +use Symfony\Component\HttpClient\Response\MockResponse;
    +use Symfony\Component\HttpClient\ScopingHttpClient;
    +use Symfony\Component\HttpFoundation\RequestStack;
     use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
     use Terminal42\Escargot\BaseUriCollection;
     use Terminal42\Escargot\Queue\InMemoryQueue;
    @@ -39,7 +43,7 @@ public function testHandlesSubscribersCorrectly(): void
                 ->willReturn('subscriber-2')
             ;
     
    -        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class));
    +        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class), new RequestStack());
             $factory->addSubscriber($subscriber1);
             $factory->addSubscriber($subscriber2);
     
    @@ -71,6 +75,7 @@ public function testBuildsUriCollectionsCorrectly(): void
                 $this->createMock(Connection::class),
                 $this->mockContaoFramework([PageModel::class => $pageModelAdapter]),
                 $urlGenerator,
    +            new RequestStack(),
                 ['https://example.com'],
             );
     
    @@ -93,7 +98,7 @@ public function testCreatesEscargotCorrectlyWithNewJobId(): void
                 ->willReturn('subscriber-1')
             ;
     
    -        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class));
    +        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class), new RequestStack());
             $factory->addSubscriber($subscriber1);
     
             $uriCollection = new BaseUriCollection([new Uri('https://contao.org')]);
    @@ -116,7 +121,7 @@ public function testCreatesEscargotCorrectlyWithExistingJobId(): void
                 ->willReturn('subscriber-1')
             ;
     
    -        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class));
    +        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class), new RequestStack());
             $factory->addSubscriber($subscriber1);
     
             $queue = new InMemoryQueue();
    @@ -132,4 +137,81 @@ public function testCreatesEscargotCorrectlyWithExistingJobId(): void
             $escargot = $factory->createFromJobId($jobId, $queue, ['subscriber-8']);
             $this->assertSame(Factory::USER_AGENT, $escargot->getUserAgent());
         }
    +
    +    public function testScopesConfidentialHeadersAutomatically(): void
    +    {
    +        $expectedRequests = [
    +            function (string $method, string $url, array $options): MockResponse {
    +                $this->assertSame('GET', $method);
    +                $this->assertSame('https://contao.org/robots.txt', $url);
    +                $this->assertContains('Cookie: Confidential', $options['headers']);
    +                $this->assertContains('Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $options['headers']);
    +
    +                return new MockResponse();
    +            },
    +            function (string $method, string $url, array $options): MockResponse {
    +                $this->assertSame('GET', $method);
    +                $this->assertSame('https://contao.de/robots.txt', $url);
    +                $this->assertContains('Cookie: Confidential', $options['headers']);
    +                $this->assertContains('Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $options['headers']);
    +
    +                return new MockResponse();
    +            },
    +            function (string $method, string $url, array $options): MockResponse {
    +                $this->assertSame('GET', $method);
    +                $this->assertSame('https://www.foreign-domain.com/robots.txt', $url);
    +                $this->assertNotContains('Cookie: Confidential', $options['headers']);
    +                $this->assertNotContains('Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $options['headers']);
    +
    +                return new MockResponse();
    +            },
    +        ];
    +
    +        $mockClient = new MockHttpClient($expectedRequests);
    +        $clientFactory = static fn (array $defaultOptions) => $mockClient;
    +
    +        $rootPage1 = $this->mockClassWithProperties(PageModel::class, ['dns' => 'contao.org']);
    +        $rootPage2 = $this->mockClassWithProperties(PageModel::class, ['dns' => 'contao.de']);
    +
    +        $urlGenerator = $this->createMock(ContentUrlGenerator::class);
    +        $urlGenerator
    +            ->method('generate')
    +            ->willReturnCallback(static fn (PageModel $rootPage) => 'https://'.$rootPage->dns)
    +        ;
    +
    +        $pageModelAdapter = $this->mockAdapter(['findPublishedRootPages']);
    +        $pageModelAdapter
    +            ->method('findPublishedRootPages')
    +            ->willReturn([$rootPage1, $rootPage2])
    +        ;
    +
    +        $subscriber1 = $this->createMock(EscargotSubscriberInterface::class);
    +        $subscriber1
    +            ->method('getName')
    +            ->willReturn('subscriber-1')
    +        ;
    +
    +        $factory = new Factory(
    +            $this->createMock(Connection::class),
    +            $this->mockContaoFramework([PageModel::class => $pageModelAdapter]),
    +            $urlGenerator,
    +            new RequestStack(),
    +            ['https://www.foreign-domain.com'],
    +            [
    +                'headers' => [
    +                    'Cookie' => 'Confidential',
    +                ],
    +                'auth_basic' => 'username:password',
    +            ],
    +            $clientFactory,
    +        );
    +
    +        $factory->addSubscriber($subscriber1);
    +
    +        $escargot = $factory->create($factory->getCrawlUriCollection(), new InMemoryQueue(), ['subscriber-1']);
    +        $escargot->crawl();
    +
    +        $this->assertSame(3, $mockClient->getRequestsCount());
    +        $this->assertInstanceOf(ScopingHttpClient::class, $escargot->getClient());
    +    }
     }
    
  • core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php+2 2 modified
    @@ -277,8 +277,8 @@ public function testSetsTheCrawlOptionsOnTheEscargotFactory(): void
     
             $definition = $container->getDefinition('contao.crawl.escargot.factory');
     
    -        $this->assertSame(['https://example.com'], $definition->getArgument(3));
    -        $this->assertSame(['proxy' => 'http://localhost:7080', 'headers' => ['Foo' => 'Bar']], $definition->getArgument(4));
    +        $this->assertSame(['https://example.com'], $definition->getArgument(4));
    +        $this->assertSame(['proxy' => 'http://localhost:7080', 'headers' => ['Foo' => 'Bar']], $definition->getArgument(5));
         }
     
         public function testConfiguresTheBackupManagerCorrectly(): void
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.