Security configuration enhancements in the application (#1208)

This commit is contained in:
Alex Tselegidis 2022-02-23 14:54:41 +01:00
parent aeee91f4ed
commit bb71c97736
9 changed files with 209 additions and 19 deletions

View File

@ -65,7 +65,7 @@ $autoload['libraries'] = ['database', 'session'];
| $autoload['helper'] = array('url', 'file'); | $autoload['helper'] = array('url', 'file');
*/ */
$autoload['helper'] = ['custom_exceptions', 'url', 'file', 'language', 'asset', 'config', 'render']; $autoload['helper'] = ['custom_exceptions', 'url', 'file', 'language', 'asset', 'config', 'render', 'rate_limit', 'security'];
/* /*

View File

@ -367,7 +367,7 @@ $config['sess_regenerate_destroy'] = FALSE;
$config['cookie_prefix'] = ""; $config['cookie_prefix'] = "";
$config['cookie_domain'] = ""; $config['cookie_domain'] = "";
$config['cookie_path'] = "/"; $config['cookie_path'] = "/";
$config['cookie_secure'] = FALSE; $config['cookie_secure'] = strpos($config['base_url'], 'https') !== FALSE;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -457,6 +457,17 @@ $config['rewrite_short_tags'] = FALSE;
*/ */
$config['proxy_ips'] = ''; $config['proxy_ips'] = '';
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| Toggle the rate limiting feature in your application. Using rate limiting
| will control the number of requests a client can sent to the app.
|
*/
$config['rate_limiting'] = TRUE;
/* End of file config.php */ /* End of file config.php */
/* Location: ./application/config/config.php */ /* Location: ./application/config/config.php */

View File

@ -88,6 +88,23 @@ class Appointments extends EA_Controller {
$available_providers[$index] = $stripped_data; $available_providers[$index] = $stripped_data;
} }
// Remove the data that are not needed inside the $available_services array.
foreach ($available_services as $index => $service)
{
$stripped_data = [
'id' => $service['id'],
'name' => $service['name'],
'duration' => $service['duration'],
'location' => $service['location'],
'price' => $service['price'],
'currency' => $service['currency'],
'description' => $service['description'],
'category_id' => $service['category_id'],
'category_name' => $service['category_name']
];
$available_services[$index] = $stripped_data;
}
// If an appointment hash is provided then it means that the customer is trying to edit a registered // If an appointment hash is provided then it means that the customer is trying to edit a registered
// appointment record. // appointment record.
if ($appointment_hash !== '') if ($appointment_hash !== '')
@ -133,9 +150,40 @@ class Appointments extends EA_Controller {
} }
$appointment = $results[0]; $appointment = $results[0];
$appointment = [
'id' => $appointment['id'],
'hash' => $appointment['hash'],
'start_datetime' => $appointment['start_datetime'],
'end_datetime' => $appointment['end_datetime'],
'id_services' => $appointment['id_services'],
'id_users_customer' => $appointment['id_users_customer'],
'id_users_provider' => $appointment['id_users_provider'],
'notes' => $appointment['notes']
];
$provider = $this->providers_model->get_row($appointment['id_users_provider']); $provider = $this->providers_model->get_row($appointment['id_users_provider']);
$provider = [
'id' => $provider['id'],
'first_name' => $provider['first_name'],
'last_name' => $provider['last_name'],
'services' => $provider['services'],
'timezone' => $provider['timezone']
];
$customer = $this->customers_model->get_row($appointment['id_users_customer']); $customer = $this->customers_model->get_row($appointment['id_users_customer']);
$customer = [
'id' => $customer['id'],
'first_name' => $customer['first_name'],
'last_name' => $customer['last_name'],
'timezone' => $customer['timezone'],
'address' => $customer['address'],
'city' => $customer['city'],
'zip_code' => $customer['zip_code']
];
$customer_token = md5(uniqid(mt_rand(), TRUE)); $customer_token = md5(uniqid(mt_rand(), TRUE));
// Save the token for 10 minutes. // Save the token for 10 minutes.
@ -197,6 +245,13 @@ class Appointments extends EA_Controller {
{ {
try try
{ {
$cancel_reason = $this->input->post('cancel_reason');
if ($this->input->method() !== 'post' || empty($cancel_reason))
{
show_error('Bad Request', 400);
}
// Check whether the appointment hash exists in the database. // Check whether the appointment hash exists in the database.
$appointments = $this->appointments_model->get_batch(['hash' => $appointment_hash]); $appointments = $this->appointments_model->get_batch(['hash' => $appointment_hash]);
@ -279,22 +334,25 @@ class Appointments extends EA_Controller {
$exceptions = $this->session->flashdata('book_success'); $exceptions = $this->session->flashdata('book_success');
$view = [ $view = [
'appointment_data' => $appointment, 'appointment_data' => [
'start_datetime' => $appointment['start_datetime'],
'end_datetime' => $appointment['end_datetime'],
],
'provider_data' => [ 'provider_data' => [
'id' => $provider['id'],
'first_name' => $provider['first_name'], 'first_name' => $provider['first_name'],
'last_name' => $provider['last_name'], 'last_name' => $provider['last_name'],
'email' => $provider['email'], 'email' => $provider['email'],
'timezone' => $provider['timezone'], 'timezone' => $provider['timezone'],
], ],
'customer_data' => [ 'customer_data' => [
'id' => $customer['id'],
'first_name' => $customer['first_name'], 'first_name' => $customer['first_name'],
'last_name' => $customer['last_name'], 'last_name' => $customer['last_name'],
'email' => $customer['email'], 'email' => $customer['email'],
'timezone' => $customer['timezone'], 'timezone' => $customer['timezone'],
], ],
'service_data' => $service, 'service_data' => [
'name' => $service['name'],
],
'company_name' => $company_name, 'company_name' => $company_name,
]; ];
@ -386,7 +444,7 @@ class Appointments extends EA_Controller {
* *
* @throws Exception * @throws Exception
*/ */
protected function search_any_provider($service_id, $date, $hour = null) protected function search_any_provider($service_id, $date, $hour = NULL)
{ {
$available_providers = $this->providers_model->get_available_providers(); $available_providers = $this->providers_model->get_available_providers();
@ -405,7 +463,7 @@ class Appointments extends EA_Controller {
// Check if the provider is available for the requested date. // Check if the provider is available for the requested date.
$available_hours = $this->availability->get_available_hours($date, $service, $provider); $available_hours = $this->availability->get_available_hours($date, $service, $provider);
if (count($available_hours) > $max_hours_count && (empty($hour) || in_array($hour, $available_hours, false))) if (count($available_hours) > $max_hours_count && (empty($hour) || in_array($hour, $available_hours, FALSE)))
{ {
$provider_id = $provider['id']; $provider_id = $provider['id'];
$max_hours_count = count($available_hours); $max_hours_count = count($available_hours);
@ -448,7 +506,7 @@ class Appointments extends EA_Controller {
$captcha_phrase = $this->session->userdata('captcha_phrase'); $captcha_phrase = $this->session->userdata('captcha_phrase');
// Validate the CAPTCHA string. // Validate the CAPTCHA string.
if ($require_captcha === '1' && $captcha_phrase !== $captcha) if ($require_captcha === '1' && strtolower($captcha_phrase) !== strtolower($captcha))
{ {
$this->output $this->output
->set_content_type('application/json') ->set_content_type('application/json')
@ -473,6 +531,16 @@ class Appointments extends EA_Controller {
$customer['language'] = config('language'); $customer['language'] = config('language');
$customer_id = $this->customers_model->add($customer); $customer_id = $this->customers_model->add($customer);
$appointment_start_instance = new DateTime($appointment['start_datetime']);
$appointment['end_datetime'] = $appointment_start_instance
->add(new DateInterval('PT' . $service['duration'] . 'M'))
->format('Y-m-d H:i:s');
if ( ! in_array($service['id'], $provider['services']))
{
throw new Exception('Invalid provider record selected for appointment.');
}
$appointment['id_users_customer'] = $customer_id; $appointment['id_users_customer'] = $customer_id;
$appointment['is_unavailable'] = (int)$appointment['is_unavailable']; // needs to be type casted $appointment['is_unavailable'] = (int)$appointment['is_unavailable']; // needs to be type casted
$appointment['id'] = $this->appointments_model->add($appointment); $appointment['id'] = $this->appointments_model->add($appointment);

View File

@ -55,6 +55,10 @@ class Backend_api extends EA_Controller {
{ {
$this->privileges = $this->roles_model->get_privileges($this->session->userdata('role_slug')); $this->privileges = $this->roles_model->get_privileges($this->session->userdata('role_slug'));
} }
else
{
show_error('Forbidden', 403);
}
} }
/** /**
@ -66,6 +70,11 @@ class Backend_api extends EA_Controller {
{ {
try try
{ {
if ($this->privileges[PRIV_APPOINTMENTS]['view'] == FALSE)
{
throw new Exception('You do not have the required privileges for this task.');
}
$start_date = $this->input->post('startDate') . ' 00:00:00'; $start_date = $this->input->post('startDate') . ' 00:00:00';
$end_date = $this->input->post('endDate') . ' 23:59:59'; $end_date = $this->input->post('endDate') . ' 23:59:59';
@ -1527,18 +1536,25 @@ class Backend_api extends EA_Controller {
{ {
try try
{ {
if ( ! $this->input->post('provider_id')) $provider_id = $this->input->post('provider_id');
if ( ! $provider_id)
{ {
throw new Exception('Provider id is required in order to fetch the google calendars.'); throw new Exception('Provider id is required in order to fetch the google calendars.');
} }
if ($this->privileges[PRIV_USERS]['view'] == FALSE && $provider_id !== $this->session->userdata('user_id'))
{
throw new Exception('You do not have the required privileges for this task.');
}
// Check if selected provider has sync enabled. // Check if selected provider has sync enabled.
$google_sync = $this->providers_model->get_setting('google_sync', $this->input->post('provider_id')); $google_sync = $this->providers_model->get_setting('google_sync', $provider_id);
if ($google_sync) if ($google_sync)
{ {
$google_token = json_decode($this->providers_model->get_setting('google_token', $google_token = json_decode($this->providers_model->get_setting('google_token',
$this->input->post('provider_id'))); $provider_id));
$this->google_sync->refresh_token($google_token->refresh_token); $this->google_sync->refresh_token($google_token->refresh_token);
$calendars = $this->google_sync->get_google_calendars(); $calendars = $this->google_sync->get_google_calendars();

View File

@ -45,9 +45,9 @@ class Google extends EA_Controller {
$CI = get_instance(); $CI = get_instance();
// The user must be logged in. // The user must be logged in.
if ($CI->session->userdata('user_id') == FALSE && is_cli() === FALSE) if ( ! $CI->session->userdata('user_id') && ! is_cli())
{ {
return; show_error('Forbidden', 403);
} }
if ($provider_id === NULL) if ($provider_id === NULL)
@ -204,7 +204,6 @@ class Google extends EA_Controller {
continue; continue;
} }
// Record doesn't exist in the Easy!Appointments, so add the event now. // Record doesn't exist in the Easy!Appointments, so add the event now.
$appointment = [ $appointment = [
'start_datetime' => $event_start->format('Y-m-d H:i:s'), 'start_datetime' => $event_start->format('Y-m-d H:i:s'),
@ -248,6 +247,11 @@ class Google extends EA_Controller {
*/ */
public function oauth($provider_id) public function oauth($provider_id)
{ {
if ( ! $this->session->userdata('user_id'))
{
show_error('Forbidden', 403);
}
// Store the provider id for use on the callback function. // Store the provider id for use on the callback function.
$this->session->set_userdata('oauth_provider_id', $provider_id); $this->session->set_userdata('oauth_provider_id', $provider_id);
@ -268,6 +272,11 @@ class Google extends EA_Controller {
*/ */
public function oauth_callback() public function oauth_callback()
{ {
if ( ! $this->session->userdata('user_id'))
{
show_error('Forbidden', 403);
}
$code = $this->input->get('code'); $code = $this->input->get('code');
if (empty($code)) if (empty($code))
@ -298,6 +307,4 @@ class Google extends EA_Controller {
$this->output->set_output('Sync provider id not specified.'); $this->output->set_output('Sync provider id not specified.');
} }
} }
} }

View File

@ -51,6 +51,7 @@
* @property User_model $user_model * @property User_model $user_model
* *
* @property Availability $availability * @property Availability $availability
* @property Captcha_builder $captcha_builder
* @property Google_Sync $google_sync * @property Google_Sync $google_sync
* @property Ics_file $ics_file * @property Ics_file $ics_file
* @property Notifications $notifications * @property Notifications $notifications
@ -66,6 +67,8 @@ class EA_Controller extends CI_Controller {
parent::__construct(); parent::__construct();
$this->configure_language(); $this->configure_language();
rate_limit($this->input->ip_address());
} }
/** /**

View File

@ -51,6 +51,7 @@
* @property User_model $user_model * @property User_model $user_model
* *
* @property Availability $availability * @property Availability $availability
* @property Captcha_builder $captcha_builder
* @property Google_Sync $google_sync * @property Google_Sync $google_sync
* @property Ics_file $ics_file * @property Ics_file $ics_file
* @property Notifications $notifications * @property Notifications $notifications

View File

@ -0,0 +1,84 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
* @link http://easyappointments.org
* @since v1.1.0
* ---------------------------------------------------------------------------- */
if ( ! function_exists('rate_limit'))
{
/**
* Rate-limit the application requests.
*
* Example:
*
* rate_limit($CI->input->ip_address(), 100, 300);
*
* @link https://github.com/alexandrugaidei-atomate/ratelimit-codeigniter-filebased
*
* @param string $ip Client IP address.
* @param int $max_requests Number of allowed requests, defaults to 100.
* @param int $duration In seconds, defaults to 5 minutes.
*/
function rate_limit($ip, $max_requests = 100, $duration = 300)
{
$CI =& get_instance();
$rate_limiting = $CI->config->item('rate_limiting');
if ( ! $rate_limiting)
{
return;
}
$CI->load->driver('cache', ['adapter' => 'file']);
$cache_key = str_replace(':', '', 'rate_limit_key_' . $ip);
$cache_remain_time_key = str_replace(':', '', 'rate_limit_tmp_' . $ip);
$current_time = date('Y-m-d H:i:s');
if ($CI->cache->get($cache_key) === FALSE) // First request
{
$current_time_plus = date('Y-m-d H:i:s', strtotime('+' . $duration . ' seconds'));
$CI->cache->save($cache_key, 1, $duration);
$CI->cache->save($cache_remain_time_key, $current_time_plus, $duration * 2);
}
else // Consequent request
{
$requests = $CI->cache->get($cache_key);
$time_lost = $CI->cache->get($cache_remain_time_key);
if ($current_time > $time_lost)
{
$current_time_plus = date('Y-m-d H:i:s', strtotime('+' . $duration . ' seconds'));
$CI->cache->save($cache_key, 1, $duration);
$CI->cache->save($cache_remain_time_key, $current_time_plus, $duration * 2);
}
else
{
$CI->cache->save($cache_key, $requests + 1, $duration);
}
$requests = $CI->cache->get($cache_key);
if ($requests > $max_requests)
{
header('HTTP/1.0 429 Too Many Requests');
exit;
}
}
}
}

View File

@ -73,7 +73,7 @@ class Settings_model extends EA_Model {
if ($query->num_rows() > 0) if ($query->num_rows() > 0)
{ {
// Update setting // Update setting
if ( ! $this->db->update('settings', ['value' => $value], ['name' => $name])) if ( ! $this->db->update('settings', ['value' => xss_clean($value)], ['name' => $name]))
{ {
throw new Exception('Could not update database setting.'); throw new Exception('Could not update database setting.');
} }
@ -144,7 +144,7 @@ class Settings_model extends EA_Model {
foreach ($settings as $setting) foreach ($settings as $setting)
{ {
$this->db->where('name', $setting['name']); $this->db->where('name', $setting['name']);
if ( ! $this->db->update('settings', ['value' => $setting['value']])) if ( ! $this->db->update('settings', ['value' => xss_clean($setting['value'])]))
{ {
throw new Exception('Could not save setting (' . $setting['name'] throw new Exception('Could not save setting (' . $setting['name']
. ' - ' . $setting['value'] . ')'); . ' - ' . $setting['value'] . ')');