mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 17:10:53 +00:00
Merge branch '1.11'
* 1.11: [Security] XSS - SVG file upload vulnerability fixed [PHPUnit] Move subscribers tests to main directory Replace str_contains with strpos method to support PHP 7 [Behat] Extract browser element and context [Behat] Add scenarios for securing access to account and dashboard after logging out Minor fixes for specs and unit tests of cache control subscribers Remove type declarations for properties due to supporting PHP 7.3 [Maintenace] Test existence of new cache headers [UI] Force no-store cache directives for admin and customer account section suggested review changes listener added to finish response with X-Frame-Options sameorigin header
This commit is contained in:
commit
1cb6209a65
34 changed files with 764 additions and 8 deletions
|
|
@ -43,6 +43,7 @@
|
|||
"doctrine/orm": "^2.7",
|
||||
"doctrine/persistence": "^2.3",
|
||||
"egulias/email-validator": "^3.1",
|
||||
"enshrined/svg-sanitize": "^0.15.4",
|
||||
"fakerphp/faker": "^1.9",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
@customer_account
|
||||
Feature: Securing access to the account after using the back button after logging out
|
||||
In order to have my personal information secured
|
||||
As a Customer
|
||||
I want to be unable to access to the account by using the back button after logging out
|
||||
|
||||
Background:
|
||||
Given the store operates on a single channel in "United States"
|
||||
And I am a logged in customer
|
||||
And I am browsing my orders
|
||||
|
||||
@ui @javascript @no-api
|
||||
Scenario: Securing access to the account after using the back button after logging out
|
||||
When I log out
|
||||
And I go back one page in the browser
|
||||
Then I should not see my orders
|
||||
And I should be on the login page
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
@admin_dashboard
|
||||
Feature: Securing access to the administration panel after using the back button after logging out
|
||||
In order to have administration panel secured
|
||||
As an Administrator
|
||||
I want to be unable to access to the administration panel by using the back button after logging out
|
||||
|
||||
Background:
|
||||
Given the store operates on a single channel in "United States"
|
||||
And I am logged in as an administrator
|
||||
And I am on the administration dashboard
|
||||
|
||||
@ui @javascript @no-api
|
||||
Scenario: Securing access to administration dashboard after using the back button after logging out
|
||||
When I log out
|
||||
And I go back one page in the browser
|
||||
Then I should not see the administration dashboard
|
||||
And I should be on the login page
|
||||
|
|
@ -29,9 +29,10 @@ final class DashboardContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @Given I am on the administration dashboard
|
||||
* @When I (try to )open administration dashboard
|
||||
*/
|
||||
public function iOpenAdministrationDashboard()
|
||||
public function iOpenAdministrationDashboard(): void
|
||||
{
|
||||
try {
|
||||
$this->dashboardPage->open();
|
||||
|
|
@ -55,6 +56,14 @@ final class DashboardContext implements Context
|
|||
$this->dashboardPage->chooseChannel($channelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When I log out
|
||||
*/
|
||||
public function iLogOut(): void
|
||||
{
|
||||
$this->dashboardPage->logOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should see :number new orders
|
||||
*/
|
||||
|
|
@ -102,4 +111,12 @@ final class DashboardContext implements Context
|
|||
{
|
||||
Assert::same($this->dashboardPage->getNumberOfNewOrdersInTheList(), (int) $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should not see the administration dashboard
|
||||
*/
|
||||
public function iShouldNotSeeTheAdministrationDashboard(): void
|
||||
{
|
||||
Assert::false($this->dashboardPage->isOpen());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,4 +139,12 @@ final class LoginContext implements Context
|
|||
$this->loginPage->specifyPassword($password);
|
||||
$this->loginPage->logIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should be on the login page
|
||||
*/
|
||||
public function iShouldBeOnTheLoginPage(): void
|
||||
{
|
||||
Assert::true($this->loginPage->isOpen());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
src/Sylius/Behat/Context/Ui/BrowserContext.php
Normal file
36
src/Sylius/Behat/Context/Ui/BrowserContext.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Behat\Context\Ui;
|
||||
|
||||
use Behat\Behat\Context\Context;
|
||||
use Sylius\Behat\Element\BrowserElementInterface;
|
||||
|
||||
final class BrowserContext implements Context
|
||||
{
|
||||
/** @var BrowserElementInterface */
|
||||
private $browserElement;
|
||||
|
||||
public function __construct(BrowserElementInterface $browserElement)
|
||||
{
|
||||
$this->browserElement = $browserElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @When I go back one page in the browser
|
||||
*/
|
||||
public function iGoBackOnePageInTheBrowser(): void
|
||||
{
|
||||
$this->browserElement->goBack();
|
||||
}
|
||||
}
|
||||
|
|
@ -255,9 +255,10 @@ final class AccountContext implements Context
|
|||
}
|
||||
|
||||
/**
|
||||
* @Given I am browsing my orders
|
||||
* @When I browse my orders
|
||||
*/
|
||||
public function iBrowseMyOrders()
|
||||
public function iBrowseMyOrders(): void
|
||||
{
|
||||
$this->orderIndexPage->open();
|
||||
}
|
||||
|
|
@ -523,4 +524,20 @@ final class AccountContext implements Context
|
|||
|
||||
throw new \InvalidArgumentException('Dashboard has been openned, but it shouldn\'t as customer should not be logged in');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should not see my orders
|
||||
*/
|
||||
public function iShouldNotSeeMyOrders(): void
|
||||
{
|
||||
Assert::false($this->orderIndexPage->isOpen());
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then I should be on the login page
|
||||
*/
|
||||
public function iShouldBeOnTheLoginPage(): void
|
||||
{
|
||||
Assert::true($this->loginPage->isOpen());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
src/Sylius/Behat/Element/BrowserElement.php
Normal file
24
src/Sylius/Behat/Element/BrowserElement.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Behat\Element;
|
||||
|
||||
use FriendsOfBehat\PageObjectExtension\Element\Element;
|
||||
|
||||
final class BrowserElement extends Element implements BrowserElementInterface
|
||||
{
|
||||
public function goBack(): void
|
||||
{
|
||||
$this->getDriver()->back();
|
||||
}
|
||||
}
|
||||
19
src/Sylius/Behat/Element/BrowserElementInterface.php
Normal file
19
src/Sylius/Behat/Element/BrowserElementInterface.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Behat\Element;
|
||||
|
||||
interface BrowserElementInterface
|
||||
{
|
||||
public function goBack(): void;
|
||||
}
|
||||
|
|
@ -317,6 +317,10 @@
|
|||
<tag name="fob.context_service" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.behat.context.ui.browser" class="Sylius\Behat\Context\Ui\BrowserContext">
|
||||
<argument type="service" id="sylius.behat.element.browser" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.behat.context.ui.channel" class="Sylius\Behat\Context\Ui\ChannelContext">
|
||||
<argument type="service" id="sylius.behat.shared_storage" />
|
||||
<argument type="service" id="sylius.behat.channel_context_setter" />
|
||||
|
|
|
|||
|
|
@ -20,10 +20,13 @@
|
|||
<import resource="elements/shop.xml" />
|
||||
<import resource="elements/product.xml" />
|
||||
</imports>
|
||||
|
||||
<services>
|
||||
<service id="sylius.behat.element" class="FriendsOfBehat\PageObjectExtension\Element\Element" abstract="true" public="false">
|
||||
<argument type="service" id="behat.mink.default_session" />
|
||||
<argument type="service" id="behat.mink.parameters" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.behat.element.browser" class="Sylius\Behat\Element\BrowserElement" parent="sylius.behat.element" public="false" />
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ default:
|
|||
- sylius.behat.context.setup.user
|
||||
- sylius.behat.context.setup.zone
|
||||
|
||||
- sylius.behat.context.ui.browser
|
||||
- sylius.behat.context.ui.channel
|
||||
- sylius.behat.context.ui.email
|
||||
- sylius.behat.context.ui.shop.account
|
||||
|
|
@ -44,6 +45,7 @@ default:
|
|||
- sylius.behat.context.ui.shop.checkout.shipping
|
||||
- sylius.behat.context.ui.shop.currency
|
||||
- sylius.behat.context.ui.shop.homepage
|
||||
- sylius.behat.context.ui.user
|
||||
|
||||
filters:
|
||||
tags: "@customer_account&&@ui"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ default:
|
|||
- sylius.behat.context.transform.shared_storage
|
||||
|
||||
- sylius.behat.context.ui.admin.dashboard
|
||||
- sylius.behat.context.ui.admin.login
|
||||
- sylius.behat.context.ui.admin.notification
|
||||
- sylius.behat.context.ui.browser
|
||||
|
||||
filters:
|
||||
tags: "@admin_dashboard&&@ui"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\AdminBundle\EventListener;
|
||||
|
||||
use Sylius\Bundle\AdminBundle\SectionResolver\AdminSection;
|
||||
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
final class AdminSectionCacheControlSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** @var SectionProviderInterface */
|
||||
private $sectionProvider;
|
||||
|
||||
public function __construct(SectionProviderInterface $sectionProvider)
|
||||
{
|
||||
$this->sectionProvider = $sectionProvider;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::RESPONSE => 'setCacheControlDirectives',
|
||||
];
|
||||
}
|
||||
|
||||
public function setCacheControlDirectives(ResponseEvent $event): void
|
||||
{
|
||||
if (!$this->sectionProvider->getSection() instanceof AdminSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $event->getResponse();
|
||||
|
||||
$response->headers->addCacheControlDirective('no-cache', true);
|
||||
$response->headers->addCacheControlDirective('max-age', '0');
|
||||
$response->headers->addCacheControlDirective('must-revalidate', true);
|
||||
$response->headers->addCacheControlDirective('no-store', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,5 +25,10 @@
|
|||
<argument type="service" id="session" />
|
||||
<tag name="kernel.event_subscriber" event="kernel.exception" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.event_subscriber.admin_cache_control_subscriber" class="Sylius\Bundle\AdminBundle\EventListener\AdminSectionCacheControlSubscriber">
|
||||
<argument type="service" id="sylius.section_resolver.uri_based_section_resolver" />
|
||||
<tag name="kernel.event_subscriber" event="kernel.response" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace spec\Sylius\Bundle\AdminBundle\EventListener;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Bundle\AdminBundle\SectionResolver\AdminSection;
|
||||
use Sylius\Bundle\CoreBundle\SectionResolver\SectionInterface;
|
||||
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
final class AdminSectionCacheControlSubscriberSpec extends ObjectBehavior
|
||||
{
|
||||
function let(SectionProviderInterface $sectionProvider): void
|
||||
{
|
||||
$this->beConstructedWith($sectionProvider);
|
||||
}
|
||||
|
||||
function it_subscribes_to_kernel_response_event(): void
|
||||
{
|
||||
$this::getSubscribedEvents()->shouldReturn([KernelEvents::RESPONSE => 'setCacheControlDirectives']);
|
||||
}
|
||||
|
||||
function it_adds_cache_control_directives_to_admin_requests(
|
||||
SectionProviderInterface $sectionProvider,
|
||||
HttpKernelInterface $kernel,
|
||||
Request $request,
|
||||
Response $response,
|
||||
ResponseHeaderBag $responseHeaderBag,
|
||||
AdminSection $adminSection
|
||||
): void {
|
||||
$sectionProvider->getSection()->willReturn($adminSection);
|
||||
|
||||
$response->headers = $responseHeaderBag->getWrappedObject();
|
||||
|
||||
$event = new ResponseEvent(
|
||||
$kernel->getWrappedObject(),
|
||||
$request->getWrappedObject(),
|
||||
KernelInterface::MASTER_REQUEST,
|
||||
$response->getWrappedObject()
|
||||
);
|
||||
|
||||
$responseHeaderBag->addCacheControlDirective('no-cache', true)->shouldBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('max-age', '0')->shouldBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('must-revalidate', true)->shouldBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('no-store', true)->shouldBeCalled();
|
||||
|
||||
$this->setCacheControlDirectives($event);
|
||||
}
|
||||
|
||||
function it_does_nothing_if_section_is_different_than_admin(
|
||||
SectionProviderInterface $sectionProvider,
|
||||
HttpKernelInterface $kernel,
|
||||
Request $request,
|
||||
Response $response,
|
||||
ResponseHeaderBag $responseHeaderBag,
|
||||
SectionInterface $section
|
||||
): void {
|
||||
$sectionProvider->getSection()->willReturn($section);
|
||||
|
||||
$response->headers = $responseHeaderBag->getWrappedObject();
|
||||
|
||||
$event = new ResponseEvent(
|
||||
$kernel->getWrappedObject(),
|
||||
$request->getWrappedObject(),
|
||||
KernelInterface::MASTER_REQUEST,
|
||||
$response->getWrappedObject()
|
||||
);
|
||||
|
||||
$responseHeaderBag->addCacheControlDirective('no-cache', true)->shouldNotBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('max-age', '0')->shouldNotBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('must-revalidate', true)->shouldNotBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('no-store', true)->shouldNotBeCalled();
|
||||
|
||||
$this->setCacheControlDirectives($event);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
"php": "^8.0",
|
||||
"doctrine/dbal": "^2.7",
|
||||
"api-platform/core": "^2.6",
|
||||
"enshrined/svg-sanitize": "^0.15.4",
|
||||
"lexik/jwt-authentication-bundle": "^2.6",
|
||||
"sylius/core-bundle": "^1.7",
|
||||
"symfony/messenger": "^5.4"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\CoreBundle\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
final class XFrameOptionsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::RESPONSE => 'onKernelResponse',
|
||||
];
|
||||
}
|
||||
|
||||
public function onKernelResponse(ResponseEvent $event): void
|
||||
{
|
||||
if (!$this->isMainRequest($event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $event->getResponse();
|
||||
|
||||
$response->headers->set('X-Frame-Options', 'sameorigin');
|
||||
}
|
||||
|
||||
private function isMainRequest(ResponseEvent $event): bool
|
||||
{
|
||||
if (\method_exists($event, 'isMainRequest')) {
|
||||
return $event->isMainRequest();
|
||||
}
|
||||
|
||||
return $event->isMasterRequest();
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,10 @@
|
|||
<argument type="service" id="Sylius\Bundle\CoreBundle\EventListener\LocaleAwareListener.inner" />
|
||||
</service>
|
||||
|
||||
<service id="Sylius\Bundle\CoreBundle\EventListener\XFrameOptionsSubscriber">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.listener.taxon_deletion" class="Sylius\Bundle\CoreBundle\EventListener\TaxonDeletionListener">
|
||||
<argument type="service" id="session" />
|
||||
<argument type="service" id="sylius.repository.channel" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ShopBundle\EventListener;
|
||||
|
||||
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
|
||||
use Sylius\Bundle\ShopBundle\SectionResolver\ShopCustomerAccountSubSection;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
final class ShopCustomerAccountSubSectionCacheControlSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** @var SectionProviderInterface */
|
||||
private $sectionProvider;
|
||||
|
||||
public function __construct(SectionProviderInterface $sectionProvider)
|
||||
{
|
||||
$this->sectionProvider = $sectionProvider;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::RESPONSE => 'setCacheControlDirectives',
|
||||
];
|
||||
}
|
||||
|
||||
public function setCacheControlDirectives(ResponseEvent $event): void
|
||||
{
|
||||
if (!$this->sectionProvider->getSection() instanceof ShopCustomerAccountSubSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $event->getResponse();
|
||||
|
||||
$response->headers->addCacheControlDirective('no-cache', true);
|
||||
$response->headers->addCacheControlDirective('max-age', '0');
|
||||
$response->headers->addCacheControlDirective('must-revalidate', true);
|
||||
$response->headers->addCacheControlDirective('no-store', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
</service>
|
||||
|
||||
<service id="sylius.section_resolver.shop_uri_based_section_resolver" class="Sylius\Bundle\ShopBundle\SectionResolver\ShopUriBasedSectionResolver">
|
||||
<argument>account</argument>
|
||||
<tag name="sylius.uri_based_section_resolver" priority="-10" />
|
||||
</service>
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@
|
|||
<tag name="kernel.event_listener" event="sylius.customer.post_update" method="sendVerificationEmail" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.listener.shop_customer_account_sub_section_cache_control_subscriber" class="Sylius\Bundle\ShopBundle\EventListener\ShopCustomerAccountSubSectionCacheControlSubscriber">
|
||||
<argument type="service" id="sylius.section_resolver.uri_based_section_resolver" />
|
||||
<tag name="kernel.event_subscriber" event="kernel.response" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.listener.order_customer_ip" class="Sylius\Bundle\ShopBundle\EventListener\OrderCustomerIpListener">
|
||||
<argument type="service" id="sylius.customer_ip_assigner" />
|
||||
<argument type="service" id="request_stack" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ShopBundle\SectionResolver;
|
||||
|
||||
class ShopCustomerAccountSubSection extends ShopSection
|
||||
{
|
||||
}
|
||||
|
|
@ -18,8 +18,20 @@ use Sylius\Bundle\CoreBundle\SectionResolver\UriBasedSectionResolverInterface;
|
|||
|
||||
final class ShopUriBasedSectionResolver implements UriBasedSectionResolverInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $shopCustomerAccountUri;
|
||||
|
||||
public function __construct(string $shopCustomerAccountUri = 'account')
|
||||
{
|
||||
$this->shopCustomerAccountUri = $shopCustomerAccountUri;
|
||||
}
|
||||
|
||||
public function getSection(string $uri): SectionInterface
|
||||
{
|
||||
if (strpos($uri, $this->shopCustomerAccountUri) !== false) {
|
||||
return new ShopCustomerAccountSubSection();
|
||||
}
|
||||
|
||||
return new ShopSection();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace spec\Sylius\Bundle\ShopBundle\EventListener;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Bundle\CoreBundle\SectionResolver\SectionInterface;
|
||||
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
|
||||
use Sylius\Bundle\ShopBundle\SectionResolver\ShopCustomerAccountSubSection;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
final class ShopCustomerAccountSubSectionCacheControlSubscriberSpec extends ObjectBehavior
|
||||
{
|
||||
function let(SectionProviderInterface $sectionProvider): void
|
||||
{
|
||||
$this->beConstructedWith($sectionProvider);
|
||||
}
|
||||
|
||||
function it_subscribes_to_kernel_response_event()
|
||||
{
|
||||
$this::getSubscribedEvents()->shouldReturn([KernelEvents::RESPONSE => 'setCacheControlDirectives']);
|
||||
}
|
||||
|
||||
function it_adds_cache_control_directives_to_customer_account_requests(
|
||||
SectionProviderInterface $sectionProvider,
|
||||
HttpKernelInterface $kernel,
|
||||
Request $request,
|
||||
Response $response,
|
||||
ResponseHeaderBag $responseHeaderBag,
|
||||
ShopCustomerAccountSubSection $customerAccountSubSection
|
||||
): void {
|
||||
$sectionProvider->getSection()->willReturn($customerAccountSubSection);
|
||||
|
||||
$response->headers = $responseHeaderBag->getWrappedObject();
|
||||
|
||||
$event = new ResponseEvent(
|
||||
$kernel->getWrappedObject(),
|
||||
$request->getWrappedObject(),
|
||||
KernelInterface::MASTER_REQUEST,
|
||||
$response->getWrappedObject()
|
||||
);
|
||||
|
||||
$responseHeaderBag->addCacheControlDirective('no-cache', true)->shouldBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('max-age', '0')->shouldBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('must-revalidate', true)->shouldBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('no-store', true)->shouldBeCalled();
|
||||
|
||||
$this->setCacheControlDirectives($event);
|
||||
}
|
||||
|
||||
function it_does_nothing_if_section_is_different_than_customer_account(
|
||||
SectionProviderInterface $sectionProvider,
|
||||
HttpKernelInterface $kernel,
|
||||
Request $request,
|
||||
Response $response,
|
||||
ResponseHeaderBag $responseHeaderBag,
|
||||
SectionInterface $section
|
||||
): void {
|
||||
$sectionProvider->getSection()->willReturn($section);
|
||||
|
||||
$response->headers = $responseHeaderBag->getWrappedObject();
|
||||
|
||||
$event = new ResponseEvent(
|
||||
$kernel->getWrappedObject(),
|
||||
$request->getWrappedObject(),
|
||||
KernelInterface::MASTER_REQUEST,
|
||||
$response->getWrappedObject()
|
||||
);
|
||||
|
||||
$responseHeaderBag->addCacheControlDirective('no-cache', true)->shouldNotBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('max-age', '0')->shouldNotBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('must-revalidate', true)->shouldNotBeCalled();
|
||||
$responseHeaderBag->addCacheControlDirective('no-store', true)->shouldNotBeCalled();
|
||||
|
||||
$this->setCacheControlDirectives($event);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ namespace spec\Sylius\Bundle\ShopBundle\SectionResolver;
|
|||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Bundle\CoreBundle\SectionResolver\UriBasedSectionResolverInterface;
|
||||
use Sylius\Bundle\ShopBundle\SectionResolver\ShopCustomerAccountSubSection;
|
||||
use Sylius\Bundle\ShopBundle\SectionResolver\ShopSection;
|
||||
|
||||
final class ShopUriBasedSectionResolverSpec extends ObjectBehavior
|
||||
|
|
@ -24,7 +25,7 @@ final class ShopUriBasedSectionResolverSpec extends ObjectBehavior
|
|||
$this->shouldImplement(UriBasedSectionResolverInterface::class);
|
||||
}
|
||||
|
||||
function it_always_returns_shop(): void
|
||||
function it_returns_shop_by_default(): void
|
||||
{
|
||||
$this->getSection('/api/something')->shouldBeLike(new ShopSection());
|
||||
$this->getSection('/api')->shouldBeLike(new ShopSection());
|
||||
|
|
@ -33,4 +34,29 @@ final class ShopUriBasedSectionResolverSpec extends ObjectBehavior
|
|||
$this->getSection('/admin/asd')->shouldBeLike(new ShopSection());
|
||||
$this->getSection('/en_US/api')->shouldBeLike(new ShopSection());
|
||||
}
|
||||
|
||||
function it_uses_account_prefix_for_customer_account_subsection_by_default(): void
|
||||
{
|
||||
$this->getSection('/account')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
$this->getSection('/api/account')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
$this->getSection('/en_US/account')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
$this->getSection('/account/random')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
}
|
||||
|
||||
function it_may_have_account_prefix_customized(): void
|
||||
{
|
||||
$this->beConstructedWith('konto');
|
||||
|
||||
$this->getSection('/konto')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
$this->getSection('/konto')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
$this->getSection('/api/konto')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
$this->getSection('/en_US/konto')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
$this->getSection('/konto/random')->shouldBeLike(new ShopCustomerAccountSubSection());
|
||||
|
||||
$this->getSection('/account')->shouldBeLike(new ShopSection());
|
||||
$this->getSection('/account')->shouldBeLike(new ShopSection());
|
||||
$this->getSection('/api/account')->shouldBeLike(new ShopSection());
|
||||
$this->getSection('/en_US/account')->shouldBeLike(new ShopSection());
|
||||
$this->getSection('/account/random')->shouldBeLike(new ShopSection());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Sylius\Component\Core\Uploader;
|
||||
|
||||
use enshrined\svgSanitize\Sanitizer;
|
||||
use Gaufrette\Filesystem;
|
||||
use Sylius\Component\Core\Generator\ImagePathGeneratorInterface;
|
||||
use Sylius\Component\Core\Generator\UploadedImagePathGenerator;
|
||||
|
|
@ -22,9 +23,15 @@ use Webmozart\Assert\Assert;
|
|||
|
||||
class ImageUploader implements ImageUploaderInterface
|
||||
{
|
||||
private const MIME_SVG_XML = 'image/svg+xml';
|
||||
private const MIME_SVG = 'image/svg';
|
||||
|
||||
/** @var ImagePathGeneratorInterface */
|
||||
protected $imagePathGenerator;
|
||||
|
||||
/** @var Sanitizer */
|
||||
protected $sanitizer;
|
||||
|
||||
public function __construct(
|
||||
protected Filesystem $filesystem,
|
||||
?ImagePathGeneratorInterface $imagePathGenerator = null
|
||||
|
|
@ -37,6 +44,7 @@ class ImageUploader implements ImageUploaderInterface
|
|||
}
|
||||
|
||||
$this->imagePathGenerator = $imagePathGenerator ?? new UploadedImagePathGenerator();
|
||||
$this->sanitizer = new Sanitizer();
|
||||
}
|
||||
|
||||
public function upload(ImageInterface $image): void
|
||||
|
|
@ -45,11 +53,13 @@ class ImageUploader implements ImageUploaderInterface
|
|||
return;
|
||||
}
|
||||
|
||||
/** @var File $file */
|
||||
$file = $image->getFile();
|
||||
|
||||
/** @var File $file */
|
||||
Assert::isInstanceOf($file, File::class);
|
||||
|
||||
$fileContent = $this->sanitizeContent(file_get_contents($file->getPathname()), $file->getMimeType());
|
||||
|
||||
if (null !== $image->getPath() && $this->has($image->getPath())) {
|
||||
$this->remove($image->getPath());
|
||||
}
|
||||
|
|
@ -60,10 +70,7 @@ class ImageUploader implements ImageUploaderInterface
|
|||
|
||||
$image->setPath($path);
|
||||
|
||||
$this->filesystem->write(
|
||||
$image->getPath(),
|
||||
file_get_contents($image->getFile()->getPathname())
|
||||
);
|
||||
$this->filesystem->write($image->getPath(), $fileContent);
|
||||
}
|
||||
|
||||
public function remove(string $path): bool
|
||||
|
|
@ -75,6 +82,15 @@ class ImageUploader implements ImageUploaderInterface
|
|||
return false;
|
||||
}
|
||||
|
||||
protected function sanitizeContent(string $fileContent, string $mimeType): string
|
||||
{
|
||||
if (self::MIME_SVG_XML === $mimeType || self::MIME_SVG === $mimeType) {
|
||||
$fileContent = $this->sanitizer->sanitize($fileContent);
|
||||
}
|
||||
|
||||
return $fileContent;
|
||||
}
|
||||
|
||||
private function has(string $path): bool
|
||||
{
|
||||
return $this->filesystem->has($path);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
],
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"enshrined/svg-sanitize": "^0.15.4",
|
||||
"knplabs/gaufrette": "^0.8",
|
||||
"payum/payum": "^1.6",
|
||||
"php-http/guzzle6-adapter": "^2.0",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,9 @@
|
|||
"egulias/email-validator": {
|
||||
"version": "2.1.19"
|
||||
},
|
||||
"enshrined/svg-sanitize": {
|
||||
"version": "0.15.4"
|
||||
},
|
||||
"fakerphp/faker": {
|
||||
"version": "v1.12.0"
|
||||
},
|
||||
|
|
|
|||
29
tests/Controller/XFrameOptionsTest.php
Normal file
29
tests/Controller/XFrameOptionsTest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Tests\Controller;
|
||||
|
||||
use ApiTestCase\JsonApiTestCase;
|
||||
|
||||
final class XFrameOptionsTest extends JsonApiTestCase
|
||||
{
|
||||
/** @test */
|
||||
public function it_sets_frame_options_header(): void
|
||||
{
|
||||
$this->client->request('GET', '/');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertSame('sameorigin', $response->headers->get('X-Frame-Options'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Tests\EventListener;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
final class AdminSectionCacheControlSubscriberTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function it_returns_proper_cache_headers_for_admin_endpoints(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/admin/');
|
||||
|
||||
$this->assertResponseHeaderSame('Cache-Control', 'max-age=0, must-revalidate, no-cache, no-store, private');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Tests\EventListener;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
final class ShopCustomerAccountSubSectionCacheControlSubscriberTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function it_returns_proper_cache_headers_for_customer_account_endpoints(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
$client->request('GET', '/en_US/account/');
|
||||
|
||||
$this->assertResponseHeaderSame('Cache-Control', 'max-age=0, must-revalidate, no-cache, no-store, private');
|
||||
}
|
||||
}
|
||||
59
tests/Functional/ImageUploaderTest.php
Normal file
59
tests/Functional/ImageUploaderTest.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Tests\Functional;
|
||||
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Sylius\Component\Core\Model\ProductImage;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\BrowserKit\Client;
|
||||
|
||||
final class ImageUploaderTest extends WebTestCase
|
||||
{
|
||||
/** @var Client */
|
||||
private static $client;
|
||||
|
||||
/** @test */
|
||||
public function it_sanitizes_file_content_if_it_is_svg_mime_type(): void
|
||||
{
|
||||
self::$client = static::createClient();
|
||||
self::$container = self::$client->getContainer();
|
||||
|
||||
$imageUploader = self::$container->get('sylius.image_uploader');
|
||||
$fileSystem = self::$container->get('gaufrette.sylius_image_filesystem');
|
||||
|
||||
$file = new UploadedFile(__DIR__ . '/../Resources/xss.svg', 'xss.svg');
|
||||
Assert::assertStringContainsString('<script', $this->getContent($file));
|
||||
|
||||
$image = new ProductImage();
|
||||
$image->setFile($file);
|
||||
|
||||
$imageUploader->upload($image);
|
||||
|
||||
$sanitizedFile = $fileSystem->get($image->getPath());
|
||||
Assert::assertStringNotContainsString('<script', $sanitizedFile->getContent());
|
||||
}
|
||||
|
||||
private function getContent(UploadedFile $file): string
|
||||
{
|
||||
$content = file_get_contents($file->getPathname());
|
||||
|
||||
if (false === $content) {
|
||||
throw new FileException(sprintf('Could not get the content of the file "%s".', $file->getPathname()));
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
9
tests/Resources/xss.svg
Normal file
9
tests/Resources/xss.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100" height="100" style="fill:rgb(210,0,37);" />
|
||||
<script type="text/javascript">
|
||||
alert("XSS attack");
|
||||
</script>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 364 B |
Loading…
Add table
Reference in a new issue