diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b7b573d..231dfc58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,9 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.deps }}-composer - name: Install dependencies - run: composer update --no-ansi --no-interaction --prefer-${{ matrix.deps }} + run: | + composer config audit.block-insecure false + composer update --no-ansi --no-interaction --no-audit --prefer-${{ matrix.deps }} - name: Run tests run: composer test diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a20d36c..cb28b471 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## Unreleased +### Fix +- Secure proxy endpoint + ## [v5.4.0] - 2025.03.05 ### Add - Add possibility to disable Web Components integration per sales channel diff --git a/README.md b/README.md index 4c8065dd..fd951aa0 100755 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ modifications in order to fit their needs. For more advanced features please che - [Split ASN on Category Page](#split-asn-on-category-page) - [Set custom Field Roles](#set-custom-field-roles) - [Enrich data received from FACT-Finder in ProxyController](#enrich-data-received-from-fact-finder-in-proxycontroller) +- [Troubleshooting](#troubleshooting) - [Contribute](#contribute) - [License](#license) @@ -55,12 +56,12 @@ modifications in order to fit their needs. For more advanced features please che - Shopware 6.5 - PHP version: 8.1, 8.2 or 8.3 -For Shopware 6.4 please use SDK version 4.x: -https://github.com/FACT-Finder-Web-Components/shopware6-plugin/tree/release/4.x - For Shopware 6.6 please use SDK version 6.x: https://github.com/FACT-Finder-Web-Components/shopware6-plugin/tree/release/6.x +For Shopware 6.7 please use SDK version 7.x: +https://github.com/FACT-Finder-Web-Components/shopware6-plugin/tree/release/7.x + ## FACT-Finder® Supported Sections Version | Compatibility @@ -647,7 +648,21 @@ class EnrichProxyDataEventSubscriber implements EventSubscriberInterface } ``` +## Troubleshooting + +### Composer Security Advisories +When working with **Shopware 6.5.x**, you may encounter a build failure during `composer update` or `composer install`. This is caused by the **Composer Audit** feature (introduced in Composer 2.7+), which automatically blocks the installation of packages with known security vulnerabilities. + +The error message typically looks like this: +`...these were not loaded, because they are affected by security advisories (PKSA-...)` + +### Recommended Solution: Upgrade +The most effective and secure way to resolve this is to **upgrade to the latest version of our plugin** and, if possible, move to **Shopware 6.7+**. +The latest versions provide: +* **Security Patches:** Protection against the vulnerabilities flagged by Composer. +* **Stability:** Improved compatibility with modern PHP versions and server environments. +* **Performance:** Optimized code execution for faster storefront response times. ## Contribute diff --git a/spec/Storefront/Controller/ProxyControllerSpec.php b/spec/Storefront/Controller/ProxyControllerSpec.php index eb73f7e9..fd3ed4b4 100644 --- a/spec/Storefront/Controller/ProxyControllerSpec.php +++ b/spec/Storefront/Controller/ProxyControllerSpec.php @@ -15,10 +15,10 @@ use PhpSpec\Wrapper\Collaborator; use PHPUnit\Framework\Assert; use Prophecy\Argument; -use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -31,7 +31,7 @@ class ProxyControllerSpec extends ObjectBehavior public function let( Communication $config, ClientInterface $client, - ClientBuilder $clientBuilder + ClientBuilder $clientBuilder, ): void { $serverUrl = 'https://example.fact-finder.de/fact-finder'; $config->getServerUrl()->willReturn($serverUrl); @@ -40,11 +40,13 @@ public function let( 'username', 'pass', ]); + $config->isProxyEnabled()->willReturn(true); $this->beConstructedWith($config); $clientBuilder->build()->willReturn($client); $clientBuilder->withServerUrl(Argument::any())->willReturn($clientBuilder); $clientBuilder->withCredentials(Argument::any())->willReturn($clientBuilder); $clientBuilder->withVersion(Argument::any())->willReturn($clientBuilder); + $this->client = $client; $this->clientBuilder = $clientBuilder; } @@ -54,15 +56,23 @@ public function it_should_return_success_response( ResponseInterface $response, EventDispatcherInterface $eventDispatcher, EnrichProxyDataEvent $event, - Stream $stream + Stream $stream, ): void { - // Expect & Given $request->getMethod()->willReturn(Request::METHOD_GET); + $request->headers = new HeaderBag([ + '1234567890abcdef1234' => 'val', + 'abcdef1234567890abcd' => 'val', + '0987654321fedcba0987' => 'val', + ]); + $uri = 'rest/v5/search/example_channel?query=bag&sid=123&format=json'; $_SERVER['REQUEST_URI'] = sprintf('/fact-finder/proxy/%s', $uri); + $this->client->request(Request::METHOD_GET, $uri)->willReturn($response); - $jsonResponse = file_get_contents(dirname(__DIR__, 2) . '/data/proxy/search-bag.json'); + + $jsonResponse = json_encode(['some' => 'data']); // Skrócone dla przykładu $responseData = json_decode($jsonResponse, true); + $stream->__toString()->willReturn($jsonResponse); $response->getBody()->willReturn($stream); $event->getData()->willReturn($responseData); @@ -80,13 +90,25 @@ public function it_should_return_error_response( Request $request, EventDispatcherInterface $eventDispatcher, BeforeProxyErrorResponseEvent $event, - RequestInterface $requestInterface + RequestInterface $requestInterface, ): void { // Expect & Given $request->getMethod()->willReturn(Request::METHOD_GET); + $request->headers = new HeaderBag([ + '1234567890abcdef1234' => 'val', + 'abcdef1234567890abcd' => 'val', + '0987654321fedcba0987' => 'val', + ]); + $uri = 'rest/v5/search/example_channel?query=bag&sid=123&format=json'; $_SERVER['REQUEST_URI'] = sprintf('/fact-finder/proxy/%s', $uri); - $this->client->request(Request::METHOD_GET, $uri)->willThrow(new ConnectException('Unable to connect with server.', $requestInterface->getWrappedObject())); + + $this->client->request(Request::METHOD_GET, $uri)->willThrow( + new ConnectException('Unable to connect with server.', $requestInterface->getWrappedObject()) + ); + + $errorResponse = new JsonResponse(['message' => 'Unable to connect with server.'], Response::HTTP_BAD_REQUEST); + $event->getResponse()->willReturn($errorResponse); $eventDispatcher->dispatch(Argument::type(BeforeProxyErrorResponseEvent::class))->willReturn($event); // When diff --git a/src/Api/TestConnectionController.php b/src/Api/TestConnectionController.php index bc2aaa77..176fc8c7 100644 --- a/src/Api/TestConnectionController.php +++ b/src/Api/TestConnectionController.php @@ -10,7 +10,6 @@ use Omikron\FactFinder\Shopware6\Config\Communication as CommunicationConfig; use Omikron\FactFinder\Shopware6\Upload\UploadService; use Psr\Log\LoggerInterface; -use Shopware\Core\Framework\Routing\Annotation\RouteScope; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Annotation\Route; @@ -33,12 +32,6 @@ public function __construct( */ public function testApiConnection(): JsonResponse { - $client = $this->clientBuilder - ->withCredentials(new Credentials(...$this->config->getCredentials())) - ->withServerUrl($this->config->getServerUrl()) - ->withVersion($this->config->getVersion()) - ->build(); - try { $client = $this->clientBuilder ->withCredentials(new Credentials(...$this->config->getCredentials())) diff --git a/src/Api/UiFeedExportController.php b/src/Api/UiFeedExportController.php index d8e3b2e0..d174d952 100644 --- a/src/Api/UiFeedExportController.php +++ b/src/Api/UiFeedExportController.php @@ -10,7 +10,6 @@ use Omikron\FactFinder\Shopware6\MessageQueue\FeedExportHandler; use Omikron\FactFinder\Shopware6\MessageQueue\RefreshExportCacheHandler; use Psr\Log\LoggerInterface; -use Shopware\Core\Framework\Routing\Annotation\RouteScope; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Api/UpdateFieldRolesController.php b/src/Api/UpdateFieldRolesController.php index 17321d7e..77c7e8b0 100644 --- a/src/Api/UpdateFieldRolesController.php +++ b/src/Api/UpdateFieldRolesController.php @@ -10,7 +10,6 @@ use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; -use Shopware\Core\Framework\Routing\Annotation\RouteScope; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Annotation\Route; diff --git a/src/Storefront/Controller/ProxyController.php b/src/Storefront/Controller/ProxyController.php index 7c658b1f..08ac83af 100644 --- a/src/Storefront/Controller/ProxyController.php +++ b/src/Storefront/Controller/ProxyController.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; /** @@ -46,6 +47,17 @@ public function execute( ClientBuilder $clientBuilder, EventDispatcherInterface $eventDispatcher, ): Response { + if (!$this->config->isProxyEnabled()) { + throw new NotFoundHttpException('Proxy is disabled.'); + } + + if (!$this->isWebcRequest($request->headers->all())) { + return new JsonResponse( + ['message' => 'UNAUTHORIZED'], + Response::HTTP_UNAUTHORIZED + ); + } + $client = $clientBuilder ->withServerUrl($this->config->getServerUrl()) ->withCredentials(new Credentials(...$this->config->getCredentials())) @@ -84,4 +96,20 @@ public function execute( return $event->getResponse(); } } + + private function isWebcRequest(array $headers): bool + { + $pattern = '/^[0-9a-f]{20}$/i'; + + $matchingHeaders = array_filter( + array_keys($headers), + fn ($headerName) => preg_match($pattern, (string) $headerName) + ); + + if (count($matchingHeaders) >= 3) { + return true; + } + + return false; + } }