Gamoxion
Gamoxion

Reputation: 169

How to set CSRF cookie to samesite on CakePHP 3.4?

I'm using CakePHP 3.4 (can't upgrade) and in order to protect the system from Cross Site Request Forgery I need to set the CSRF token cookie to SameSite = Strict. However, it seems this version of CakePHP can't handle such setting.

I have tried using the CsrfComponent class and loading the component in AppController

$this->loadComponent('Csrf', [
            'secure' => true,
            'httpOnly' => true,
        ]);

How can I workaround setting this cookie to SameSite = Strict or another alternative to be protected from Cross Site Request Forgery?

Upvotes: 0

Views: 1025

Answers (1)

ndm
ndm

Reputation: 60493

In CakePHP 3.9.3 support for samesite with CSRF cookies has been added, you'd have to switch to the CSRF protection middleware though.

If you can't upgrade, then you'll a bit of custom code, namely a custom/extended CSRF component that accepts further options for the attribute, and a custom/exteneded response object that creates cookies with that attribute accordingly.

In PHP versions earlier than PHP 7.3, you can, respectively must inject the SameSite attribute by utilizing the cookie path hack, which consists of appending further cookie attributes to the path, by simply closing the path of with a semicolon. In PHP versions as of PHP 7.3 you would use the as of then supported samesite for setcookie().

btw, for session cookies you'd modify your session.cookie_path or session.cookie_samesite PHP INI options accordingly, and other places in CakePHP that set cookies would possibly need to be adapted too, for example the cookie component, even if your app doesn't use it, it might be used by 3rd party plugins.

Example:

<?php
// in src/Controller/Component/CsrfComponent.php
namespace App\Controller\Component;

use Cake\Controller\ComponentRegistry;
use Cake\Http\Response;
use Cake\Http\ServerRequest;

class CsrfComponent extends \Cake\Controller\Component\CsrfComponent
{
    public function __construct(ComponentRegistry $registry, array $config = [])
    {
        // Use Lax by default
        $config += [
            'samsite' => 'Lax',
        ];
    
        parent::__construct($registry, $config);
    }
    
    protected function _setCookie(ServerRequest $request, Response $response)
    {
        parent::_setCookie($request, $response);

        // Add samesite option to the cookie that has been created by the parent
        $cookie = $response->cookie($this->getConfig('cookieName'));
        $cookie['samesite'] = $this->getConfig('samesite');

        $response->cookie($cookie);
    }
}

https://github.com/cakephp/cakephp/blob/3.4.14/src/Controller/Component/CsrfComponent.php#L125

// in src/Http/Response.php
namespace App\Http;

class Response extends \Cake\Http\Response
{
    protected function _setCookies()
    {
        foreach ($this->_cookies as $name => $c) {
            if (version_compare(PHP_VERSION, '7.3.0', '<')) {
                // Use regular syntax (with possible path hack) in case
                // no samesite has been set, or the PHP version doesn't
                // support the samesite option.
                
                if (isset($c['samesite'])) {
                    $c['path'] .= '; SameSite=' . $c['samesite'];
                }
                
                setcookie(
                    $name,
                    $c['value'],
                    $c['expire'],
                    $c['path'],
                    $c['domain'],
                    $c['secure'],
                    $c['httpOnly']
                );
            } else {
                setcookie($name, $c['value'], [
                    'expires' => $c['expire'],
                    'path' => $c['path'],
                    'domain' => $c['domain'],
                    'secure' => $c['secure'],
                    'httponly' => $c['httpOnly'],
                    'samesite' => $c['samesite'],
                ]);
            }
        }
    }
}

https://github.com/cakephp/cakephp/blob/3.4.14/src/Http/Response.php#L540

Inject the custom response object in your AppController's constructor:

// in src/Controller/AppController.php

use Cake\Http\Response;
use Cake\Http\ServerRequest;

// ...

class AppController extends Controller
{
    // ...

    public function __construct(
        ServerRequest $request = null,
        Response $response = null,
        $name = null,
        $eventManager = null,
        $components = null
    ) {
        if ($response !== null) {
            throw new \InvalidArgumentException(
                'This should not happen, we want to use a custom response class.'
            );
        }
        $response = new \App\Http\Response();
    
        parent::__construct($request, $response, $name, $eventManager, $components);
    }

    // ...
}

Finally alias the Csrf component with the custom component class and set your samesite config:

// in src/Controller/AppController.php

// ...

class AppController extends Controller
{
    // ...
    
    public function initialize()
    {
        parent::initialize();

        // ...
        $this->loadComponent('Csrf', [
            'className' => \App\Controller\Component\CsrfComponent::class,
            'secure' => true,
            'httpOnly' => true,
            'samesite' => 'Strict',
        ]);
    }

    // ...
}

On a closing note it should be noted that in later CakePHP 3.x versions the cookie arrays have been replaced with cookie objects, so that would require changes accordingly, but since you can't upgrade, this should be fine, and once you decide to upgrade, shoot for the stars and upgrade to the latest version.

Upvotes: 1

Related Questions