Phase 1: Implement CSRF protection, input validation, and rate limiting

Major security improvements:
- Added CSRF token generation, validation, and cleanup functions
- Implemented comprehensive input validators (email, phone, name, date, amount, ID, file uploads)
- Added rate limiting with login attempt tracking and account lockout (5 failures = 15 min lockout)
- Implemented session fixation protection with session_regenerate_id() and 30-min timeout
- Fixed SQL injection in getResultFromTable() with whitelisted columns/tables
- Added audit logging for security events
- Applied CSRF validation to all 7 process_*.php files
- Applied input validation to critical endpoints (login, registration, bookings, application)
- Created database migration for login_attempts, audit_log tables and locked_until column

Modified files:
- functions.php: +500 lines of security functions
- validate_login.php: Added CSRF, rate limiting, session hardening
- register_user.php: Added CSRF, input validation, registration rate limiting
- process_*.php (7 files): Added CSRF token validation
- Created migration: 001_phase1_security_schema.sql

Next steps: Add CSRF tokens to form templates, harden file uploads, create testing checklist
This commit is contained in:
twotalesanimation
2025-12-03 11:28:53 +02:00
parent 062dc46ffd
commit 1ef4d06627
13 changed files with 1729 additions and 133 deletions

680
DB_existing schema.sql Normal file
View File

