From 544d57ca46bc13d19d2831d53ae131f74062485b Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 1 May 2014 06:26:09 +0000 Subject: [PATCH] prevent CSRF for setup and admin site configuration --- admin/inc/class.uiconfig.inc.php | 9 ++- phpgwapi/inc/class.egw_csrf.inc.php | 71 +++++++++++++++++++ setup/account_migration.php | 7 ++ setup/admin_account.php | 8 +++ setup/applications.php | 7 ++ setup/config.php | 11 ++- setup/db_backup.php | 7 ++ setup/index.php | 2 + setup/templates/default/account_migration.tpl | 1 + setup/templates/default/admin_account.tpl | 1 + setup/templates/default/applications.tpl | 3 +- setup/templates/default/config_pre_script.tpl | 1 + setup/templates/default/db_backup.tpl | 1 + setup/templates/default/setup_db_blocks.tpl | 1 + 14 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 phpgwapi/inc/class.egw_csrf.inc.php diff --git a/admin/inc/class.uiconfig.inc.php b/admin/inc/class.uiconfig.inc.php index bb4202bac1..49176c8e02 100644 --- a/admin/inc/class.uiconfig.inc.php +++ b/admin/inc/class.uiconfig.inc.php @@ -21,6 +21,12 @@ class uiconfig // allowing inline js egw_framework::csp_script_src_attrs('unsafe-inline'); + // for POST requests validate CSRF token (or terminate request) + if ($_SERVER['REQUEST_METHOD'] == 'POST') + { + egw_csrf::validate($_POST['csrf_token'], __CLASS__); + } + if (empty($_GET['appname']) && isset($params['appname'])) { $_appname = $params['appname']; @@ -172,7 +178,8 @@ class uiconfig $t->set_var('th_text', $GLOBALS['egw_info']['theme']['th_text']); $t->set_var('row_on', $GLOBALS['egw_info']['theme']['row_on']); $t->set_var('row_off', $GLOBALS['egw_info']['theme']['row_off']); - $t->set_var('hidden_vars',''); + $t->set_var('hidden_vars', html::input_hidden('referer', $referer). + html::input_hidden('csrf_token', egw_csrf::token(__CLASS__))); $vars = $t->get_undefined('body'); diff --git a/phpgwapi/inc/class.egw_csrf.inc.php b/phpgwapi/inc/class.egw_csrf.inc.php new file mode 100644 index 0000000000..06eab8493a --- /dev/null +++ b/phpgwapi/inc/class.egw_csrf.inc.php @@ -0,0 +1,71 @@ + + * @copyright (c) 2014 by Ralf Becker + * @version $Id$ + */ + +/** + * Class supplying methods to prevent successful CSRF by requesting a random token, + * stored on server and validated when request get posted. + * + * CSRF token generation used openssl_random_pseudo_bytes, if available, otherwise + * mt_rand based auth::randomstring is used. + * + * CSRF tokens are stored (incl. optional purpose) in user session. + * + * If a token does not validate (incl. purpose, if specified in generation) + * the request will be imediatly terminated. + */ +class egw_csrf +{ + /** + * Get a CSRF token for an optional $purpose, which can be validated + * + * @param mixed $_purpose=true if given it need to be used in validate too! (It must NOT be NULL) + * @return string CSRF token + */ + public static function token($_purpose=true) + { + if (is_null($_purpose)) + { + throw new egw_exception_wrong_parameter(__METHOD__.'(NULL) $_purspose must NOT be NULL!'); + } + // generate random token (using oppenssl if available otherwise mt_rand based auth::randomstring) + $token = function_exists('openssl_random_pseudo_bytes') ? + base64_encode(openssl_random_pseudo_bytes(64)) : + auth::randomstring(64); + + // store it in session for later validation + egw_cache::setSession(__CLASS__, $token, $_purpose); + + return $token; + } + + /** + * Validate a CSRF token or teminate the request + * + * @param string $_token CSRF token generated with egw_csfr::token() + * @param string $_purpose=true optional purpose string passed to token method + * @param boolean $_delete_token=true true if token should be deleted after validation, it will validate no second time + */ + public static function validate($_token, $_purpose=true, $_delete_token=true) + { + $stored_purpose = egw_cache::getSession(__CLASS__, $_token); + + // if token and purpose dont validate, log and terminate request + if (!isset($stored_purpose) || $stored_purpose !== $_purpose) + { + error_log('CSRF detected from IP '.$_SERVER['REMOTE_ADDR'].' to '.$_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI']); + if ($_POST) error_log(array2string($_POST)); + // we are not throwing an exception here, but die, to not allow catching it! + die("CSRF detected, request terminated!"); + } + if ($_delete_token) egw_cache::unsetTree (__CLASS__, $_token); + } +} \ No newline at end of file diff --git a/setup/account_migration.php b/setup/account_migration.php index a1a74cb080..4b6edb8012 100644 --- a/setup/account_migration.php +++ b/setup/account_migration.php @@ -29,6 +29,13 @@ $setup_tpl->set_file(array( 'T_footer' => 'footer.tpl', 'T_alert_msg' => 'msg_alert_msg.tpl' )); +$setup_tpl->set_var('hidden_vars', html::input_hidden('csrf_token', egw_csrf::token(__FILE__))); + +// check CSRF token for POST requests with any content (setup uses empty POST to call it's modules!) +if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST) +{ + egw_csrf::validate($_POST['csrf_token'], __FILE__); +} // determine from where we migrate to what if (!is_object($GLOBALS['egw_setup']->db)) diff --git a/setup/admin_account.php b/setup/admin_account.php index 4c7afc28e0..41d901dced 100644 --- a/setup/admin_account.php +++ b/setup/admin_account.php @@ -26,6 +26,12 @@ if (strpos($_SERVER['PHP_SELF'],'admin_account.php') !== false) $error = ''; if ($_POST['submit']) { + // for POST (not GET or cli call via setup_cmd_admin) validate CSRF token + if ($_SERVER['REQUEST_METHOD'] == 'POST') + { + egw_csrf::validate($_POST['csrf_token'], __FILE__); + } + /* Posted admin data */ $passwd = get_var('passwd',Array('POST')); $passwd2 = get_var('passwd2',Array('POST')); @@ -83,6 +89,8 @@ if(!$_POST['submit'] || $error) $setup_tpl->set_var('create_demo_accounts',lang('Create demo accounts')); $setup_tpl->set_var('demo_desc',lang('The username/passwords are: demo/guest, demo2/guest and demo3/guest.')); + $setup_tpl->set_var('hidden_vars', html::input_hidden('csrf_token', egw_csrf::token(__FILE__))); + $setup_tpl->set_var('lang_submit',lang('Save')); $setup_tpl->set_var('lang_cancel',lang('Cancel')); $setup_tpl->pparse('out','T_admin_account'); diff --git a/setup/applications.php b/setup/applications.php index 78a1e8efbf..7e805d6e69 100644 --- a/setup/applications.php +++ b/setup/applications.php @@ -40,6 +40,13 @@ $setup_tpl->set_file(array( 'T_login_stage_header' => 'login_stage_header.tpl', 'T_setup_main' => 'applications.tpl' )); +$setup_tpl->set_var('hidden_vars', html::input_hidden('csrf_token', egw_csrf::token(__FILE__))); + +// check CSRF token for POST requests with any content (setup uses empty POST to call it's modules!) +if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST) +{ + egw_csrf::validate($_POST['csrf_token'], __FILE__); +} $setup_tpl->set_block('T_login_stage_header','B_multi_domain','V_multi_domain'); $setup_tpl->set_block('T_login_stage_header','B_single_domain','V_single_domain'); diff --git a/setup/config.php b/setup/config.php index 78be151d78..ada73afda6 100644 --- a/setup/config.php +++ b/setup/config.php @@ -32,6 +32,13 @@ $setup_tpl->set_file(array( 'T_config_pre_script' => 'config_pre_script.tpl', 'T_config_post_script' => 'config_post_script.tpl' )); +$setup_tpl->set_var('hidden_vars', html::input_hidden('csrf_token', egw_csrf::token(__FILE__))); + +// check CSRF token for POST requests with any content (setup uses empty POST to call it's modules!) +if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST) +{ + egw_csrf::validate($_POST['csrf_token'], __FILE__); +} /* Following to ensure windows file paths are saved correctly */ if (function_exists('get_magic_quotes_runtime') && get_magic_quotes_runtime()) @@ -132,16 +139,14 @@ $setup_tpl->pparse('out','T_config_pre_script'); /* Now parse each of the templates we want to show here */ class phpgw { - var $common; var $accounts; var $applications; var $db; } $GLOBALS['egw'] = new phpgw; -$GLOBALS['egw']->common =& CreateObject('phpgwapi.common'); $GLOBALS['egw']->db =& $GLOBALS['egw_setup']->db; -$t = CreateObject('phpgwapi.Template',$GLOBALS['egw']->common->get_tpl_dir('setup')); +$t = CreateObject('phpgwapi.Template', common::get_tpl_dir('setup')); $t->set_unknowns('keep'); $t->set_file(array('config' => 'config.tpl')); diff --git a/setup/db_backup.php b/setup/db_backup.php index 7c030920e0..7f06a625a6 100644 --- a/setup/db_backup.php +++ b/setup/db_backup.php @@ -49,6 +49,13 @@ $setup_tpl->set_file(array( 'T_footer' => 'footer.tpl', 'T_db_backup' => 'db_backup.tpl', )); +$setup_tpl->set_var('hidden_vars', html::input_hidden('csrf_token', egw_csrf::token(__FILE__))); + +// check CSRF token for POST requests with any content (setup uses empty POST to call it's modules!) +if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST) +{ + egw_csrf::validate($_POST['csrf_token'], __FILE__); +} $setup_tpl->set_block('T_db_backup','schedule_row','schedule_rows'); $setup_tpl->set_block('T_db_backup','set_row','set_rows'); diff --git a/setup/index.php b/setup/index.php index ab348775f9..3b4082b614 100644 --- a/setup/index.php +++ b/setup/index.php @@ -268,6 +268,7 @@ switch($GLOBALS['egw_info']['setup']['stage']['db']) $setup_tpl->set_var('V_db_filled_block',$db_filled_block); break; case 5: + $setup_tpl->set_var('hidden_vars', html::input_hidden('csrf_token', egw_csrf::token(__FILE__))); $setup_tpl->set_var('are_you_sure',lang('ARE YOU SURE?')); $setup_tpl->set_var('really_uninstall_all_applications',lang('REALLY Uninstall all applications')); $setup_tpl->set_var('dropwarn',lang('Your tables will be dropped and you will lose data')); @@ -292,6 +293,7 @@ switch($GLOBALS['egw_info']['setup']['stage']['db']) !preg_match('/^[0-9.a-z_]+$/i', $_POST['db_grant_host']) ? 'localhost' : $_POST['db_grant_host']); break; case 'drop': + egw_csrf::validate($_POST['csrf_token'], __FILE__); $setup_info = $GLOBALS['egw_setup']->detection->get_versions($setup_info); $setup_info = $GLOBALS['egw_setup']->process->droptables($setup_info); break; diff --git a/setup/templates/default/account_migration.tpl b/setup/templates/default/account_migration.tpl index d0c1c243e4..65e5bc6baf 100644 --- a/setup/templates/default/account_migration.tpl +++ b/setup/templates/default/account_migration.tpl @@ -1,5 +1,6 @@
+ {hidden_vars} diff --git a/setup/templates/default/admin_account.tpl b/setup/templates/default/admin_account.tpl index 1a155a5392..5317b387ff 100644 --- a/setup/templates/default/admin_account.tpl +++ b/setup/templates/default/admin_account.tpl @@ -1,5 +1,6 @@ +{hidden_vars}
{description}
diff --git a/setup/templates/default/applications.tpl b/setup/templates/default/applications.tpl index 1e6511311f..f0a148c556 100644 --- a/setup/templates/default/applications.tpl +++ b/setup/templates/default/applications.tpl @@ -15,7 +15,7 @@ function check_all(which) { document.apps.elements[i].checked = true; } - } + } } } // --> @@ -29,6 +29,7 @@ function check_all(which)
+{hidden_vars} diff --git a/setup/templates/default/config_pre_script.tpl b/setup/templates/default/config_pre_script.tpl index 12346c2e59..b7eac5edcb 100644 --- a/setup/templates/default/config_pre_script.tpl +++ b/setup/templates/default/config_pre_script.tpl @@ -1,5 +1,6 @@ +{hidden_vars}
diff --git a/setup/templates/default/db_backup.tpl b/setup/templates/default/db_backup.tpl index 537a18557a..5372e47593 100644 --- a/setup/templates/default/db_backup.tpl +++ b/setup/templates/default/db_backup.tpl @@ -70,6 +70,7 @@ function sort_table(id) +{hidden_vars}
diff --git a/setup/templates/default/setup_db_blocks.tpl b/setup/templates/default/setup_db_blocks.tpl index 2e2ba788bc..4e35d758af 100644 --- a/setup/templates/default/setup_db_blocks.tpl +++ b/setup/templates/default/setup_db_blocks.tpl @@ -142,6 +142,7 @@
+ {hidden_vars} {dropwarn}