use etemplate-exec-id as CSRF token for ajax requests

This commit is contained in:
Ralf Becker 2020-01-29 11:08:44 +01:00
parent 2045c08e54
commit d95894d530
7 changed files with 70 additions and 33 deletions

View File

@ -339,9 +339,12 @@ class admin_account
* *
* @param int $account_id * @param int $account_id
* @param String[] $data Optional data * @param String[] $data Optional data
* @param string $etemplate_exec_id to check against CSRF
*/ */
public static function ajax_delete_group($account_id, $data) public static function ajax_delete_group($account_id, $data, $etemplate_exec_id)
{ {
Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
$cmd = new admin_cmd_delete_account(Api\Accounts::id2name(Api\Accounts::id2name($account_id)), null, false, (array)$data['admin_cmd']); $cmd = new admin_cmd_delete_account(Api\Accounts::id2name(Api\Accounts::id2name($account_id)), null, false, (array)$data['admin_cmd']);
$msg = $cmd->run(); $msg = $cmd->run();

View File

@ -335,12 +335,15 @@ class admin_acl
* Checks access and throws an exception, if a change is attempted without proper access * Checks access and throws an exception, if a change is attempted without proper access
* *
* @param string|array $ids "$app:$account:$location" string used as row-id in list * @param string|array $ids "$app:$account:$location" string used as row-id in list
* @param int $rights =null null to delete, or new rights * @param int $rights null to delete, or new rights
* @param Array $values =array() Additional values from UI * @param array $values Additional values from UI
* @param string $etemplate_exec_id to check against CSRF
* @throws Api\Exception\NoPermission * @throws Api\Exception\NoPermission
*/ */
public static function ajax_change_acl($ids, $rights=null, $values = array()) public static function ajax_change_acl($ids, $rights, $values, $etemplate_exec_id)
{ {
Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
try { try {
foreach((array)$ids as $id) foreach((array)$ids as $id)
{ {

View File

@ -288,13 +288,17 @@ class admin_customfields
} }
/** /**
* Delete a type over ajax. Used when Policy is involved, otherwise * Delete a type over ajax.
* things go normally
* *
* @param Array $content * Used when Policy is involved, otherwise things go normally
*
* @param array $content
* @param string $etemplate_exec_id to check against CSRF
*/ */
public function ajax_delete_type($content) public function ajax_delete_type($content, $etemplate_exec_id)
{ {
Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
// Read fields // Read fields
$this->appname = $content['appname']; $this->appname = $content['appname'];
$this->fields = Api\Storage\Customfields::get($content['appname'],true); $this->fields = Api\Storage\Customfields::get($content['appname'],true);

View File

@ -1568,10 +1568,13 @@ class admin_mail
* domain => mailLocalAddress, * domain => mailLocalAddress,
* status => mail activation status('active'|'') * status => mail activation status('active'|'')
* ) * )
* @param string $etemplate_exec_id to check against CSRF
* @return json response * @return json response
*/ */
public function ajax_activeAccounts($_data) public function ajax_activeAccounts($_data, $etemplate_exec_id)
{ {
Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
if (!$this->is_admin) die('no rights to be here!'); if (!$this->is_admin) die('no rights to be here!');
$response = Api\Json\Response::get(); $response = Api\Json\Response::get();
if (($account = $GLOBALS['egw']->accounts->read($_data['id']))) if (($account = $GLOBALS['egw']->accounts->read($_data['id'])))

View File

@ -4,9 +4,8 @@
* @link http://www.egroupware.org * @link http://www.egroupware.org
* @package filemanager * @package filemanager
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de> * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2013-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de> * @copyright (c) 2013-20 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id: app.js 56051 2016-05-06 07:58:37Z ralfbecker $
*/ */
/** /**
@ -36,7 +35,7 @@ app.classes.admin = AppJS.extend(
nm: null, nm: null,
/** /**
* Refarence to div to hold AJAX loadable pages * Reference to div to hold AJAX loadable pages
* *
* {et2_box} * {et2_box}
*/ */
@ -443,7 +442,7 @@ app.classes.admin = AppJS.extend(
break; break;
case 'delete': case 'delete':
this.egw.json('admin_account::ajax_delete_group', [account_id, _action.data]).sendRequest(); this.egw.json('admin_account::ajax_delete_group', [account_id, _action.data, this.et2._inst.etemplate_exec_id]).sendRequest();
break; break;
default: default:
if (!_action.data.url) if (!_action.data.url)
@ -510,7 +509,7 @@ app.classes.admin = AppJS.extend(
var callback = function(_button_id, _value) { var callback = function(_button_id, _value) {
if(_button_id != et2_dialog.OK_BUTTON) return; if(_button_id != et2_dialog.OK_BUTTON) return;
var request = egw.json(className+'::ajax_change_acl', [ids,null,_value], this._acl_callback,this,false,this) var request = egw.json(className+'::ajax_change_acl', [ids, null, _value, this.et2._inst.etemplate_exec_id], this._acl_callback,this,false,this)
.sendRequest(); .sendRequest();
}.bind(this); }.bind(this);
@ -674,7 +673,7 @@ app.classes.admin = AppJS.extend(
{ {
// Changed the account or location, remove previous or we // Changed the account or location, remove previous or we
// get a new line instead of an edit // get a new line instead of an edit
this.egw.json(className+'::ajax_change_acl', [content.id, 0], null,this,false,this) this.egw.json(className+'::ajax_change_acl', [content.id, 0, [], this.et2._inst.etemplate_exec_id], null,this,false,this)
.sendRequest(); .sendRequest();
} }
id = [id]; id = [id];
@ -708,11 +707,11 @@ app.classes.admin = AppJS.extend(
// Remove any removed // Remove any removed
if(removed.length > 0) if(removed.length > 0)
{ {
this.egw.json(className+'::ajax_change_acl', [removed, 0], callback ? callback : this._acl_callback,this,false,this) this.egw.json(className+'::ajax_change_acl', [removed, 0, [], this.et2._inst.etemplate_exec_id], callback ? callback : this._acl_callback,this,false,this)
.sendRequest(); .sendRequest();
} }
} }
this.egw.json(className+'::ajax_change_acl', [id, rights, _value], callback ? callback : this._acl_callback,this,false,this) this.egw.json(className+'::ajax_change_acl', [id, rights, _value, this.et2._inst.etemplate_exec_id], callback ? callback : this._acl_callback,this,false,this)
.sendRequest(); .sendRequest();
} }
},this), },this),
@ -971,7 +970,7 @@ app.classes.admin = AppJS.extend(
value, value,
{appname: this.getRoot().getArrayMgr('content').getEntry('content_types[appname]')} {appname: this.getRoot().getArrayMgr('content').getEntry('content_types[appname]')}
); );
egw.json('admin.admin_customfields.ajax_delete_type', [values]).sendRequest(); egw.json('admin.admin_customfields.ajax_delete_type', [values, this.getInstanceManager().etemplate_exec_id]).sendRequest();
// Immediately remove the type // Immediately remove the type
var types = this.getRoot().getWidgetById('types'); var types = this.getRoot().getWidgetById('types');
@ -1025,7 +1024,6 @@ app.classes.admin = AppJS.extend(
* *
* @param {egw_action} _action * @param {egw_action} _action
* @param {array} _selected selected users * @param {array} _selected selected users
* @todo remove under construction message
*/ */
emailadminActiveAccounts: function (_action, _selected){ emailadminActiveAccounts: function (_action, _selected){
@ -1035,7 +1033,7 @@ app.classes.admin = AppJS.extend(
for (var i=0;i< Object.keys(_selected).length;i++) for (var i=0;i< Object.keys(_selected).length;i++)
{ {
accounts[i] = {id:_selected[i]['id'].split('::')[1],qouta:"", domain:"", status:_action.id == 'active'?_action.id:''}; accounts[i] = [{id:_selected[i]['id'].split('::')[1],qouta:"", domain:"", status:_action.id == 'active'?_action.id:''}, this.et2._inst.etemplate_exec_id];
} }
var callbackDialog = function (btn){ var callbackDialog = function (btn){
if (btn === et2_dialog.YES_BUTTON) if (btn === et2_dialog.YES_BUTTON)

View File

@ -291,9 +291,12 @@ class Auth
} }
/** /**
* return a random string of letters [0-9a-zA-Z] of size $size * return a random string of size $size either just alphanumeric or with special chars
* *
* @param $size int-size of random string to return * @param $size int-size of random string to return
* @param $use_specialchars =false false: only letters and numbers, true: incl. special chars
* @return string
* @throws \Exception if it was not possible to gather sufficient entropy.
*/ */
static function randomstring($size, $use_specialchars=false) static function randomstring($size, $use_specialchars=false)
{ {
@ -310,13 +313,10 @@ class Auth
$random_char = array_merge($random_char, str_split(str_replace('\\', '', self::SPECIALCHARS)), $random_char); $random_char = array_merge($random_char, str_split(str_replace('\\', '', self::SPECIALCHARS)), $random_char);
} }
// use cryptographically secure random_int available in PHP 7+
$func = function_exists('random_int') ? 'random_int' : 'mt_rand';
$s = ''; $s = '';
for ($i=0; $i < $size; $i++) for ($i=0; $i < $size; $i++)
{ {
$s .= $random_char[$func(0, count($random_char)-1)]; $s .= $random_char[random_int(0, count($random_char)-1)];
} }
return $s; return $s;
} }

View File

@ -142,9 +142,10 @@ class Request
* the sesison to constantly grow). * the sesison to constantly grow).
* *
* @param string $id =null * @param string $id =null
* @return Request * @param bool $handle_not_found =true true: handle not found by trying to redirect, false: just return null
* @return Request|null null if Request not found and $handle_not_found === false
*/ */
public static function read($id=null) public static function read($id=null, $handle_not_found=true)
{ {
if (is_null(self::$request_class)) if (is_null(self::$request_class))
{ {
@ -192,7 +193,7 @@ class Request
//error_log(__METHOD__."() size of request = ".bytes($id)); //error_log(__METHOD__."() size of request = ".bytes($id));
} }
} }
if (!$request) // eT2 request/session expired if (!$request && $handle_not_found) // eT2 request/session expired
{ {
list($app) = explode('.', $_GET['menuaction']); list($app) = explode('.', $_GET['menuaction']);
$global = false; $global = false;
@ -228,6 +229,33 @@ class Request
return $request; return $request;
} }
/**
* CSRF check using an etemplate-exec-id
*
* If eTemplate request object could not be read, the function will NOT return,
* but send an Ajax error response and exit or die with the error-message!
*
* @param string $id etemplate-exec-id
* @param string $caller calling method to log
* @param array $args =[] arguments to log
* @throws Api\Json\Exception
*/
public static function csrfCheck($id, $caller, $args=[])
{
if (!self::read($id, false)) // false: do NOT handle not found, but return null
{
error_log(__METHOD__."('$id', $caller, ".json_encode($args).") called with invalid/expired etemplate_exec_id: possible CSRF detected from IP ".$_SERVER['REMOTE_ADDR'].' to '.$_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI']);
$msg = lang('Request could not be processed, please reload your window (press F5 or Cmd R)!');
if (Api\Json\Request::isJSONRequest())
{
Api\Json\Response::get()->message($msg, 'error');
exit;
}
die($msg);
}
}
/** /**
* Private constructor to force the instancation of this class only via it's static factory method read * Private constructor to force the instancation of this class only via it's static factory method read
* *
@ -381,17 +409,15 @@ class Request
* creates a new unique request-id * creates a new unique request-id
* *
* @return string * @return string
* @throws \Exception if it was not possible to gather sufficient entropy.
*/ */
static function request_id() static function request_id()
{ {
// replace url-unsafe chars with _ to not run into url-encoding issues when used in a url // replace url-unsafe chars with _ to not run into url-encoding issues when used in a url
$userID = preg_replace('/[^a-z0-9_\\.@-]/i', '_', $GLOBALS['egw_info']['user']['account_lid']); $userID = preg_replace('/[^a-z0-9_\\.@-]/i', '_', $GLOBALS['egw_info']['user']['account_lid']);
// generate random token (using oppenssl if available otherwise mt_rand based Auth::randomstring) // replace + with _ to not run into url-encoding issues when used in a url
$token = function_exists('openssl_random_pseudo_bytes') ? $token = str_replace('+', '_', base64_encode(random_bytes(32)));
// replace + with _ to not run into url-encoding issues when used in a url
str_replace('+', '_', base64_encode(openssl_random_pseudo_bytes(32))) :
\EGroupware\Api\Auth::randomstring(44);
return $GLOBALS['egw_info']['flags']['currentapp'].'_'.$userID.'_'.$token; return $GLOBALS['egw_info']['flags']['currentapp'].'_'.$userID.'_'.$token;
} }