diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index 239c46a33a..a07354a375 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -6,12 +6,14 @@ use View; use Lang; use Flash; +use Crypt; use Config; use Session; use Request; use Response; use Exception; use BackendAuth; +use Carbon\Carbon; use Twig\Environment as TwigEnvironment; use Twig\Cache\FilesystemCache as TwigCacheFilesystem; use Cms\Twig\Loader as TwigLoader; @@ -25,6 +27,7 @@ use October\Rain\Exception\AjaxException; use October\Rain\Exception\ValidationException; use October\Rain\Parse\Bracket as TextParser; +use Symfony\Component\HttpFoundation\Cookie; use Illuminate\Http\RedirectResponse; /** @@ -147,6 +150,13 @@ public function run($url = '/') $url = '/'; } + /* + * Check security token. + */ + if (!$this->verifyCsrfToken()) { + return Response::make(Lang::get('system::lang.page.invalid_token.label'), 403); + } + /* * Hidden page */ @@ -256,7 +266,13 @@ public function run($url = '/') return $result; } - return Response::make($result, $this->statusCode); + $response = Response::make($result, $this->statusCode); + + if (Config::get('cms.enableCsrfProtection')) { + $this->addCsrfCookie($response); + } + + return $response; } /** @@ -802,13 +818,6 @@ protected function execAjaxHandlers() */ protected function runAjaxHandler($handler) { - /* - * Check security token. - */ - if (!$this->verifyCsrfToken()) { - return Response::make(Lang::get('system::lang.page.invalid_token.label'), 403); - } - /** * @event cms.ajax.beforeRunHandler * Provides an opportunity to modify an AJAX request @@ -1583,6 +1592,32 @@ protected function setComponentPropertiesFromParams($component, $parameters = [] // Security // + /** + * Adds anti-CSRF cookie. + * Adds a cookie with a token for CSRF checks to the response. + * @return void + */ + protected function addCsrfCookie(\Illuminate\Http\Response $response) + { + $config = Config::get('session'); + + $response->headers->setCookie( + new Cookie( + 'XSRF-TOKEN', + Session::token(), + Carbon::now()->addSeconds(60 * $config['lifetime'])->getTimestamp(), + $config['path'], + $config['domain'], + $config['secure'], + false, + false, + $config['same_site'] ?? null + ) + ); + + return $response; + } + /** * Checks the request data / headers for a valid CSRF token. * Returns false if a valid token is not found. Override this @@ -1601,6 +1636,10 @@ protected function verifyCsrfToken() $token = Request::input('_token') ?: Request::header('X-CSRF-TOKEN'); + if (!$token && $header = Request::header('X-XSRF-TOKEN')) { + $token = Crypt::decrypt($header); + } + if (!strlen($token) || !strlen(Session::token())) { return false; } diff --git a/modules/system/assets/js/framework-min.js b/modules/system/assets/js/framework-min.js index 43fe38ba46..eb8f52fbc8 100644 --- a/modules/system/assets/js/framework-min.js +++ b/modules/system/assets/js/framework-min.js @@ -14,6 +14,8 @@ useFiles=false} if($.type(loading)=='string'){loading=$(loading)} var requestHeaders={'X-OCTOBER-REQUEST-HANDLER':handler,'X-OCTOBER-REQUEST-PARTIALS':this.extractPartials(options.update)} if(useFlash){requestHeaders['X-OCTOBER-REQUEST-FLASH']=1} +var csrfToken=getCSRFToken() +if(csrfToken){requestHeaders['X-XSRF-TOKEN']=csrfToken} var requestData,inputName,data={} $.each($el.parents('[data-request-data]').toArray().reverse(),function extendRequest(){$.extend(data,paramToObj('data-request-data',$(this).data('request-data')))}) if($el.is(':input')&&!$form.length){inputName=$el.attr('name') @@ -112,6 +114,12 @@ function paramToObj(name,value){if(value===undefined)value='' if(typeof value=='object')return value try{return ocJSON("{"+value+"}")} catch(e){throw new Error('Error parsing the '+name+' attribute value. '+e)}} +function getCSRFToken(){var cookieValue=null +if(document.cookie&&document.cookie!=''){var cookies=document.cookie.split(';') +for(var i=0;i