Security configuration enhancements in the application (#1208)

This commit is contained in:
Alex Tselegidis 2022-02-23 14:54:41 +01:00
parent 886343f80c
commit 384d442409
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'] = ['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_domain'] = "";
$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'] = '';
/*
|--------------------------------------------------------------------------
| 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 */
/* Location: ./application/config/config.php */

View file

@ -88,6 +88,23 @@ class Appointments extends EA_Controller {
$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
// appointment record.
if ($appointment_hash !== '')
@ -133,9 +150,40 @@ class Appointments extends EA_Controller {
}
$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 = [
'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 = [
'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));
// Save the token for 10 minutes.
@ -197,6 +245,13 @@ class Appointments extends EA_Controller {
{
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.
$appointments = $this->appointments_model->get_batch(['hash' => $appointment_hash]);
@ -279,22 +334,25 @@ class Appointments extends EA_Controller {
$exceptions = $this->session->flashdata('book_success');
$view = [
'appointment_data' => $appointment,
'appointment_data' => [
'start_datetime' => $appointment['start_datetime'],
'end_datetime' => $appointment['end_datetime'],
],
'provider_data' => [
'id' => $provider['id'],
'first_name' => $provider['first_name'],
'last_name' => $provider['last_name'],
'email' => $provider['email'],
'timezone' => $provider['timezone'],
],
'customer_data' => [
'id' => $customer['id'],
'first_name' => $customer['first_name'],
'last_name' => $customer['last_name'],
'email' => $customer['email'],
'timezone' => $customer['timezone'],
],
'service_data' => $service,
'service_data' => [
'name' => $service['name'],
],
'company_name' => $company_name,
];
@ -386,7 +444,7 @@ class Appointments extends EA_Controller {
*
* @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();
@ -405,7 +463,7 @@ class Appointments extends EA_Controller {
// Check if the provider is available for the requested date.
$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'];
$max_hours_count = count($available_hours);
@ -448,7 +506,7 @@ class Appointments extends EA_Controller {
$captcha_phrase = $this->session->userdata('captcha_phrase');
// Validate the CAPTCHA string.
if ($require_captcha === '1' && $captcha_phrase !== $captcha)
if ($require_captcha === '1' && strtolower($captcha_phrase) !== strtolower($captcha))
{
$this->output
->set_content_type('application/json')
@ -473,6 +531,16 @@ class Appointments extends EA_Controller {
$customer['language'] = config('language');
$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['is_unavailable'] = (int)$appointment['is_unavailable']; // needs to be type casted
$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'));
}
else
{
show_error('Forbidden', 403);
}
}
/**
@ -66,6 +70,11 @@ class Backend_api extends EA_Controller {
{
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';
$end_date = $this->input->post('endDate') . ' 23:59:59';
@ -1527,18 +1536,25 @@ class Backend_api extends EA_Controller {
{
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.');
}
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.
$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)
{
$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);
$calendars = $this->google_sync->get_google_calendars();

View file

@ -45,9 +45,9 @@ class Google extends EA_Controller {
$CI = get_instance();
// 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)
@ -204,7 +204,6 @@ class Google extends EA_Controller {
continue;
}
// Record doesn't exist in the Easy!Appointments, so add the event now.
$appointment = [
'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)
{
if ( ! $this->session->userdata('user_id'))
{
show_error('Forbidden', 403);
}
// Store the provider id for use on the callback function.
$this->session->set_userdata('oauth_provider_id', $provider_id);
@ -268,6 +272,11 @@ class Google extends EA_Controller {
*/
public function oauth_callback()
{
if ( ! $this->session->userdata('user_id'))
{
show_error('Forbidden', 403);
}
$code = $this->input->get('code');
if (empty($code))
@ -298,6 +307,4 @@ class Google extends EA_Controller {
$this->output->set_output('Sync provider id not specified.');
}
}
}

View file

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

View file

@ -51,6 +51,7 @@
* @property User_model $user_model
*
* @property Availability $availability
* @property Captcha_builder $captcha_builder
* @property Google_Sync $google_sync
* @property Ics_file $ics_file
* @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)
{
// 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.');
}
@ -144,7 +144,7 @@ class Settings_model extends EA_Model {
foreach ($settings as $setting)
{
$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']
. ' - ' . $setting['value'] . ')');