CVE-2024-28235: Insufficient Information (NVD-CWE-noinfo)
value: 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-09T16:15Z
Repository: https://github.com/contao/contao
Language: PHP
Files changed: core-bundle/config/services.yaml, core-bundle/contao/classes/Crawl.php, core-bundle/src/Crawl/Escargot/Factory.php, core-bundle/src/DependencyInjection/ContaoCoreExtension.php, core-bundle/tests/Crawl/Escargot/FactoryTest.php, core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php
Commit date: 2024-04-09 07:19:52 +0200
Commit message: Merge pull request from GHSA-9jh5-qf84-x6pr

* Ensure confidential headers are not leaked to external URIs when crawling

* CS

---------

Co-authored-by: Leo Feyer <1192057+leofeyer@users.noreply.github.com>
Diff: diff --git a/core-bundle/config/services.yaml b/core-bundle/config/services.yaml
index de537dd7f8c..f86df2aebf1 100644
--- a/core-bundle/config/services.yaml
+++ b/core-bundle/config/services.yaml
@@ -74,10 +74,11 @@ services:
         public: true
         arguments:
             - '@database_connection'
             - '@contao.framework'
             - '@contao.routing.content_url_generator'
+            - '@request_stack'
 
     contao.crawl.escargot.search_index_subscriber:
         class: Contao\CoreBundle\Crawl\Escargot\Subscriber\SearchIndexSubscriber
         arguments:
             - '@contao.search.indexer'
diff --git a/core-bundle/contao/classes/Crawl.php b/core-bundle/contao/classes/Crawl.php
index a4c5a5987af..e255199488a 100644
--- a/core-bundle/contao/classes/Crawl.php
+++ b/core-bundle/contao/classes/Crawl.php
@@ -126,16 +126,13 @@ class Crawl extends Backend implements MaintenanceModuleInterface
 				$objAuthenticator->removeFrontendAuthentication();
 				$clientOptions = array();
 			}
 			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
 		{
 			$objAuthenticator->removeFrontendAuthentication();
diff --git a/core-bundle/src/Crawl/Escargot/Factory.php b/core-bundle/src/Crawl/Escargot/Factory.php
index 565498c1b00..39f1e2690cd 100644
--- a/core-bundle/src/Crawl/Escargot/Factory.php
+++ b/core-bundle/src/Crawl/Escargot/Factory.php
@@ -16,11 +16,14 @@ use Contao\CoreBundle\Crawl\Escargot\Subscriber\EscargotSubscriberInterface;
 use Contao\CoreBundle\Framework\ContaoFramework;
 use Contao\CoreBundle\Routing\ContentUrlGenerator;
 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;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 use Terminal42\Escargot\BaseUriCollection;
@@ -41,19 +44,28 @@ class Factory
      * @var array<EscargotSubscriberInterface>
      */
     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
     {
         $this->subscribers[] = $subscriber;
@@ -160,22 +172,81 @@ class Factory
         return $escargot;
     }
 
     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
     {
         $escargot->addSubscriber(new RobotsSubscriber());
diff --git a/core-bundle/src/DependencyInjection/ContaoCoreExtension.php b/core-bundle/src/DependencyInjection/ContaoCoreExtension.php
index ef783498344..55db388f882 100644
--- a/core-bundle/src/DependencyInjection/ContaoCoreExtension.php
+++ b/core-bundle/src/DependencyInjection/ContaoCoreExtension.php
@@ -320,12 +320,12 @@ class ContaoCoreExtension extends Extension implements PrependExtensionInterface
         if (!$container->hasDefinition('contao.crawl.escargot.factory')) {
             return;
         }
 
         $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']);
     }
 
     /**
      * Validates and sets the "contao.image.sizes" parameter.
      */
diff --git a/core-bundle/tests/Crawl/Escargot/FactoryTest.php b/core-bundle/tests/Crawl/Escargot/FactoryTest.php
index c4e1693b8c5..6a1900e9d5f 100644
--- a/core-bundle/tests/Crawl/Escargot/FactoryTest.php
+++ b/core-bundle/tests/Crawl/Escargot/FactoryTest.php
@@ -17,10 +17,14 @@ use Contao\CoreBundle\Crawl\Escargot\Subscriber\EscargotSubscriberInterface;
 use Contao\CoreBundle\Routing\ContentUrlGenerator;
 use Contao\CoreBundle\Tests\TestCase;
 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;
 
 class FactoryTest extends TestCase
@@ -37,11 +41,11 @@ class FactoryTest extends TestCase
         $subscriber2
             ->method('getName')
             ->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);
 
         $this->assertCount(2, $factory->getSubscribers());
         $this->assertCount(2, $factory->getSubscribers(['subscriber-1', 'subscriber-2']));
