Merge pull request #4720 from octobercms/csrf_fix

Implement XSRF checking for AJAX handlers. Credit to @bennothommo @daftspunk.
This commit is contained in:
Luke Towers 2019-10-28 13:37:25 -06:00 committed by GitHub
commit 0de4f1903f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 9 deletions

View File

@ -6,12 +6,14 @@ use App;
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 System\Twig\Extension as SystemTwigExtension;
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;
/**
@ -133,11 +136,29 @@ class Controller
* Finds and serves the requested page.
* If the page cannot be found, returns the page with the URL /404.
* If the /404 page doesn't exist, returns the system 404 page.
* * If the parameter is omitted, the current URL used.
*
* @param string $url Specifies the requested page URL.
* If the parameter is omitted, the current URL used.
* @return string Returns the processed page content.
* @return Response Returns the processed page content.
*/
public function run($url = '/')
{
$response = $this->runInternal($url);
if (Config::get('cms.enableCsrfProtection') && $response instanceof \Symfony\Component\HttpFoundation\Response) {
$this->addXsrfCookie($response);
}
return $response;
}
/**
* Process the request internally
*
* @param string $url Specifies the requested page URL.
* @return Response Returns the processed page content.
*/
protected function runInternal($url = '/')
{
if ($url === null) {
$url = Request::path();
@ -147,6 +168,13 @@ class Controller
$url = '/';
}
/*
* Check security token.
*/
if (!$this->verifyCsrfToken()) {
return Response::make(Lang::get('system::lang.page.invalid_token.label'), 403);
}
/*
* Hidden page
*/
@ -802,13 +830,6 @@ class Controller
*/
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 +1604,32 @@ class Controller
// Security
//
/**
* Adds anti-CSRF cookie.
* Adds a cookie with a token for CSRF checks to the response.
* @return Response
*/
protected function addXsrfCookie(\Illuminate\Http\Response $response)
{
$config = Config::get('session');
$response->headers->setCookie(
new Cookie(
'XSRF-TOKEN',
Session::token(),
Carbon::now()->addMinutes((int) $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 +1648,10 @@ class Controller
$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;
}

View File

@ -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=getXSRFToken()
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 getXSRFToken(){var cookieValue=null
if(document.cookie&&document.cookie!=''){var cookies=document.cookie.split(';')
for(var i=0;i<cookies.length;i++){var cookie=jQuery.trim(cookies[i])
if(cookie.substring(0,11)==('XSRF-TOKEN'+'=')){cookieValue=decodeURIComponent(cookie.substring(11))
break}}}
return cookieValue}
$(document).on('change','select[data-request], input[type=radio][data-request], input[type=checkbox][data-request], input[type=file][data-request]',function documentOnChange(){$(this).request()})
$(document).on('click','a[data-request], button[data-request], input[type=button][data-request], input[type=submit][data-request]',function documentOnClick(e){e.preventDefault()
$(this).request()

View File

@ -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=getXSRFToken()
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 getXSRFToken(){var cookieValue=null
if(document.cookie&&document.cookie!=''){var cookies=document.cookie.split(';')
for(var i=0;i<cookies.length;i++){var cookie=jQuery.trim(cookies[i])
if(cookie.substring(0,11)==('XSRF-TOKEN'+'=')){cookieValue=decodeURIComponent(cookie.substring(11))
break}}}
return cookieValue}
$(document).on('change','select[data-request], input[type=radio][data-request], input[type=checkbox][data-request], input[type=file][data-request]',function documentOnChange(){$(this).request()})
$(document).on('click','a[data-request], button[data-request], input[type=button][data-request], input[type=submit][data-request]',function documentOnClick(e){e.preventDefault()
$(this).request()

View File

@ -68,6 +68,11 @@ if (window.jQuery.request !== undefined) {
requestHeaders['X-OCTOBER-REQUEST-FLASH'] = 1
}
var csrfToken = getXSRFToken()
if (csrfToken) {
requestHeaders['X-XSRF-TOKEN'] = csrfToken
}
/*
* Request data
*/
@ -465,6 +470,21 @@ if (window.jQuery.request !== undefined) {
}
}
function getXSRFToken() {
var cookieValue = null
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';')
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i])
if (cookie.substring(0, 11) == ('XSRF-TOKEN' + '=')) {
cookieValue = decodeURIComponent(cookie.substring(11))
break
}
}
}
return cookieValue
}
$(document).on('change', 'select[data-request], input[type=radio][data-request], input[type=checkbox][data-request], input[type=file][data-request]', function documentOnChange() {
$(this).request()
})