Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
36 changes: 29 additions & 7 deletions spec/Storefront/Controller/ProxyControllerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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
Expand Down
7 changes: 0 additions & 7 deletions src/Api/TestConnectionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()))
Expand Down
1 change: 0 additions & 1 deletion src/Api/UiFeedExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion src/Api/UpdateFieldRolesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions src/Storefront/Controller/ProxyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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;
}
}
Loading