@@ -0,0 +1,680 @@
-- phpMyAdmin SQL Dump
-- version 5.2.2
-- https://www.phpmyadmin.net/
--
-- Host: db
-- Generation Time: Dec 02, 2025 at 07:32 PM
-- Server version: 8.0.41
-- PHP Version: 8.2.27
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `4wdcsa`
--
-- --------------------------------------------------------
--
-- Table structure for table `bar_items`
--
DROP TABLE IF EXISTS `bar_items`;
CREATE TABLE `bar_items` (
`item_id` int NOT NULL,
`price` decimal(10,2) DEFAULT NULL,
`description` varchar(64) DEFAULT NULL,
`image` varchar(255) DEFAULT NULL,
`qty` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `bar_tabs`
--
DROP TABLE IF EXISTS `bar_tabs`;
CREATE TABLE `bar_tabs` (
`tab_id` int NOT NULL,
`user_id` int DEFAULT NULL,
`image` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `bar_transactions`
--
DROP TABLE IF EXISTS `bar_transactions`;
CREATE TABLE `bar_transactions` (
`transaction_id` int NOT NULL,
`user_id` int DEFAULT NULL,
`item_price` decimal(10,2) DEFAULT NULL,
`item_name` varchar(64) DEFAULT NULL,
`eft_id` varchar(255) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`item_id` int DEFAULT NULL,
`tab_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `blacklist`
--
DROP TABLE IF EXISTS `blacklist`;
CREATE TABLE `blacklist` (
`blacklist_id` int NOT NULL,
`ip` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `blogs`
--
DROP TABLE IF EXISTS `blogs`;
CREATE TABLE `blogs` (
`blog_id` int NOT NULL,
`title` varchar(255) DEFAULT NULL,
`date` date DEFAULT NULL,
`category` varchar(255) DEFAULT NULL,
`description` text,
`image` varchar(255) DEFAULT NULL,
`author` int DEFAULT NULL,
`link` varchar(255) DEFAULT NULL,
`members_only` tinyint(1) NOT NULL DEFAULT '1',
`content` text,
`status` enum('draft','published','deleted') CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT 'draft'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `bookings`
--
DROP TABLE IF EXISTS `bookings`;
CREATE TABLE `bookings` (
`booking_id` int NOT NULL,
`booking_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`user_id` int NOT NULL,
`from_date` date DEFAULT NULL,
`to_date` date DEFAULT NULL,
`num_vehicles` int NOT NULL DEFAULT '1',
`num_adults` int NOT NULL DEFAULT '0',
`num_children` int NOT NULL DEFAULT '0',
`add_firewood` tinyint(1) DEFAULT '0',
`total_amount` decimal(10,2) DEFAULT NULL,
`discount_amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`payment_id` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`trip_id` int DEFAULT NULL,
`radio` tinyint(1) DEFAULT '0',
`course_id` int DEFAULT NULL,
`course_non_members` int DEFAULT '0',
`eft_id` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
`accept_indemnity` tinyint(1) DEFAULT '0',
`num_pensioners` int DEFAULT '0',
`notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `campsites`
--
DROP TABLE IF EXISTS `campsites`;
CREATE TABLE `campsites` (
`id` int NOT NULL,
`name` varchar(255) NOT NULL,
`description` text,
`latitude` float(10,6) NOT NULL,
`longitude` float(10,6) NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`website` varchar(255) DEFAULT NULL,
`telephone` varchar(50) DEFAULT NULL,
`thumbnail` varchar(255) DEFAULT NULL,
`user_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `comments`
--
DROP TABLE IF EXISTS `comments`;
CREATE TABLE `comments` (
`comment_id` int NOT NULL,
`page_id` varchar(255) NOT NULL,
`user_id` varchar(100) NOT NULL,
`comment` text NOT NULL,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `courses`
--
DROP TABLE IF EXISTS `courses`;
CREATE TABLE `courses` (
`course_id` int NOT NULL,
`course_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`date` date NOT NULL,
`capacity` int NOT NULL,
`booked` int NOT NULL,
`cost_members` decimal(10,2) NOT NULL,
`cost_nonmembers` decimal(10,2) NOT NULL,
`instructor` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`instructor_email` varchar(255) COLLATE utf8mb4_general_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `efts`
--
DROP TABLE IF EXISTS `efts`;
CREATE TABLE `efts` (
`eft_id` varchar(255) NOT NULL,
`booking_id` int DEFAULT NULL,
`user_id` int NOT NULL,
`status` varchar(64) NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`amount` decimal(10,2) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`membershipfee_id` int DEFAULT NULL,
`proof_of_payment` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `events`
--
DROP TABLE IF EXISTS `events`;
CREATE TABLE `events` (
`event_id` int NOT NULL,
`date` date DEFAULT NULL,
`time` time DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`image` varchar(255) DEFAULT NULL,
`description` text,
`feature` varchar(255) DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
`type` varchar(255) DEFAULT NULL,
`promo` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `legacy_members`
--
DROP TABLE IF EXISTS `legacy_members`;
CREATE TABLE `legacy_members` (
`legacy_id` varchar(12) NOT NULL,
`last_name` varchar(255) DEFAULT NULL,
`first_name` varchar(255) DEFAULT NULL,
`amount` varchar(12) DEFAULT NULL,
`phone_number` varchar(16) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `membership_application`
--
DROP TABLE IF EXISTS `membership_application`;
CREATE TABLE `membership_application` (
`application_id` int NOT NULL,
`user_id` int NOT NULL,
`first_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`last_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`id_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`dob` date DEFAULT NULL,
`occupation` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`tel_cell` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`spouse_first_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`spouse_last_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`spouse_id_number` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
`spouse_dob` date DEFAULT NULL,
`spouse_occupation` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`spouse_tel_cell` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
`spouse_email` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`child_name1` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`child_dob1` date DEFAULT NULL,
`child_name2` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`child_dob2` date DEFAULT NULL,
`child_name3` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`child_dob3` date DEFAULT NULL,
`physical_address` text COLLATE utf8mb4_general_ci,
`postal_address` text COLLATE utf8mb4_general_ci,
`interests_hobbies` text COLLATE utf8mb4_general_ci,
`vehicle_make` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`vehicle_model` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`vehicle_year` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL,
`vehicle_registration` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
`secondary_vehicle_make` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`secondary_vehicle_model` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`secondary_vehicle_year` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL,
`secondary_vehicle_registration` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`accept_indemnity` tinyint(1) NOT NULL DEFAULT '0',
`sig` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`code` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `membership_fees`
--
DROP TABLE IF EXISTS `membership_fees`;
CREATE TABLE `membership_fees` (
`fee_id` int NOT NULL,
`user_id` int NOT NULL,
`payment_amount` decimal(10,2) NOT NULL,
`payment_date` date DEFAULT NULL,
`payment_status` varchar(255) COLLATE utf8mb4_general_ci DEFAULT 'PENDING',
`membership_start_date` date NOT NULL,
`membership_end_date` date NOT NULL,
`due_date` date DEFAULT NULL,
`renewal_reminder_sent` tinyint(1) DEFAULT '0',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`payment_id` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `password_resets`
--
DROP TABLE IF EXISTS `password_resets`;
CREATE TABLE `password_resets` (
`id` int NOT NULL,
`user_id` int NOT NULL,
`token` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`expires_at` datetime NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `payments`
--
DROP TABLE IF EXISTS `payments`;
CREATE TABLE `payments` (
`payment_id` varchar(255) NOT NULL,
`user_id` int NOT NULL,
`amount` decimal(10,2) NOT NULL,
`status` varchar(255) NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`description` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `prices`
--
DROP TABLE IF EXISTS `prices`;
CREATE TABLE `prices` (
`price_id` int NOT NULL,
`description` varchar(255) DEFAULT NULL,
`type` varchar(255) DEFAULT NULL,
`amount` decimal(10,2) DEFAULT NULL,
`amount_nonmembers` decimal(10,2) DEFAULT NULL,
`detail` text
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `trips`
--
DROP TABLE IF EXISTS `trips`;
CREATE TABLE `trips` (
`trip_id` int NOT NULL,
`trip_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`start_date` date NOT NULL,
`end_date` date NOT NULL,
`short_description` text COLLATE utf8mb4_general_ci NOT NULL,
`long_description` text COLLATE utf8mb4_general_ci NOT NULL,
`vehicle_capacity` int NOT NULL,
`cost_members` decimal(10,2) NOT NULL,
`cost_nonmembers` decimal(10,2) NOT NULL,
`location` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`places_booked` int DEFAULT NULL,
`booking_fee` decimal(10,2) NOT NULL,
`trip_code` varchar(12) COLLATE utf8mb4_general_ci DEFAULT NULL,
`published` tinyint(1) NOT NULL DEFAULT '0',
`cost_pensioner_member` decimal(10,2) NOT NULL,
`cost_pensioner` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`user_id` int NOT NULL,
`first_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`last_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`email` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`member` tinyint(1) NOT NULL DEFAULT '0',
`date_joined` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`is_verified` tinyint(1) NOT NULL DEFAULT '0',
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`phone_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`profile_pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'assets/images/pp/default.png',
`role` enum('user','admin','superadmin','') COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'user',
`type` enum('google','credentials') COLLATE utf8mb4_general_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `visitor_logs`
--
DROP TABLE IF EXISTS `visitor_logs`;
CREATE TABLE `visitor_logs` (
`id` int NOT NULL,
`ip_address` varchar(45) NOT NULL,
`page_url` text NOT NULL,
`referrer_url` text,
`visit_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`user_id` int DEFAULT NULL,
`country` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `bar_items`
--
ALTER TABLE `bar_items`
ADD PRIMARY KEY (`item_id`);
--
-- Indexes for table `bar_tabs`
--
ALTER TABLE `bar_tabs`
ADD PRIMARY KEY (`tab_id`);
--
-- Indexes for table `bar_transactions`
--
ALTER TABLE `bar_transactions`
ADD PRIMARY KEY (`transaction_id`);
--
-- Indexes for table `blacklist`
--
ALTER TABLE `blacklist`
ADD PRIMARY KEY (`blacklist_id`);
--
-- Indexes for table `blogs`
--
ALTER TABLE `blogs`
ADD PRIMARY KEY (`blog_id`);
--
-- Indexes for table `bookings`
--
ALTER TABLE `bookings`
ADD PRIMARY KEY (`booking_id`),
ADD KEY `user_id` (`user_id`);
--
-- Indexes for table `campsites`
--
ALTER TABLE `campsites`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `comments`
--
ALTER TABLE `comments`
ADD PRIMARY KEY (`comment_id`);
--
-- Indexes for table `courses`
--
ALTER TABLE `courses`
ADD PRIMARY KEY (`course_id`);
--
-- Indexes for table `efts`
--
ALTER TABLE `efts`
ADD PRIMARY KEY (`eft_id`);
--
-- Indexes for table `events`
--
ALTER TABLE `events`
ADD PRIMARY KEY (`event_id`);
--
-- Indexes for table `legacy_members`
--
ALTER TABLE `legacy_members`
ADD PRIMARY KEY (`legacy_id`);
--
-- Indexes for table `membership_application`
--
ALTER TABLE `membership_application`
ADD PRIMARY KEY (`application_id`);
--
-- Indexes for table `membership_fees`
--
ALTER TABLE `membership_fees`
ADD PRIMARY KEY (`fee_id`);
--
-- Indexes for table `password_resets`
--
ALTER TABLE `password_resets`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `token` (`token`),
ADD KEY `user_id` (`user_id`);
--
-- Indexes for table `payments`
--
ALTER TABLE `payments`
ADD PRIMARY KEY (`payment_id`);
--
-- Indexes for table `prices`
--
ALTER TABLE `prices`
ADD PRIMARY KEY (`price_id`);
--
-- Indexes for table `trips`
--
ALTER TABLE `trips`
ADD PRIMARY KEY (`trip_id`);
--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`user_id`);
--
-- Indexes for table `visitor_logs`
--
ALTER TABLE `visitor_logs`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `bar_items`
--
ALTER TABLE `bar_items`
MODIFY `item_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `bar_tabs`
--
ALTER TABLE `bar_tabs`
MODIFY `tab_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `bar_transactions`
--
ALTER TABLE `bar_transactions`
MODIFY `transaction_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `blacklist`
--
ALTER TABLE `blacklist`
MODIFY `blacklist_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `blogs`
--
ALTER TABLE `blogs`
MODIFY `blog_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `bookings`
--
ALTER TABLE `bookings`
MODIFY `booking_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `campsites`
--
ALTER TABLE `campsites`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `comments`
--
ALTER TABLE `comments`
MODIFY `comment_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `courses`
--
ALTER TABLE `courses`
MODIFY `course_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `events`
--
ALTER TABLE `events`
MODIFY `event_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `membership_application`
--
ALTER TABLE `membership_application`
MODIFY `application_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `membership_fees`
--
ALTER TABLE `membership_fees`
MODIFY `fee_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `password_resets`
--
ALTER TABLE `password_resets`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `prices`
--
ALTER TABLE `prices`
MODIFY `price_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `trips`
--
ALTER TABLE `trips`
MODIFY `trip_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `user_id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `visitor_logs`
--
ALTER TABLE `visitor_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `bookings`
--
ALTER TABLE `bookings`
ADD CONSTRAINT `bookings_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE;
--
-- Constraints for table `password_resets`
--
ALTER TABLE `password_resets`
ADD CONSTRAINT `password_resets_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@@ -92,14 +92,12 @@ function calculateDaysAndNights($startDate, $endDate)
function sendVerificationEmail($email, $name, $token) function sendVerificationEmail($email, $name, $token)
{ {
global $mailjet;
$message = [ $message = [
'Messages' => [ 'Messages' => [
[ [
'From' => [ 'From' => [
'Email' => "info@4wdcsa.co.za", 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => "4WDCSA" 'Name' => $_ENV['MAILJET_FROM_NAME']
], ],
'To' => [ 'To' => [
[ [
@@ -119,36 +117,33 @@ function sendVerificationEmail($email, $name, $token)
]; ];
$client = new Client([ $client = new Client([
// Base URI is used with relative requests
'base_uri' => 'https://api.mailjet.com/v3.1/', 'base_uri' => 'https://api.mailjet.com/v3.1/',
]); ]);
$response = $client->request('POST', 'send', [ $response = $client->request('POST', 'send', [
'json' => $message, 'json' => $message,
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
]); ]);
if ($response->getStatusCode() == 200) { if ($response->getStatusCode() == 200) {
$body = $response->getBody(); $body = $response->getBody();
$response = json_decode($body); $response = json_decode($body);
if ($response->Messages[0]->Status == 'success') { if ($response->Messages[0]->Status == 'success') {
return True; return true;
} else { } else {
return False; return false;
} }
} }
} }
function sendInvoice($email, $name, $eft_id, $amount, $description) function sendInvoice($email, $name, $eft_id, $amount, $description)
{ {
global $mailjet;
$message = [ $message = [
'Messages' => [ 'Messages' => [
[ [
'From' => [ 'From' => [
'Email' => "info@4wdcsa.co.za", 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => "4WDCSA" 'Name' => $_ENV['MAILJET_FROM_NAME']
], ],
'To' => [ 'To' => [
[ [
@@ -169,22 +164,21 @@ function sendInvoice($email, $name, $eft_id, $amount, $description)
]; ];
$client = new Client([ $client = new Client([
// Base URI is used with relative requests
'base_uri' => 'https://api.mailjet.com/v3.1/', 'base_uri' => 'https://api.mailjet.com/v3.1/',
]); ]);
$response = $client->request('POST', 'send', [ $response = $client->request('POST', 'send', [
'json' => $message, 'json' => $message,
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
]); ]);
if ($response->getStatusCode() == 200) { if ($response->getStatusCode() == 200) {
$body = $response->getBody(); $body = $response->getBody();
$response = json_decode($body); $response = json_decode($body);
if ($response->Messages[0]->Status == 'success') { if ($response->Messages[0]->Status == 'success') {
return True; return true;
} else { } else {
return False; return false;
} }
} }
} }
@@ -209,14 +203,12 @@ function getEFTDetails($eft_id) {
function sendPOP($fullname, $eft_id, $amount, $description) function sendPOP($fullname, $eft_id, $amount, $description)
{ {
global $mailjet;
$message = [ $message = [
'Messages' => [ 'Messages' => [
[ [
'From' => [ 'From' => [
'Email' => "info@4wdcsa.co.za", 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => "4WDCSA Web Admin" 'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin'
], ],
'To' => [ 'To' => [
[ [
@@ -224,7 +216,7 @@ function sendPOP($fullname, $eft_id, $amount, $description)
'Name' => 'Chris Pinto' 'Name' => 'Chris Pinto'
], ],
[ [
'Email' => 'info@4wdcsa.co.za', 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => 'Jacqui Boshoff' 'Name' => 'Jacqui Boshoff'
], ],
[ [
@@ -246,36 +238,33 @@ function sendPOP($fullname, $eft_id, $amount, $description)
]; ];
$client = new Client([ $client = new Client([
// Base URI is used with relative requests
'base_uri' => 'https://api.mailjet.com/v3.1/', 'base_uri' => 'https://api.mailjet.com/v3.1/',
]); ]);
$response = $client->request('POST', 'send', [ $response = $client->request('POST', 'send', [
'json' => $message, 'json' => $message,
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
]); ]);
if ($response->getStatusCode() == 200) { if ($response->getStatusCode() == 200) {
$body = $response->getBody(); $body = $response->getBody();
$response = json_decode($body); $response = json_decode($body);
if ($response->Messages[0]->Status == 'success') { if ($response->Messages[0]->Status == 'success') {
return True; return true;
} else { } else {
return False; return false;
} }
} }
} }
function sendEmail($email, $subject, $message) function sendEmail($email, $subject, $message)
{ {
global $mailjet; $messageData = [
$message = [
'Messages' => [ 'Messages' => [
[ [
'From' => [ 'From' => [
'Email' => "info@4wdcsa.co.za", 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => "4WDCSA" 'Name' => $_ENV['MAILJET_FROM_NAME']
], ],
'To' => [ 'To' => [
[ [
@@ -289,36 +278,33 @@ function sendEmail($email, $subject, $message)
]; ];
$client = new Client([ $client = new Client([
// Base URI is used with relative requests
'base_uri' => 'https://api.mailjet.com/v3.1/', 'base_uri' => 'https://api.mailjet.com/v3.1/',
]); ]);
$response = $client->request('POST', 'send', [ $response = $client->request('POST', 'send', [
'json' => $message, 'json' => $messageData,
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
]); ]);
if ($response->getStatusCode() == 200) { if ($response->getStatusCode() == 200) {
$body = $response->getBody(); $body = $response->getBody();
$response = json_decode($body); $response = json_decode($body);
if ($response->Messages[0]->Status == 'success') { if ($response->Messages[0]->Status == 'success') {
return True; return true;
} else { } else {
return False; return false;
} }
} }
} }
function sendAdminNotification($subject, $message) function sendAdminNotification($subject, $message)
{ {
global $mailjet;
$mail = [ $mail = [
'Messages' => [ 'Messages' => [
[ [
'From' => [ 'From' => [
'Email' => "info@4wdcsa.co.za", 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => "4WDCSA" 'Name' => $_ENV['MAILJET_FROM_NAME']
], ],
'To' => [ 'To' => [
[ [
@@ -337,36 +323,33 @@ function sendAdminNotification($subject, $message)
]; ];
$client = new Client([ $client = new Client([
// Base URI is used with relative requests
'base_uri' => 'https://api.mailjet.com/v3.1/', 'base_uri' => 'https://api.mailjet.com/v3.1/',
]); ]);
$response = $client->request('POST', 'send', [ $response = $client->request('POST', 'send', [
'json' => $mail, 'json' => $mail,
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
]); ]);
if ($response->getStatusCode() == 200) { if ($response->getStatusCode() == 200) {
$body = $response->getBody(); $body = $response->getBody();
$response = json_decode($body); $response = json_decode($body);
if ($response->Messages[0]->Status == 'success') { if ($response->Messages[0]->Status == 'success') {
return True; return true;
} else { } else {
return False; return false;
} }
} }
} }
function sendPaymentConfirmation($email, $name, $description) function sendPaymentConfirmation($email, $name, $description)
{ {
global $mailjet;
$message = [ $message = [
'Messages' => [ 'Messages' => [
[ [
'From' => [ 'From' => [
'Email' => "info@4wdcsa.co.za", 'Email' => $_ENV['MAILJET_FROM_EMAIL'],
'Name' => "4WDCSA" 'Name' => $_ENV['MAILJET_FROM_NAME']
], ],
'To' => [ 'To' => [
[ [
@@ -386,22 +369,21 @@ function sendPaymentConfirmation($email, $name, $description)
]; ];
$client = new Client([ $client = new Client([
// Base URI is used with relative requests
'base_uri' => 'https://api.mailjet.com/v3.1/', 'base_uri' => 'https://api.mailjet.com/v3.1/',
]); ]);
$response = $client->request('POST', 'send', [ $response = $client->request('POST', 'send', [
'json' => $message, 'json' => $message,
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280'] 'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
]); ]);
if ($response->getStatusCode() == 200) { if ($response->getStatusCode() == 200) {
$body = $response->getBody(); $body = $response->getBody();
$response = json_decode($body); $response = json_decode($body);
if ($response->Messages[0]->Status == 'success') { if ($response->Messages[0]->Status == 'success') {
return True; return true;
} else { } else {
return False; return false;
} }
} }
} }
@@ -591,7 +573,7 @@ function processPayment($payment_id, $amount, $description)
{ {
$conn = openDatabaseConnection(); $conn = openDatabaseConnection();
$status = "AWAITING PAYMENT"; $status = "AWAITING PAYMENT";
$domain = 'www.thepinto.co.za/4wdcsa'; $domain = $_ENV['PAYFAST_DOMAIN'];
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
// Insert the order into the orders table // Insert the order into the orders table
$stmt = $conn->prepare(" $stmt = $conn->prepare("
@@ -625,7 +607,7 @@ function processPayment($payment_id, $amount, $description)
* @param null $passPhrase * @param null $passPhrase
* @return string * @return string
*/ */
function generateSignature($data, $passPhrase = 'SheSells7Shells') function generateSignature($data, $passPhrase = null)
{ {
// Create parameter string // Create parameter string
$pfOutput = ''; $pfOutput = '';
@@ -644,8 +626,8 @@ function processPayment($payment_id, $amount, $description)
// Construct variables // Construct variables
$data = array( $data = array(
// Merchant details // Merchant details
'merchant_id' => '10021495', 'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'],
'merchant_key' => 'yzpdydo934j92', 'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'],
'return_url' => 'https://' . $domain . '/bookings.php', 'return_url' => 'https://' . $domain . '/bookings.php',
'cancel_url' => 'https://' . $domain . '/cancel_booking.php?booking_id=' . $encryptedId, 'cancel_url' => 'https://' . $domain . '/cancel_booking.php?booking_id=' . $encryptedId,
'notify_url' => 'https://' . $domain . '/confirm.php', 'notify_url' => 'https://' . $domain . '/confirm.php',
@@ -659,11 +641,11 @@ function processPayment($payment_id, $amount, $description)
'item_name' => '4WDCSA: ' . $description // Describe the item(s) or use a generic description 'item_name' => '4WDCSA: ' . $description // Describe the item(s) or use a generic description
); );
$signature = generateSignature($data); // Assuming you have this function defined $signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']);
$data['signature'] = $signature; $data['signature'] = $signature;
// Determine the PayFast URL based on the mode // Determine the PayFast URL based on the mode
$testingMode = true; $testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true';
$pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; $pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
// Generate the HTML form with hidden inputs and an auto-submit script // Generate the HTML form with hidden inputs and an auto-submit script
@@ -690,7 +672,7 @@ function processMembershipPayment($payment_id, $amount, $description)
{ {
$conn = openDatabaseConnection(); $conn = openDatabaseConnection();
$status = "AWAITING PAYMENT"; $status = "AWAITING PAYMENT";
$domain = 'www.thepinto.co.za/4wdcsa'; $domain = $_ENV['PAYFAST_DOMAIN'];
$user_id = $_SESSION['user_id']; $user_id = $_SESSION['user_id'];
// Insert the order into the orders table // Insert the order into the orders table
$stmt = $conn->prepare(" $stmt = $conn->prepare("
@@ -724,7 +706,7 @@ function processMembershipPayment($payment_id, $amount, $description)
* @param null $passPhrase * @param null $passPhrase
* @return string * @return string
*/ */
function generateSignature($data, $passPhrase = 'SheSells7Shells') function generateSignature($data, $passPhrase = null)
{ {
// Create parameter string // Create parameter string
$pfOutput = ''; $pfOutput = '';
@@ -743,8 +725,8 @@ function processMembershipPayment($payment_id, $amount, $description)
// Construct variables // Construct variables
$data = array( $data = array(
// Merchant details // Merchant details
'merchant_id' => '10021495', 'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'],
'merchant_key' => 'yzpdydo934j92', 'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'],
'return_url' => 'https://' . $domain . '/account_settings.php', 'return_url' => 'https://' . $domain . '/account_settings.php',
'cancel_url' => 'https://' . $domain . '/cancel_application.php?id=' . $encryptedId, 'cancel_url' => 'https://' . $domain . '/cancel_application.php?id=' . $encryptedId,
'notify_url' => 'https://' . $domain . '/confirm2.php', 'notify_url' => 'https://' . $domain . '/confirm2.php',
@@ -758,11 +740,11 @@ function processMembershipPayment($payment_id, $amount, $description)
'item_name' => $description // Describe the item(s) or use a generic description 'item_name' => $description // Describe the item(s) or use a generic description
); );
$signature = generateSignature($data); // Assuming you have this function defined $signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']);
$data['signature'] = $signature; $data['signature'] = $signature;
// Determine the PayFast URL based on the mode // Determine the PayFast URL based on the mode
$testingMode = true; $testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true';
$pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; $pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
// Generate the HTML form with hidden inputs and an auto-submit script // Generate the HTML form with hidden inputs and an auto-submit script
@@ -1904,16 +1886,84 @@ function processLegacyMembership($user_id) {
} }
} }
/**
* SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection.
* ONLY call this function with whitelisted table and column names.
* NEVER accept table/column names directly from user input.
*
* Retrieves a single value from a database table.
* @param string $table Table name (MUST be whitelisted - see allowed_tables array)
* @param string $column Column name to retrieve (MUST be whitelisted - see allowed_columns array)
* @param string $match Column name for WHERE clause (MUST be whitelisted)
* @param mixed $identifier Value to match in WHERE clause (parameterized - safe)
* @return mixed The result value or null if not found
*/
function getResultFromTable($table, $column, $match, $identifier) { function getResultFromTable($table, $column, $match, $identifier) {
// WHITELIST: Define allowed tables to prevent table name injection
$allowed_tables = [
'users',
'membership_application',
'membership_fees',
'bookings',
'payments',
'efts',
'trips',
'courses',
'blogs',
'events',
'campsites',
'bar_transactions',
'login_attempts',
'legacy_members'
];
// WHITELIST: Define allowed columns per table (simplified - add more as needed)
$allowed_columns = [
'legacy_members' => ['amount', 'legacy_id', 'email', 'name'],
'users' => ['user_id', 'email', 'first_name', 'last_name', 'phone_number', 'password', 'profile_pic', 'is_verified', 'type', 'locked_until'],
'membership_fees' => ['payment_id', 'user_id', 'amount', 'payment_status', 'payment_date'],
'bookings' => ['booking_id', 'user_id', 'total_amount', 'status', 'booking_type'],
'payments' => ['payment_id', 'user_id', 'amount', 'status'],
'trips' => ['trip_id', 'trip_name', 'description'],
'courses' => ['course_id', 'course_name', 'description'],
'blogs' => ['blog_id', 'title', 'content'],
'events' => ['event_id', 'event_name', 'description'],
'campsites' => ['campsite_id', 'name', 'location'],
'efts' => ['eft_id', 'amount', 'status', 'booking_id'],
'bar_transactions' => ['transaction_id', 'amount', 'date'],
'login_attempts' => ['attempt_id', 'email', 'ip_address', 'success']
];
$conn = openDatabaseConnection(); // Validate table name is in whitelist
$sql = "SELECT `$column` FROM `$table` WHERE `$match` = ?"; if (!in_array($table, $allowed_tables, true)) {
$stmt = $conn->prepare($sql); error_log("Security Warning: getResultFromTable() called with non-whitelisted table: $table");
if (!$stmt) { return null;
}
// Validate column name is in whitelist for this table
if (!isset($allowed_columns[$table]) || !in_array($column, $allowed_columns[$table], true)) {
error_log("Security Warning: getResultFromTable() called with non-whitelisted column: $column for table: $table");
return null;
}
// Validate match column is in whitelist for this table
if (!isset($allowed_columns[$table]) || !in_array($match, $allowed_columns[$table], true)) {
error_log("Security Warning: getResultFromTable() called with non-whitelisted match column: $match for table: $table");
return null; return null;
} }
$stmt->bind_param('i', $identifier); $conn = openDatabaseConnection();
// Use backticks for table and column identifiers (safe after whitelist validation)
$sql = "SELECT `" . $column . "` FROM `" . $table . "` WHERE `" . $match . "` = ?";
$stmt = $conn->prepare($sql);
if (!$stmt) {
error_log("Database prepare error: " . $conn->error);
return null;
}
// Determine parameter type based on identifier
$paramType = is_int($identifier) ? 'i' : 's';
$stmt->bind_param($paramType, $identifier);
$stmt->execute(); $stmt->execute();
$stmt->bind_result($result); $stmt->bind_result($result);
$stmt->fetch(); $stmt->fetch();
@@ -1977,3 +2027,522 @@ function hasPhoneNumber($user_id) {
// Return true only if a phone number exists and is not empty // Return true only if a phone number exists and is not empty
return !empty($phone_number); return !empty($phone_number);
} }
// ==================== CSRF PROTECTION FUNCTIONS ====================
/**
* Generates a CSRF token and stores it in the session with expiration
* @param int $duration Token expiration time in seconds (default 3600 = 1 hour)
* @return string The generated CSRF token
*/
function generateCSRFToken($duration = 3600) {
// Initialize CSRF token storage in session if needed
if (!isset($_SESSION['csrf_tokens'])) {
$_SESSION['csrf_tokens'] = [];
}
// Clean up expired tokens
cleanupExpiredTokens();
// Generate a random token
$token = bin2hex(random_bytes(32));
// Store token with expiration timestamp
$_SESSION['csrf_tokens'][$token] = time() + $duration;
return $token;
}
/**
* Validates a CSRF token from user input
* @param string $token The token to validate (typically from $_POST['csrf_token'])
* @return bool True if token is valid, false otherwise
*/
function validateCSRFToken($token) {
// Check if token exists in session
if (!isset($_SESSION['csrf_tokens']) || !isset($_SESSION['csrf_tokens'][$token])) {
return false;
}
// Check if token has expired
if ($_SESSION['csrf_tokens'][$token] < time()) {
unset($_SESSION['csrf_tokens'][$token]);
return false;
}
// Token is valid - remove it from session (single-use)
unset($_SESSION['csrf_tokens'][$token]);
return true;
}
/**
* Removes expired tokens from the session
*/
function cleanupExpiredTokens() {
if (!isset($_SESSION['csrf_tokens'])) {
return;
}
$currentTime = time();
foreach ($_SESSION['csrf_tokens'] as $token => $expiration) {
if ($expiration < $currentTime) {
unset($_SESSION['csrf_tokens'][$token]);
}
}
}
// ==================== INPUT VALIDATION FUNCTIONS ====================
/**
* Validates and sanitizes email input
* @param string $email The email to validate
* @param int $maxLength Maximum allowed length (default 254 per RFC 5321)
* @return string|false Sanitized email or false if invalid
*/
function validateEmail($email, $maxLength = 254) {
// Check length
if (strlen($email) > $maxLength) {
return false;
}
// Filter and validate
$filtered = filter_var($email, FILTER_VALIDATE_EMAIL);
// Sanitize if valid
return $filtered ? filter_var($filtered, FILTER_SANITIZE_EMAIL) : false;
}
/**
* Validates phone number format
* @param string $phone The phone number to validate
* @return string|false Sanitized phone number or false if invalid
*/
function validatePhoneNumber($phone) {
// Remove common formatting characters
$cleaned = preg_replace('/[^\d+\-\s().]/', '', $phone);
// Check length (between 7 and 20 digits)
$digitCount = strlen(preg_replace('/[^\d]/', '', $cleaned));
if ($digitCount < 7 || $digitCount > 20) {
return false;
}
return $cleaned;
}
/**
* Validates and sanitizes a name (first name, last name)
* @param string $name The name to validate
* @param int $minLength Minimum allowed length (default 2)
* @param int $maxLength Maximum allowed length (default 100)
* @return string|false Sanitized name or false if invalid
*/
function validateName($name, $minLength = 2, $maxLength = 100) {
// Trim whitespace
$name = trim($name);
// Check length
if (strlen($name) < $minLength || strlen($name) > $maxLength) {
return false;
}
// Only allow letters, spaces, hyphens, and apostrophes
if (!preg_match('/^[a-zA-Z\s\'-]+$/', $name)) {
return false;
}
return htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
}
/**
* Validates a date string in YYYY-MM-DD format
* @param string $date The date string to validate
* @param string $format Expected date format (default 'Y-m-d')
* @return string|false Valid date string or false if invalid
*/
function validateDate($date, $format = 'Y-m-d') {
$d = DateTime::createFromFormat($format, $date);
// Check if date is valid and in correct format
if (!$d || $d->format($format) !== $date) {
return false;
}
return $date;
}
/**
* Validates a numeric amount (for currency)
* @param mixed $amount The amount to validate
* @param float $min Minimum allowed amount (default 0)
* @param float $max Maximum allowed amount (default 999999.99)
* @return float|false Valid amount or false if invalid
*/
function validateAmount($amount, $min = 0, $max = 999999.99) {
// Try to convert to float
$value = filter_var($amount, FILTER_VALIDATE_FLOAT, [
'options' => [
'min_range' => $min,
'max_range' => $max,
'decimal' => '.'
]
]);
// Must have at most 2 decimal places
if ($value !== false) {
$parts = explode('.', (string)$amount);
if (isset($parts[1]) && strlen($parts[1]) > 2) {
return false;
}
}
return $value;
}
/**
* Validates an integer within a range
* @param mixed $int The integer to validate
* @param int $min Minimum allowed value (default 0)
* @param int $max Maximum allowed value (default 2147483647)
* @return int|false Valid integer or false if invalid
*/
function validateInteger($int, $min = 0, $max = 2147483647) {
$value = filter_var($int, FILTER_VALIDATE_INT, [
'options' => [
'min_range' => $min,
'max_range' => $max
]
]);
return $value !== false ? $value : false;
}
/**
* Validates South African ID number (13 digits)
* @param string $idNumber The ID number to validate
* @return string|false Valid ID number or false if invalid
*/
function validateSAIDNumber($idNumber) {
// Remove any whitespace
$idNumber = preg_replace('/\s/', '', $idNumber);
// Must be exactly 13 digits
if (!preg_match('/^\d{13}$/', $idNumber)) {
return false;
}
// Optional: Validate checksum (Luhn algorithm)
$sum = 0;
for ($i = 0; $i < 13; $i++) {
$digit = (int)$idNumber[$i];
// Double every even-positioned digit (0-indexed)
if ($i % 2 == 0) {
$digit *= 2;
if ($digit > 9) {
$digit -= 9;
}
}
$sum += $digit;
}
// Last digit should make sum divisible by 10
if ($sum % 10 != 0) {
return false;
}
return $idNumber;
}
/**
* Sanitizes text input, removing potentially dangerous characters
* @param string $text The text to sanitize
* @param int $maxLength Maximum allowed length
* @return string Sanitized text
*/
function sanitizeTextInput($text, $maxLength = 1000) {
// Trim whitespace
$text = trim($text);
// Limit length
$text = substr($text, 0, $maxLength);
// Encode HTML special characters
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
/**
* Validates file uploads for security
* @param array $file The $_FILES element to validate
* @param array $allowedTypes Array of allowed MIME types (e.g., ['image/jpeg', 'image/png', 'application/pdf'])
* @param int $maxSize Maximum file size in bytes
* @return string|false Sanitized filename or false if invalid
*/
function validateFileUpload($file, $allowedTypes = [], $maxSize = 5242880) { // 5MB default
// Check for upload errors
if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
return false;
}
// Check file size
if (!isset($file['size']) || $file['size'] > $maxSize) {
return false;
}
// Check MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes, true)) {
return false;
}
// Generate random filename with original extension
$pathinfo = pathinfo($file['name']);
$extension = strtolower($pathinfo['extension']);
// Whitelist allowed extensions
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'gif', 'webp'];
if (!in_array($extension, $allowedExtensions)) {
return false;
}
// Generate random filename
$randomName = bin2hex(random_bytes(16)) . '.' . $extension;
return $randomName;
}
// ==================== RATE LIMITING & ACCOUNT LOCKOUT FUNCTIONS ====================
/**
* Records a login attempt in the login_attempts table
* @param string $email The email address attempting to login
* @param bool $success Whether the login was successful
* @return void
*/
function recordLoginAttempt($email, $success = false) {
// Get client IP address
$ip = getClientIPAddress();
$conn = openDatabaseConnection();
if (!$conn) {
return;
}
$email = strtolower(trim($email));
$sql = "INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, ?)";
$stmt = $conn->prepare($sql);
if ($stmt) {
$success_int = $success ? 1 : 0;
$stmt->bind_param('ssi', $email, $ip, $success_int);
$stmt->execute();
$stmt->close();
}
}
/**
* Gets the client's IP address safely
* @return string The client's IP address
*/
function getClientIPAddress() {
// Check for IP from share internet
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
// Check for IP passed from proxy
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
}
// Check for remote IP
else {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
// Validate IP format
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$ip = '0.0.0.0';
}
return $ip;
}
/**
* Checks if an account is locked and returns lockout status
* @param string $email The email address to check
* @return array ['is_locked' => bool, 'locked_until' => datetime string or null, 'minutes_remaining' => int]
*/
function checkAccountLockout($email) {
$conn = openDatabaseConnection();
if (!$conn) {
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
}
$email = strtolower(trim($email));
$now = date('Y-m-d H:i:s');
$sql = "SELECT locked_until FROM users WHERE email = ? LIMIT 1";
$stmt = $conn->prepare($sql);
if (!$stmt) {
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
}
$stmt->bind_param('s', $email);
$stmt->execute();
$stmt->bind_result($locked_until);
$stmt->fetch();
$stmt->close();
if ($locked_until === null) {
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
}
if ($locked_until > $now) {
// Account is still locked
$lockTime = strtotime($locked_until);
$nowTime = strtotime($now);
$secondsRemaining = max(0, $lockTime - $nowTime);
$minutesRemaining = ceil($secondsRemaining / 60);
return [
'is_locked' => true,
'locked_until' => $locked_until,
'minutes_remaining' => $minutesRemaining
];
}
// Lockout has expired, clear it
$sql = "UPDATE users SET locked_until = NULL WHERE email = ?";
$stmt = $conn->prepare($sql);
if ($stmt) {
$stmt->bind_param('s', $email);
$stmt->execute();
$stmt->close();
}
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
}
/**
* Counts recent failed login attempts for an email + IP combination
* @param string $email The email address
* @param int $minutesBack How many minutes back to check (default 15)
* @return int Number of failed attempts
*/
function countRecentFailedAttempts($email, $minutesBack = 15) {
$conn = openDatabaseConnection();
if (!$conn) {
return 0;
}
$email = strtolower(trim($email));
$ip = getClientIPAddress();
$cutoffTime = date('Y-m-d H:i:s', time() - ($minutesBack * 60));
$sql = "SELECT COUNT(*) as count FROM login_attempts
WHERE email = ? AND ip_address = ? AND success = 0
AND attempted_at > ?";
$stmt = $conn->prepare($sql);
if (!$stmt) {
return 0;
}
$stmt->bind_param('sss', $email, $ip, $cutoffTime);
$stmt->execute();
$stmt->bind_result($count);
$stmt->fetch();
$stmt->close();
return (int)$count;
}
/**
* Locks an account for a specified duration
* @param string $email The email address to lock
* @param int $minutes Duration of lockout in minutes (default 15)
* @return bool True if successful, false otherwise
*/
function lockAccount($email, $minutes = 15) {
$conn = openDatabaseConnection();
if (!$conn) {
return false;
}
$email = strtolower(trim($email));
$lockUntil = date('Y-m-d H:i:s', time() + ($minutes * 60));
$sql = "UPDATE users SET locked_until = ? WHERE email = ?";
$stmt = $conn->prepare($sql);
if (!$stmt) {
return false;
}
$stmt->bind_param('ss', $lockUntil, $email);
$result = $stmt->execute();
$stmt->close();
return $result;
}
/**
* Unlocks an account (admin function)
* @param string $email The email address to unlock
* @return bool True if successful, false otherwise
*/
function unlockAccount($email) {
$conn = openDatabaseConnection();
if (!$conn) {
return false;
}
$email = strtolower(trim($email));
$sql = "UPDATE users SET locked_until = NULL WHERE email = ?";
$stmt = $conn->prepare($sql);
if (!$stmt) {
return false;
}
$stmt->bind_param('s', $email);
$result = $stmt->execute();
$stmt->close();
return $result;
}
/**
* Logs an action to the audit log table
* @param int $user_id User ID (null if not authenticated)
* @param string $action Action name (e.g., 'LOGIN', 'FAILED_LOGIN', 'ACCOUNT_LOCKED')
* @param string $resource_type Resource type being affected
* @param int $resource_id Resource ID being affected
* @param array $details Additional details about the action
* @return bool True if successful
*/
function auditLog($user_id, $action, $resource_type = null, $resource_id = null, $details = null) {
$conn = openDatabaseConnection();
if (!$conn) {
return false;
}
$ip = getClientIPAddress();
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$detailsJson = $details ? json_encode($details) : null;
$sql = "INSERT INTO audit_log (user_id, action, resource_type, resource_id, ip_address, user_agent, details)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
if (!$stmt) {
return false;
}
$stmt->bind_param('issdsss', $user_id, $action, $resource_type, $resource_id, $ip, $userAgent, $detailsJson);
$result = $stmt->execute();
$stmt->close();
return $result;
}

View File

@@ -0,0 +1,47 @@
-- ============================================================================
-- MIGRATION: Phase 1 Security & Stability Database Schema Updates
-- Date: 2025-12-03
-- Description: Add tables and columns required for Phase 1 security features
-- (login rate limiting, account lockout, audit logging)
-- ============================================================================
-- Track failed login attempts for rate limiting and account lockout
CREATE TABLE IF NOT EXISTS login_attempts (
attempt_id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
success BOOLEAN DEFAULT FALSE,
INDEX idx_email_ip (email, ip_address),
INDEX idx_attempted_at (attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Add account lockout column to users table
-- Stores the timestamp until which the account is locked
-- NULL = account not locked, Future datetime = account locked until this time
ALTER TABLE users ADD COLUMN locked_until DATETIME NULL DEFAULT NULL AFTER is_verified;
CREATE INDEX idx_locked_until ON users (locked_until);
-- Security audit log for sensitive operations
DROP TABLE IF EXISTS `audit_log`;
CREATE TABLE IF NOT EXISTS audit_log (
log_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NULL,
action VARCHAR(100) NOT NULL COMMENT 'e.g., LOGIN, FAILED_LOGIN, ACCOUNT_LOCKED, FILE_UPLOAD',
resource_type VARCHAR(50) COMMENT 'e.g., users, bookings, payments',
resource_id INT COMMENT 'ID of affected resource',
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT NULL,
details TEXT COMMENT 'JSON or text details about the action',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_action (action),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================================================
-- ROLLBACK INSTRUCTIONS (if needed)
-- ============================================================================
-- ALTER TABLE users DROP COLUMN locked_until;
-- DROP TABLE IF EXISTS login_attempts;
-- DROP TABLE IF EXISTS audit_log;

View File

@@ -10,24 +10,53 @@ $status = 'AWAITING PAYMENT';
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id); $description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id);
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'membership_application', null, ['endpoint' => 'process_application.php']);
http_response_code(403);
die('Security token validation failed. Please try again.');
}
// Get all the form fields // Get all the form fields with validation
$first_name = $_POST['first_name']; $first_name = validateName($_POST['first_name'] ?? '');
$last_name = $_POST['last_name']; if ($first_name === false) {
$id_number = $_POST['id_number']; die('Invalid first name format.');
$dob = $_POST['dob']; }
$occupation = $_POST['occupation'];
$tel_cell = $_POST['tel_cell']; $last_name = validateName($_POST['last_name'] ?? '');
$email = $_POST['email']; if ($last_name === false) {
die('Invalid last name format.');
}
$id_number = validateSAIDNumber($_POST['id_number'] ?? '');
if ($id_number === false) {
die('Invalid ID number format.');
}
$dob = validateDate($_POST['dob'] ?? '');
if ($dob === false) {
die('Invalid date of birth format.');
}
$occupation = sanitizeTextInput($_POST['occupation'] ?? '', 100);
$tel_cell = validatePhoneNumber($_POST['tel_cell'] ?? '');
if ($tel_cell === false) {
die('Invalid phone number format.');
}
$email = validateEmail($_POST['email'] ?? '');
if ($email === false) {
die('Invalid email format.');
}
// Spouse or Partner details (optional) // Spouse or Partner details (optional)
$spouse_first_name = !empty($_POST['spouse_first_name']) ? $_POST['spouse_first_name'] : null; $spouse_first_name = !empty($_POST['spouse_first_name']) ? validateName($_POST['spouse_first_name']) : null;
$spouse_last_name = !empty($_POST['spouse_last_name']) ? $_POST['spouse_last_name'] : null; $spouse_last_name = !empty($_POST['spouse_last_name']) ? validateName($_POST['spouse_last_name']) : null;
$spouse_id_number = !empty($_POST['spouse_id_number']) ? $_POST['spouse_id_number'] : null; $spouse_id_number = !empty($_POST['spouse_id_number']) ? validateSAIDNumber($_POST['spouse_id_number']) : null;
$spouse_dob = !empty($_POST['spouse_dob']) ? $_POST['spouse_dob'] : NULL; // if empty, set to NULL $spouse_dob = !empty($_POST['spouse_dob']) ? validateDate($_POST['spouse_dob']) : NULL;
$spouse_occupation = !empty($_POST['spouse_occupation']) ? $_POST['spouse_occupation'] : null; $spouse_occupation = !empty($_POST['spouse_occupation']) ? sanitizeTextInput($_POST['spouse_occupation'], 100) : null;
$spouse_tel_cell = !empty($_POST['spouse_tel_cell']) ? $_POST['spouse_tel_cell'] : null; $spouse_tel_cell = !empty($_POST['spouse_tel_cell']) ? validatePhoneNumber($_POST['spouse_tel_cell']) : null;
$spouse_email = !empty($_POST['spouse_email']) ? $_POST['spouse_email'] : null; $spouse_email = !empty($_POST['spouse_email']) ? validateEmail($_POST['spouse_email']) : null;
// Children details (optional) // Children details (optional)
$child_name1 = !empty($_POST['child_name1']) ? $_POST['child_name1'] : null; $child_name1 = !empty($_POST['child_name1']) ? $_POST['child_name1'] : null;

View File

@@ -11,12 +11,45 @@ $user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
// Check if the form has been submitted // Check if the form has been submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_booking.php']);
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
exit();
}
// Validate dates and integers
$from_date = validateDate($_POST['from_date'] ?? '');
if ($from_date === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid from date format.']);
exit();
}
$to_date = validateDate($_POST['to_date'] ?? '');
if ($to_date === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid to date format.']);
exit();
}
$num_vehicles = validateInteger($_POST['vehicles'] ?? 0, 1, 10);
if ($num_vehicles === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid number of vehicles.']);
exit();
}
$num_adults = validateInteger($_POST['adults'] ?? 0, 0, 20);
if ($num_adults === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid number of adults.']);
exit();
}
$num_children = validateInteger($_POST['children'] ?? 0, 0, 20);
if ($num_children === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid number of children.']);
exit();
}
// Get values from the form // Get values from the form
$from_date = $_POST['from_date'];
$to_date = $_POST['to_date'];
$num_vehicles = (int)$_POST['vehicles'];
$num_adults = (int)$_POST['adults'];
$num_children = (int)$_POST['children'];
$add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras $add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras
$is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status $is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status
$type = "camping"; $type = "camping";

View File

@@ -18,12 +18,45 @@ $is_member = getUserMemberStatus($user_id);
// Check if the form has been submitted // Check if the form has been submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_camp_booking.php']);
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
exit();
}
// Validate dates and integers
$from_date = validateDate($_POST['from_date'] ?? '');
if ($from_date === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid from date format.']);
exit();
}
$to_date = validateDate($_POST['to_date'] ?? '');
if ($to_date === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid to date format.']);
exit();
}
$num_vehicles = validateInteger($_POST['vehicles'] ?? 1, 1, 10);
if ($num_vehicles === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid number of vehicles.']);
exit();
}
$num_adults = validateInteger($_POST['adults'] ?? 0, 0, 20);
if ($num_adults === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid number of adults.']);
exit();
}
$num_children = validateInteger($_POST['children'] ?? 0, 0, 20);
if ($num_children === false) {
echo json_encode(['status' => 'error', 'message' => 'Invalid number of children.']);
exit();
}
// Get values from the form // Get values from the form
$from_date = $_POST['from_date'];
$to_date = $_POST['to_date'];
$num_vehicles = (int)$_POST['vehicles'];
$num_adults = (int)$_POST['adults'];
$num_children = (int)$_POST['children'];
$add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras $add_firewood = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras
// $is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status // $is_member = isset($_POST['is_member']) ? (int)$_POST['is_member'] : 0; // Hidden member status
$type = "camping"; $type = "camping";

View File

@@ -18,10 +18,30 @@ $pending_member = getUserMemberStatusPending($user_id);
// Check if the form has been submitted // Check if the form has been submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_course_booking.php']);
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => 'Security token validation failed.']);
exit();
}
// Input variables from the form (use default values if not provided) // Input variables from the form (use default values if not provided)
$additional_members = isset($_POST['members']) ? intval($_POST['members']) : 0; // Default to 1 vehicle $additional_members = validateInteger($_POST['members'] ?? 0, 0, 20);
$num_adults = isset($_POST['non-members']) ? intval($_POST['non-members']) : 0; // Default to 1 adult if ($additional_members === false) $additional_members = 0;
$course_id = isset($_POST['course_id']) ? intval($_POST['course_id']) : 0; // Default to 0 children
$num_adults = validateInteger($_POST['non-members'] ?? 0, 0, 20);
if ($num_adults === false) $num_adults = 0;
$course_id = validateInteger($_POST['course_id'] ?? 0, 1, 999999);
if ($course_id === false) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(['error' => 'Invalid course ID.']);
exit();
}
checkAndRedirectCourseBooking($course_id); checkAndRedirectCourseBooking($course_id);
// Fetch trip costs from the database // Fetch trip costs from the database
$query = "SELECT date, cost_members, cost_nonmembers, course_type FROM courses WHERE course_id = ?"; $query = "SELECT date, cost_members, cost_nonmembers, course_type FROM courses WHERE course_id = ?";

View File

@@ -4,9 +4,20 @@ require_once("session.php");
require_once("connection.php"); require_once("connection.php");
require_once("functions.php"); require_once("functions.php");
checkAdmin(); checkAdmin();
// CSRF Token Validation for POST requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog($_SESSION['user_id'] ?? null, 'CSRF_VALIDATION_FAILED', 'efts', null, ['endpoint' => 'process_eft.php']);
http_response_code(403);
die('Security token validation failed.');
}
}
if (!isset($_GET['token']) || empty($_GET['token'])) { if (!isset($_GET['token']) || empty($_GET['token'])) {
die("Invalid request."); die("Invalid request.");
} }
$token = $_GET['token']; $token = $_GET['token'];
// echo $token; // echo $token;
$eft_id = decryptData($token, $salt); $eft_id = decryptData($token, $salt);

View File

@@ -9,6 +9,12 @@ if (!isset($_SESSION['user_id'])) {
} }
if (isset($_POST['signature'])) { if (isset($_POST['signature'])) {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog($_SESSION['user_id'], 'CSRF_VALIDATION_FAILED', 'membership_application', null, ['endpoint' => 'process_signature.php']);
die(json_encode(['status' => 'error', 'message' => 'Security token validation failed']));
}
$user_id = $_SESSION['user_id']; // Get the user ID from the session $user_id = $_SESSION['user_id']; // Get the user ID from the session
$signature = $_POST['signature']; // Base64 image data $signature = $_POST['signature']; // Base64 image data

View File

@@ -30,12 +30,27 @@ $is_member = getUserMemberStatus($user_id);
// Check if the form has been submitted // Check if the form has been submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog($user_id, 'CSRF_VALIDATION_FAILED', 'bookings', null, ['endpoint' => 'process_trip_booking.php']);
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => 'Security token validation failed.']);
exit();
}
// Input variables from the form (use default values if not provided) // Input variables from the form (use default values if not provided)
$num_vehicles = isset($_POST['vehicles']) ? intval($_POST['vehicles']) : 1; // Default to 1 vehicle $num_vehicles = validateInteger($_POST['vehicles'] ?? 1, 1, 10);
$num_adults = isset($_POST['adults']) ? intval($_POST['adults']) : 1; // Default to 1 adult if ($num_vehicles === false) $num_vehicles = 1;
$num_children = isset($_POST['children']) ? intval($_POST['children']) : 0; // Default to 0 children
$num_pensioners = isset($_POST['pensioners']) ? intval($_POST['pensioners']) : 0; // Default to 0 pensioners $num_adults = validateInteger($_POST['adults'] ?? 1, 1, 20);
// $radio = isset($_POST['AddExtra']) ? 1 : 0; // Checkbox for extras if ($num_adults === false) $num_adults = 1;
$num_children = validateInteger($_POST['children'] ?? 0, 0, 20);
if ($num_children === false) $num_children = 0;
$num_pensioners = validateInteger($_POST['pensioners'] ?? 0, 0, 20);
if ($num_pensioners === false) $num_pensioners = 0;
// Fetch trip costs from the database // Fetch trip costs from the database
$query = "SELECT trip_name, cost_members, cost_nonmembers, cost_pensioner_member, cost_pensioner, booking_fee, start_date, end_date, trip_code FROM trips WHERE trip_id = ?"; $query = "SELECT trip_name, cost_members, cost_nonmembers, cost_pensioner_member, cost_pensioner, booking_fee, start_date, end_date, trip_code FROM trips WHERE trip_id = ?";
$stmt = $conn->prepare($query); $stmt = $conn->prepare($query);

View File

@@ -1,13 +1,12 @@
<?php <?php
require_once("env.php"); require_once("env.php");
require_once("session.php");
require_once("connection.php"); require_once("connection.php");
require_once("functions.php"); require_once("functions.php");
require_once "vendor/autoload.php"; require_once "vendor/autoload.php";
use GuzzleHttp\Client; use GuzzleHttp\Client;
// Create connection // Create connection
$conn = openDatabaseConnection(); $conn = openDatabaseConnection();
@@ -19,20 +18,78 @@ if ($conn->connect_error) {
// Form processing // Form processing
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sanitize and validate input // CSRF Token Validation
$first_name = ucwords(strtolower($conn->real_escape_string($_POST['first_name']))); if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
$last_name = ucwords(strtolower($conn->real_escape_string($_POST['last_name']))); auditLog(null, 'CSRF_VALIDATION_FAILED', 'users', null, ['endpoint' => 'register_user.php']);
$phone_number = $conn->real_escape_string($_POST['phone_number']); echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL); exit();
$password = $_POST['password']; }
$password_confirm = $_POST['password_confirm'];
$name = $first_name . " " . $last_name; // Check rate limiting on registration endpoint (by IP)
$ip = getClientIPAddress();
// Basic validation $cutoffTime = date('Y-m-d H:i:s', time() - (3600)); // Last hour
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$stmt = $conn->prepare("SELECT COUNT(*) as count FROM audit_log WHERE action = 'REGISTRATION_ATTEMPT' AND ip_address = ? AND created_at > ?");
$stmt->bind_param('ss', $ip, $cutoffTime);
$stmt->execute();
$stmt->bind_result($regAttempts);
$stmt->fetch();
$stmt->close();
// Allow max 5 registration attempts per IP per hour
if ($regAttempts >= 5) {
auditLog(null, 'REGISTRATION_RATE_LIMIT_EXCEEDED', 'users', null, ['ip' => $ip, 'attempts' => $regAttempts]);
echo json_encode(['status' => 'error', 'message' => 'Too many registration attempts. Please try again later.']);
exit();
}
// Validate and sanitize first name
$first_name = validateName($_POST['first_name'] ?? '');
if ($first_name === false) {
auditLog(null, 'REGISTRATION_INVALID_FIRST_NAME', 'users');
echo json_encode(['status' => 'error', 'message' => 'Invalid first name. Only letters, spaces, hyphens, and apostrophes allowed (2-100 characters).']);
exit();
}
// Validate and sanitize last name
$last_name = validateName($_POST['last_name'] ?? '');
if ($last_name === false) {
auditLog(null, 'REGISTRATION_INVALID_LAST_NAME', 'users');
echo json_encode(['status' => 'error', 'message' => 'Invalid last name. Only letters, spaces, hyphens, and apostrophes allowed (2-100 characters).']);
exit();
}
// Validate and sanitize phone number
$phone_number = validatePhoneNumber($_POST['phone_number'] ?? '');
if ($phone_number === false) {
auditLog(null, 'REGISTRATION_INVALID_PHONE', 'users');
echo json_encode(['status' => 'error', 'message' => 'Invalid phone number format.']);
exit();
}
// Validate email
$email = validateEmail($_POST['email'] ?? '');
if ($email === false) {
auditLog(null, 'REGISTRATION_INVALID_EMAIL', 'users');
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']); echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
exit(); exit();
} }
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
// Validate password strength (minimum 8 characters, must contain uppercase, lowercase, number, special char)
if (strlen($password) < 8) {
echo json_encode(['status' => 'error', 'message' => 'Password must be at least 8 characters long.']);
exit();
}
if (!preg_match('/[A-Z]/', $password) || !preg_match('/[a-z]/', $password) ||
!preg_match('/[0-9]/', $password) || !preg_match('/[!@#$%^&*]/', $password)) {
echo json_encode(['status' => 'error', 'message' => 'Password must contain uppercase, lowercase, number, and special character (!@#$%^&*).']);
exit();
}
if ($password !== $password_confirm) { if ($password !== $password_confirm) {
echo json_encode(['status' => 'error', 'message' => 'Passwords do not match.']); echo json_encode(['status' => 'error', 'message' => 'Passwords do not match.']);
exit(); exit();
@@ -45,6 +102,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->store_result(); $stmt->store_result();
if ($stmt->num_rows > 0) { if ($stmt->num_rows > 0) {
auditLog(null, 'REGISTRATION_EMAIL_EXISTS', 'users', null, ['email' => $email]);
echo json_encode(['status' => 'error', 'message' => 'Email is already registered.']); echo json_encode(['status' => 'error', 'message' => 'Email is already registered.']);
$stmt->close(); $stmt->close();
$conn->close(); $conn->close();
@@ -56,7 +114,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Hash password // Hash password
$hashed_password = password_hash($password, PASSWORD_BCRYPT); $hashed_password = password_hash($password, PASSWORD_BCRYPT);
// Generate token // Generate email verification token
$token = bin2hex(random_bytes(50)); $token = bin2hex(random_bytes(50));
// Prepare and execute query // Prepare and execute query
@@ -68,14 +126,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($stmt->execute()) { if ($stmt->execute()) {
$newUser_id = $conn->insert_id; $newUser_id = $conn->insert_id;
processLegacyMembership($newUser_id); processLegacyMembership($newUser_id);
if (sendVerificationEmail($email, $name, $token)) { auditLog($newUser_id, 'USER_REGISTRATION', 'users', $newUser_id, ['email' => $email]);
sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name . ' has just created an account using Credentials.');
if (sendVerificationEmail($email, $first_name . ' ' . $last_name, $token)) {
sendEmail($_ENV['ADMIN_EMAIL'], '4WDCSA: New User Registration', $first_name . ' ' . $last_name . ' (' . $email . ') has just created an account using Credentials.');
echo json_encode(['status' => 'success', 'message' => 'Registration successful. Please check your email to verify your account.']); echo json_encode(['status' => 'success', 'message' => 'Registration successful. Please check your email to verify your account.']);
} else { } else {
echo json_encode(['status' => 'error', 'message' => 'Failed to send verification email.']); echo json_encode(['status' => 'error', 'message' => 'Failed to send verification email.']);
} }
} else { } else {
echo json_encode(['status' => 'error', 'message' => 'Failed to register user: ' . $stmt->error]); auditLog(null, 'REGISTRATION_DATABASE_ERROR', 'users', null, ['email' => $email]);
echo json_encode(['status' => 'error', 'message' => 'Failed to register user.']);
} }
$stmt->close(); $stmt->close();

20
run_migration.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
require 'env.php';
require 'connection.php';
$conn = openDatabaseConnection();
if (!$conn) {
die('Database connection failed');
}
$sql = file_get_contents('migrations/001_phase1_security_schema.sql');
if ($conn->multi_query($sql)) {
echo "✓ Migration executed successfully\n";
} else {
echo "✗ Migration error: " . $conn->error . "\n";
}
$conn->close();
?>

View File

@@ -13,8 +13,8 @@ if (!$conn) {
// Google Client Setup // Google Client Setup
$client = new Google_Client(); $client = new Google_Client();
$client->setClientId('948441222188-8qhboq2urr8o9n35mc70s5h2nhd52v0m.apps.googleusercontent.com'); $client->setClientId($_ENV['GOOGLE_CLIENT_ID']);
$client->setClientSecret('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'); $client->setClientSecret($_ENV['GOOGLE_CLIENT_SECRET']);
$client->setRedirectUri($_ENV['HOST'] . '/validate_login.php'); $client->setRedirectUri($_ENV['HOST'] . '/validate_login.php');
$client->addScope("email"); $client->addScope("email");
$client->addScope("profile"); $client->addScope("profile");
@@ -86,18 +86,57 @@ if (isset($_GET['code'])) {
// Check if email and password login is requested // Check if email and password login is requested
if (isset($_POST['email']) && isset($_POST['password'])) { if (isset($_POST['email']) && isset($_POST['password'])) {
// Retrieve and sanitize form data // CSRF Token Validation
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL); if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
$password = trim($_POST['password']); // Remove extra spaces auditLog(null, 'CSRF_VALIDATION_FAILED', 'users', null, ['endpoint' => 'validate_login.php']);
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
// Validate input exit();
}
// Retrieve and validate email input
$email = validateEmail($_POST['email']);
if ($email === false) {
auditLog(null, 'INVALID_EMAIL_FORMAT', 'users', null, ['email' => $_POST['email']]);
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
exit();
}
// Retrieve and sanitize password
$password = isset($_POST['password']) ? trim($_POST['password']) : '';
// Basic validation
if (empty($email) || empty($password)) { if (empty($email) || empty($password)) {
echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']); echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']);
exit(); exit();
} }
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { // Check for account lockout
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']); $lockoutStatus = checkAccountLockout($email);
if ($lockoutStatus['is_locked']) {
auditLog(null, 'LOGIN_ATTEMPT_LOCKED_ACCOUNT', 'users', null, [
'email' => $email,
'locked_until' => $lockoutStatus['locked_until']
]);
echo json_encode([
'status' => 'error',
'message' => 'Account is temporarily locked due to multiple failed login attempts. Please try again in ' . $lockoutStatus['minutes_remaining'] . ' minutes.'
]);
exit();
}
// Check recent failed attempts
$recentFailedAttempts = countRecentFailedAttempts($email);
if ($recentFailedAttempts >= 5) {
// Lock account for 15 minutes
lockAccount($email, 15);
auditLog(null, 'ACCOUNT_LOCKED_THRESHOLD', 'users', null, [
'email' => $email,
'failed_attempts' => $recentFailedAttempts
]);
echo json_encode([
'status' => 'error',
'message' => 'Account locked due to multiple failed login attempts. Please try again in 15 minutes.'
]);
exit(); exit();
} }
@@ -120,22 +159,55 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
// Check if the user is verified // Check if the user is verified
if ($row['is_verified'] == 0) { if ($row['is_verified'] == 0) {
recordLoginAttempt($email, false);
auditLog(null, 'LOGIN_ATTEMPT_UNVERIFIED_ACCOUNT', 'users', $row['user_id']);
echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']); echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']);
exit(); exit();
} }
if (password_verify($password, $row['password'])) { if (password_verify($password, $row['password'])) {
// Record successful attempt
recordLoginAttempt($email, true);
// Regenerate session ID to prevent session fixation attacks
session_regenerate_id(true);
// Password is correct, set up session // Password is correct, set up session
$_SESSION['user_id'] = $row['user_id']; // Adjust as per your table structure $_SESSION['user_id'] = $row['user_id'];
$_SESSION['first_name'] = $row['first_name']; // Adjust as per your table structure $_SESSION['first_name'] = $row['first_name'];
$_SESSION['profile_pic'] = $row['profile_pic']; $_SESSION['profile_pic'] = $row['profile_pic'];
// Set session timeout (30 minutes)
$_SESSION['login_time'] = time();
$_SESSION['session_timeout'] = 1800; // 30 minutes in seconds
auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']);
echo json_encode(['status' => 'success', 'message' => 'Successful Login']); echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
} else { } else {
// Password is incorrect // Password is incorrect - record failed attempt
echo json_encode(['status' => 'error', 'message' => 'Invalid password.']); recordLoginAttempt($email, false);
auditLog(null, 'LOGIN_FAILED_INVALID_PASSWORD', 'users', null, ['email' => $email]);
// Check if this was the threshold failure
$newFailureCount = countRecentFailedAttempts($email);
if ($newFailureCount >= 5) {
lockAccount($email, 15);
echo json_encode([
'status' => 'error',
'message' => 'Too many failed login attempts. Account locked for 15 minutes.'
]);
} else {
$attemptsRemaining = 5 - $newFailureCount;
echo json_encode([
'status' => 'error',
'message' => 'Invalid password. ' . $attemptsRemaining . ' attempts remaining before account lockout.'
]);
}
} }
} else { } else {
// User does not exist // User does not exist - still record attempt
recordLoginAttempt($email, false);
auditLog(null, 'LOGIN_FAILED_USER_NOT_FOUND', 'users', null, ['email' => $email]);
echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']); echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']);
} }