@@ -69,10 +73,11 @@ class FactoryTest extends TestCase
 
         $factory = new Factory(
             $this->createMock(Connection::class),
             $this->mockContaoFramework([PageModel::class => $pageModelAdapter]),
             $urlGenerator,
+            new RequestStack(),
             ['https://example.com'],
         );
 
         $this->assertCount(1, $factory->getAdditionalCrawlUriCollection());
         $this->assertTrue($factory->getAdditionalCrawlUriCollection()->containsHost('example.com'));
@@ -91,11 +96,11 @@ class FactoryTest extends TestCase
         $subscriber1
             ->method('getName')
             ->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')]);
         $escargot = $factory->create($uriCollection, new InMemoryQueue(), ['subscriber-1']);
 
@@ -114,11 +119,11 @@ class FactoryTest extends TestCase
         $subscriber1
             ->method('getName')
             ->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();
         $jobId = $queue->createJobId(new BaseUriCollection([new Uri('https://contao.org')]));
 
@@ -130,6 +135,83 @@ class FactoryTest extends TestCase
         $this->expectExceptionMessage('You have to specify at least one valid subscriber name. Valid subscribers are: subscriber-1');
 
         $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());
+    }
 }
diff --git a/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php b/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php
index 07bacac3676..1ad708a7aed 100644
--- a/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php
+++ b/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php
@@ -275,12 +275,12 @@ class ContaoCoreExtensionTest extends TestCase
             $container,
         );
 
         $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
     {
         $container = $this->getContainerBuilder();

Vulnerable code: 
				// 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();
     * @param array<string> $additionalUris
        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
                array_merge_recursive($this->getDefaultHttpClientOptions(), $options),
            ),
        $factory->setArgument(3, $config['crawl']['additional_uris']);
        $factory->setArgument(4, $config['crawl']['default_http_client_options']);
        $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));
        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class));
        $this->assertSame(['https://example.com'], $definition->getArgument(3));
        $this->assertSame(['proxy' => 'http://localhost:7080', 'headers' => ['Foo' => 'Bar']], $definition->getArgument(4));
Fixed code:             - '@request_stack'
				// TODO: we need a way to authenticate with a token instead of our own cookie
use Psr\Http\Message\UriInterface;
use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Component\HttpFoundation\RequestStack;
     * @var \Closure(array<string, mixed>): HttpClientInterface|null
     */
    private readonly \Closure|null $httpClientFactory;

    /**
     * @param array<string>                                            $additionalUris
     * @param \Closure(array<string, mixed>): HttpClientInterface|null $httpClientFactory
        private readonly RequestStack $requestStack,
        \Closure|null $httpClientFactory = null,
        $this->httpClientFactory = $httpClientFactory ?? static fn (array $defaultOptions) => HttpClient::create($defaultOptions);
        $options = 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
            ],
            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;
        $factory->setArgument(4, $config['crawl']['additional_uris']);
        $factory->setArgument(5, $config['crawl']['default_http_client_options']);
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Component\HttpFoundation\RequestStack;
        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class), new RequestStack());
            new RequestStack(),
        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class), new RequestStack());
        $factory = new Factory($this->createMock(Connection::class), $this->mockContaoFramework(), $this->createMock(ContentUrlGenerator::class), new RequestStack());

    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());
    }
        $this->assertSame(['https://example.com'], $definition->getArgument(4));
        $this->assertSame(['proxy' => 'http://localhost:7080', 'headers' => ['Foo' => 'Bar']], $definition->getArgument(5));