Skip to content
Draft
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
14 changes: 12 additions & 2 deletions src/CoreShop/Bundle/FrontendBundle/Controller/CartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,12 @@ public function addItemAction(Request $request): Response
$this->denyAccessUnlessGranted('CORESHOP_CART');
$this->denyAccessUnlessGranted('CORESHOP_CART_ADD_ITEM');

$redirect = $this->getParameterFromRequest($request, '_redirect', $this->generateUrl('coreshop_index'));
$defaultRedirectGet = $this->generateUrl('coreshop_index');
$redirect = $this->validateRedirectUrl(
$request,
(string) $this->getParameterFromRequest($request, '_redirect', $defaultRedirectGet),
$defaultRedirectGet,
);

$product = $this->container->get('coreshop.repository.stack.purchasable')->find($this->getParameterFromRequest($request, 'product'));

Expand All @@ -249,7 +254,12 @@ public function addItemAction(Request $request): Response
$form = $this->container->get('form.factory')->createNamed('coreshop-' . $product->getId(), AddToCartType::class, $addToCart);

if ($request->isMethod('POST')) {
$redirect = $this->getParameterFromRequest($request, '_redirect', $this->generateUrl('coreshop_cart_summary'));
$defaultRedirectPost = $this->generateUrl('coreshop_cart_summary');
$redirect = $this->validateRedirectUrl(
$request,
(string) $this->getParameterFromRequest($request, '_redirect', $defaultRedirectPost),
$defaultRedirectPost,
);

$form->handleRequest($request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,14 @@ public function addressAction(Request $request): Response
$this->fireEvent($request, $address, sprintf('%s.%s.%s_post', 'coreshop', 'address', $eventType));
$this->addFlash('success', $this->container->get('translator')->trans(sprintf('coreshop.ui.customer.address_successfully_%s', $eventType === 'add' ? 'added' : 'updated')));

$defaultRedirect = $this->generateUrl('coreshop_customer_addresses');

return $this->redirect(
$this->getParameterFromRequest($request, '_redirect', $this->generateUrl('coreshop_customer_addresses')),
$this->validateRedirectUrl(
$request,
(string) $this->getParameterFromRequest($request, '_redirect', $defaultRedirect),
$defaultRedirect,
),
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
namespace CoreShop\Bundle\FrontendBundle\Controller;

use CoreShop\Bundle\FrontendBundle\TemplateConfigurator\TemplateConfiguratorInterface;
use CoreShop\Bundle\ResourceBundle\Controller\RedirectUrlValidationTrait;
use CoreShop\Component\Core\Context\ShopperContextInterface;
use CoreShop\Component\Order\Context\CartContextInterface;
use CoreShop\Component\SEO\SEOPresentationInterface;
Expand All @@ -29,6 +30,8 @@

abstract class FrontendController extends AbstractController
{
use RedirectUrlValidationTrait;

public function __construct(
\Psr\Container\ContainerInterface $container,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ public function registerAction(Request $request): Response

$form = $this->container->get('form.factory')->createNamed('customer', CustomerRegistrationType::class, $this->container->get('coreshop.factory.customer')->createNew());

$redirect = $this->getParameterFromRequest($request, '_redirect', $this->generateUrl('coreshop_customer_profile'));
$defaultRedirect = $this->generateUrl('coreshop_customer_profile');
$redirect = $this->validateRedirectUrl(
$request,
(string) $this->getParameterFromRequest($request, '_redirect', $defaultRedirect),
$defaultRedirect,
);

if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true)) {
$form = $form->handleRequest($request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

/*
* CoreShop
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - CoreShop Commercial License (CCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) CoreShop GmbH (https://www.coreshop.com)
* @license https://www.coreshop.com/license GPLv3 and CCL
*
*/

namespace CoreShop\Bundle\ResourceBundle\Controller;

use Symfony\Component\HttpFoundation\Request;

trait RedirectUrlValidationTrait
{
/**
* Validates a redirect URL to prevent open redirects.
*
* Only allows:
* - Relative URLs (starting with "/" but not "//")
* - URLs on the same host as the current request with http/https scheme
*
* @param Request $request The current request to validate against
* @param string $url The URL to validate
* @param string $default The default URL to return if validation fails
*
* @return string The validated URL or the default if invalid
*/
protected function validateRedirectUrl(Request $request, string $url, string $default): string
{
// Empty URL, use default
if ('' === $url) {
return $default;
}

// Check for protocol-relative URLs (//example.com) which could be used for open redirects
if (str_starts_with($url, '//')) {
return $default;
}

// Relative URLs (starting with /) are safe if they don't contain dangerous characters
if (str_starts_with($url, '/')) {
// Reject URLs with backslashes, control characters, or whitespace that could be misinterpreted
if (preg_match('/[\\\\\\x00-\\x1f\\x7f]/', $url)) {
return $default;
}

return $url;
}

// For absolute URLs, verify the host matches the current request
$parsedUrl = parse_url($url);

// If parsing failed or no host is present, use default
if (false === $parsedUrl || !isset($parsedUrl['host'])) {
return $default;
}

// Only allow http and https schemes
if (isset($parsedUrl['scheme']) && !in_array(strtolower($parsedUrl['scheme']), ['http', 'https'], true)) {
return $default;
}

// Reject URLs with @ in the authority component (user:pass@host manipulation)
if (isset($parsedUrl['user']) || str_contains($url, '@')) {
return $default;
}

// Check if the host matches the current request host
if (strtolower($parsedUrl['host']) === strtolower($request->getHost())) {
return $url;
}

return $default;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

namespace CoreShop\Bundle\StorageListBundle\Controller;

use CoreShop\Bundle\ResourceBundle\Controller\RedirectUrlValidationTrait;
use CoreShop\Component\Resource\Model\ResourceInterface;
use CoreShop\Component\Resource\Repository\RepositoryInterface;
use CoreShop\Component\StorageList\Context\StorageListContextInterface;
Expand Down Expand Up @@ -45,6 +46,8 @@

class StorageListController extends AbstractController
{
use RedirectUrlValidationTrait;

public function __construct(
ContainerInterface $container,
protected string $identifier,
Expand Down Expand Up @@ -85,7 +88,12 @@ public function addItemAction(Request $request): Response
$this->denyAccessUnlessGranted($privilege);
$this->denyAccessUnlessGranted($privilegeAdd);

$redirect = $this->getParameterFromRequest($request, '_redirect', $this->generateUrl($this->summaryRoute));
$defaultRedirect = $this->generateUrl($this->summaryRoute);
$redirect = $this->validateRedirectUrl(
$request,
(string) $this->getParameterFromRequest($request, '_redirect', $defaultRedirect),
$defaultRedirect,
);
$product = $this->productRepository->find($this->getParameterFromRequest($request, 'product'));
$storageList = $this->context->getStorageList();

Expand Down