Skip to content

Commit

Permalink
Prevent double cookie encryption (see #20) (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbaljet authored Nov 19, 2024
1 parent 8956f60 commit 8c5e899
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 13 deletions.
27 changes: 22 additions & 5 deletions demo-app/tests/Feature/DispatchBaseUrlRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Tests\Feature;

use Database\Factories\UserFactory;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Request;
use Illuminate\Testing\TestResponse;
Expand All @@ -20,10 +19,7 @@ public function setUp(): void
{
parent::setUp();

$this->dispatcher = new DispatchBaseUrlRequest(
$this->app['router'],
$this->app[Kernel::class]
);
$this->dispatcher = new DispatchBaseUrlRequest($this->app['router']);
}

#[Test]
Expand Down Expand Up @@ -80,4 +76,25 @@ public function toResponse($request)

$this->assertEquals('Responsable Response', $response->getContent());
}

#[Test]
public function it_disables_cookie_encryption_on_base_url()
{
$originalRequest = Request::create('/users', 'GET');

$baseUrl = '/roles';

$response = ($this->dispatcher)($originalRequest, $baseUrl);
$cookies = $response->headers->getCookies();

$this->assertNotEmpty($cookies);

foreach ($cookies as $cookie) {
$value = $cookie->getValue();

$this->assertEquals(40, strlen($value));
$decrypted = rescue(fn () => decrypt($value, false), report: false);
$this->assertNull($decrypted);
}
}
}
48 changes: 40 additions & 8 deletions src/DispatchBaseUrlRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@

namespace InertiaUI\Modal;

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Routing\Router;
use Illuminate\Support\Arr;
use Symfony\Component\HttpFoundation\Response;

class DispatchBaseUrlRequest
{
public function __construct(protected Router $router, protected Kernel $kernel)
public function __construct(protected Router $router)
{
//
}
Expand All @@ -35,17 +38,46 @@ public function __invoke(Request $originalRequest, string $baseUrl): Response
$requestForBaseUrl->headers->replace($originalRequest->headers->all());
$requestForBaseUrl->setRequestLocale($originalRequest->getLocale());
$requestForBaseUrl->setDefaultRequestLocale($originalRequest->getDefaultLocale());
$requestForBaseUrl->setRouteResolver(fn () => $this->router->getRoutes()->match($requestForBaseUrl));

$route = $this->router->getRoutes()->match($requestForBaseUrl);
$requestForBaseUrl->setRouteResolver(fn () => $route);

// No need to call setLaravelSession() as it's done by the StartSession middleware
// No need to call setUserResolver() as it's done by AuthServiceProvider::registerRequestRebindHandler()

$response = $this->kernel->handle($requestForBaseUrl);
// Dispatch the request without encrypting cookies because that has
// already happens in the original request. We don't want to
// double-encrypt them, as that would nullify the cookies.
return $this->withoutEncryptingCookies($route, function () use ($requestForBaseUrl) {
$response = app()->handle($requestForBaseUrl, Application::SUB_REQUEST);

return $response instanceof Responsable
? $response->toResponse($requestForBaseUrl)
: $response;
});
}

/**
* Run the given callback with the EncryptCookies middleware disabled.
*/
private function withoutEncryptingCookies(Route $route, callable $callback): mixed
{
$middleware = $this->router->resolveMiddleware($route->gatherMiddleware());

// Clear $route->computedMiddleware to force it to be recalculated
// after removing the EncryptCookies middleware
$route->flushController();

// Store the original excluded middleware so we can restore it later
$currentExcludedMiddleware = Arr::get($route->action, 'excluded_middleware', []);

$this->kernel->terminate($requestForBaseUrl, $response);
foreach ($middleware as $class) {
if ($class === EncryptCookies::class || is_subclass_of($class, EncryptCookies::class)) {
$route->withoutMiddleware($class);
}
}

return $response instanceof Responsable
? $response->toResponse($requestForBaseUrl)
: $response;
// Run the callback and restore the original excluded middleware
return tap($callback(), fn () => $route->action['excluded_middleware'] = $currentExcludedMiddleware);
}
}

0 comments on commit 8c5e899

Please sign in to comment.