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 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']);
$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
*
* @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 Array $values =array() Additional values from UI
* @param int $rights null to delete, or new rights
* @param array $values Additional values from UI
* @param string $etemplate_exec_id to check against CSRF
* @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 {
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
* things go normally
* Delete a type over ajax.
*
* @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
$this->appname = $content['appname'];
$this->fields = Api\Storage\Customfields::get($content['appname'],true);

View File

@ -1568,10 +1568,13 @@ class admin_mail
* domain => mailLocalAddress,
* status => mail activation status('active'|'')
* )
* @param string $etemplate_exec_id to check against CSRF
* @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!');
$response = Api\Json\Response::get();
if (($account = $GLOBALS['egw']->accounts->read($_data['id'])))

View File

@ -4,9 +4,8 @@
* @link http://www.egroupware.org
* @package filemanager
* @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
* @version $Id: app.js 56051 2016-05-06 07:58:37Z ralfbecker $
*/
/**
@ -36,7 +35,7 @@ app.classes.admin = AppJS.extend(
nm: null,
/**
* Refarence to div to hold AJAX loadable pages
* Reference to div to hold AJAX loadable pages
*
* {et2_box}
*/
@ -443,7 +442,7 @@ app.classes.admin = AppJS.extend(
break;
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;
default:
if (!_action.data.url)
@ -510,7 +509,7 @@ app.classes.admin = AppJS.extend(
var callback = function(_button_id, _value) {
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();
}.bind(this);
@ -674,7 +673,7 @@ app.classes.admin = AppJS.extend(
{
// Changed the account or location, remove previous or we
// 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();
}
id = [id];
@ -708,11 +707,11 @@ app.classes.admin = AppJS.extend(
// Remove any removed
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();
}
}
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();
}
},this),
@ -971,7 +970,7 @@ app.classes.admin = AppJS.extend(
value,
{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
var types = this.getRoot().getWidgetById('types');
@ -1025,7 +1024,6 @@ app.classes.admin = AppJS.extend(
*
* @param {egw_action} _action
* @param {array} _selected selected users
* @todo remove under construction message
*/
emailadminActiveAccounts: function (_action, _selected){
@ -1035,7 +1033,7 @@ app.classes.admin = AppJS.extend(
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){
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 $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)
{
@ -310,13 +313,10 @@ class Auth
$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 = '';
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;
}

View File

@ -142,9 +142,10 @@ class Request
* the sesison to constantly grow).
*
* @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))
{
@ -192,7 +193,7 @@ class Request
//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']);
$global = false;
@ -228,6 +229,33 @@ class 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
*
@ -381,17 +409,15 @@ class Request
* creates a new unique request-id
*
* @return string
* @throws \Exception if it was not possible to gather sufficient entropy.
*/
static function request_id()
{
// 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']);
// generate random token (using oppenssl if available otherwise mt_rand based Auth::randomstring)
$token = function_exists('openssl_random_pseudo_bytes') ?
// 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);
// replace + with _ to not run into url-encoding issues when used in a url
$token = str_replace('+', '_', base64_encode(random_bytes(32)));
return $GLOBALS['egw_info']['flags']['currentapp'].'_'.$userID.'_'.$token;
}