diff --git a/.recaptcha-config.php b/.recaptcha-config.php
new file mode 100644
index 0000000..5139c8a
--- /dev/null
+++ b/.recaptcha-config.php
@@ -0,0 +1,7 @@
+
diff --git a/admin/spam-dashboard.php b/admin/spam-dashboard.php
new file mode 100644
index 0000000..7a8a190
--- /dev/null
+++ b/admin/spam-dashboard.php
@@ -0,0 +1,233 @@
+ $matches[1],
+ 'message' => $matches[2]
+ ];
+ }
+ }
+
+ return array_reverse($entries); // Most recent first
+}
+
+// Get log data
+$contactSubmissions = parseLogFile('../logs/contact-submissions.log');
+$contactErrors = parseLogFile('../logs/contact-errors.log');
+$blockedIPs = file_exists('../logs/blocked-ips.txt') ? file('../logs/blocked-ips.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
+
+// Analyze spam patterns
+$spamPatterns = [
+ 'recaptcha_failures' => 0,
+ 'direct_post_attempts' => 0,
+ 'rate_limit_hits' => 0,
+ 'low_interaction_scores' => 0,
+ 'blocked_ips_count' => count($blockedIPs),
+ 'hourly_distribution' => array_fill(0, 24, 0),
+ 'top_ips' => []
+];
+
+$ipCounts = [];
+
+foreach ($contactErrors as $error) {
+ if (strpos($error['message'], 'RECAPTCHA FAILED') !== false) {
+ $spamPatterns['recaptcha_failures']++;
+ }
+ if (strpos($error['message'], 'DIRECT POST') !== false) {
+ $spamPatterns['direct_post_attempts']++;
+ }
+ if (strpos($error['message'], 'Too many requests') !== false) {
+ $spamPatterns['rate_limit_hits']++;
+ }
+ if (strpos($error['message'], 'LOW INTERACTION SCORE') !== false) {
+ $spamPatterns['low_interaction_scores']++;
+ }
+
+ // Extract IP addresses
+ if (preg_match('/from ([0-9.]+)/', $error['message'], $matches)) {
+ $ip = $matches[1];
+ $ipCounts[$ip] = ($ipCounts[$ip] ?? 0) + 1;
+ }
+
+ // Track hourly distribution
+ $hour = (int)date('G', strtotime($error['timestamp']));
+ $spamPatterns['hourly_distribution'][$hour]++;
+}
+
+// Get top spam IPs
+arsort($ipCounts);
+$spamPatterns['top_ips'] = array_slice($ipCounts, 0, 10, true);
+
+?>
+
+
+
+
+
+ Spam Analysis Dashboard - UK Data Services
+
+
+
+
+
Spam Analysis Dashboard
+
+
+
+
+
Direct POST Attempts
+
+
+
+
+
Low Interaction Scores
+
+
+
+
+
Successful Submissions
+
+
+
+
+
+
Hourly Spam Distribution
+
+ 0 ? ($count / $maxHourly * 100) : 0;
+ ?>
+
+
+
+
+
+
+
Top Spam Source IPs
+
+
+
+ | IP Address |
+ Attempts |
+ Status |
+
+
+
+ $count): ?>
+
+ |
+ |
+
+ Blocked' : 'Active';
+ ?>
+ |
+
+
+
+
+
+
+
+
Recent Error Log (Last 20)
+
+
+
+ | Timestamp |
+ Error |
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
Recent Successful Submissions (Last 10)
+
+
+
+ | Timestamp |
+ Details |
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/js/main.js b/assets/js/main.js
index 58fd5c3..f6f701f 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -171,13 +171,82 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Enhanced animations initialized');
- // Form Validation and Enhancement
+ // Enhanced Form Validation with Anti-Spam Measures
const contactForm = document.querySelector('.contact-form form');
if (contactForm) {
+ // Track form interactions for bot detection
+ let formInteractions = {
+ mouseMovements: 0,
+ keystrokes: 0,
+ focusEvents: 0,
+ startTime: Date.now(),
+ fields: {}
+ };
+
+ // Add hidden timestamp field
+ const timestampField = document.createElement('input');
+ timestampField.type = 'hidden';
+ timestampField.name = 'form_timestamp';
+ timestampField.value = Date.now();
+ contactForm.appendChild(timestampField);
+
+ // Add hidden interaction token
+ const interactionToken = document.createElement('input');
+ interactionToken.type = 'hidden';
+ interactionToken.name = 'interaction_token';
+ contactForm.appendChild(interactionToken);
+
+ // Track mouse movements (humans move mouse)
+ let mouseMoveHandler = function() {
+ if (formInteractions.mouseMovements < 100) {
+ formInteractions.mouseMovements++;
+ }
+ };
+ document.addEventListener('mousemove', mouseMoveHandler);
+
+ // Track interactions on form fields
+ contactForm.querySelectorAll('input, textarea, select').forEach(field => {
+ field.addEventListener('keydown', function() {
+ formInteractions.keystrokes++;
+ if (!formInteractions.fields[field.name]) {
+ formInteractions.fields[field.name] = {
+ keystrokes: 0,
+ changes: 0,
+ focusTime: 0
+ };
+ }
+ formInteractions.fields[field.name].keystrokes++;
+ });
+
+ field.addEventListener('focus', function() {
+ formInteractions.focusEvents++;
+ if (formInteractions.fields[field.name]) {
+ formInteractions.fields[field.name].focusTime = Date.now();
+ }
+ });
+
+ field.addEventListener('change', function() {
+ if (formInteractions.fields[field.name]) {
+ formInteractions.fields[field.name].changes++;
+ }
+ });
+ });
+
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
+ // Calculate interaction score
+ const timeSpent = Date.now() - formInteractions.startTime;
+ const interactionScore = calculateInteractionScore(formInteractions, timeSpent);
+
+ // Set interaction token
+ interactionToken.value = btoa(JSON.stringify({
+ score: interactionScore,
+ time: timeSpent,
+ interactions: formInteractions.mouseMovements + formInteractions.keystrokes + formInteractions.focusEvents
+ }));
+
// Basic form validation
const formData = new FormData(this);
const name = formData.get('name');
@@ -203,6 +272,12 @@ document.addEventListener('DOMContentLoaded', function() {
isValid = false;
}
+ // Check interaction score (low score = likely bot)
+ if (interactionScore < 30) {
+ errors.push('Please complete the form normally');
+ isValid = false;
+ }
+
if (isValid) {
// Show loading state
const submitButton = this.querySelector('button[type="submit"]');
@@ -210,18 +285,29 @@ document.addEventListener('DOMContentLoaded', function() {
submitButton.textContent = 'Sending...';
submitButton.disabled = true;
- // Submit form (you'll need to implement the backend handler)
+ // Submit form with XMLHttpRequest header
fetch('contact-handler.php', {
method: 'POST',
- body: formData
+ body: formData,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ }
})
.then(response => response.json())
.then(data => {
if (data.success) {
- showNotification('Message sent successfully! We\'ll get back to you soon.', 'success');
+ showNotification(data.message || 'Message sent successfully! We\'ll get back to you soon.', 'success');
this.reset();
+ // Reset interaction tracking
+ formInteractions = {
+ mouseMovements: 0,
+ keystrokes: 0,
+ focusEvents: 0,
+ startTime: Date.now(),
+ fields: {}
+ };
} else {
- showNotification('There was an error sending your message. Please try again.', 'error');
+ showNotification(data.message || 'There was an error sending your message. Please try again.', 'error');
}
})
.catch(error => {
@@ -238,6 +324,29 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
+ // Calculate interaction score to detect bots
+ function calculateInteractionScore(interactions, timeSpent) {
+ let score = 0;
+
+ // Time-based scoring (bots submit too fast)
+ if (timeSpent > 5000) score += 20; // More than 5 seconds
+ if (timeSpent > 10000) score += 20; // More than 10 seconds
+ if (timeSpent > 30000) score += 10; // More than 30 seconds
+
+ // Mouse movement scoring (humans move mouse)
+ if (interactions.mouseMovements > 10) score += 20;
+ if (interactions.mouseMovements > 50) score += 10;
+
+ // Keystroke scoring (humans type)
+ if (interactions.keystrokes > 20) score += 20;
+ if (interactions.keystrokes > 50) score += 10;
+
+ // Focus event scoring (humans tab/click between fields)
+ if (interactions.focusEvents > 3) score += 10;
+
+ return Math.min(score, 100); // Cap at 100
+ }
+
// Email validation function
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
diff --git a/contact-handler.php b/contact-handler.php
index 52c9fee..6a80328 100644
--- a/contact-handler.php
+++ b/contact-handler.php
@@ -2,6 +2,9 @@
// Enhanced Contact Form Handler with Security
session_start();
+// Include reCAPTCHA config
+require_once '.recaptcha-config.php';
+
// Security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
@@ -143,6 +146,54 @@ if (!checkRateLimit()) {
sendResponse(false, 'Too many requests. Please try again later.');
}
+// Verify reCAPTCHA v3
+if (isset($_POST['recaptcha_response'])) {
+ $recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
+ $recaptcha_secret = RECAPTCHA_SECRET_KEY;
+ $recaptcha_response = $_POST['recaptcha_response'];
+
+ $recaptcha_data = array(
+ 'secret' => $recaptcha_secret,
+ 'response' => $recaptcha_response,
+ 'remoteip' => $_SERVER['REMOTE_ADDR']
+ );
+
+ $recaptcha_options = array(
+ 'http' => array(
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'method' => 'POST',
+ 'content' => http_build_query($recaptcha_data)
+ )
+ );
+
+ $recaptcha_context = stream_context_create($recaptcha_options);
+ $recaptcha_result = @file_get_contents($recaptcha_url, false, $recaptcha_context);
+
+ if ($recaptcha_result === false) {
+ // Log reCAPTCHA API failure but don't block submission
+ error_log("reCAPTCHA API call failed for IP: " . $_SERVER['REMOTE_ADDR']);
+ } else {
+ $recaptcha_json = json_decode($recaptcha_result, true);
+
+ if (!$recaptcha_json['success'] || $recaptcha_json['score'] < RECAPTCHA_THRESHOLD) {
+ // Log suspicious activity and block
+ $logEntry = date('Y-m-d H:i:s') . " - RECAPTCHA FAILED: Score " . ($recaptcha_json['score'] ?? '0') . " from " . $_SERVER['REMOTE_ADDR'] . "\n";
+ file_put_contents('logs/contact-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
+
+ // Add to blocked IPs if score is very low
+ if (isset($recaptcha_json['score']) && $recaptcha_json['score'] < 0.3) {
+ $blockEntry = $_SERVER['REMOTE_ADDR'] . '|' . time() . "\n";
+ file_put_contents('logs/blocked-ips.txt', $blockEntry, FILE_APPEND | LOCK_EX);
+ }
+
+ sendResponse(false, 'Security verification failed. Please try again.');
+ }
+ }
+} else {
+ // No reCAPTCHA response - likely a bot
+ sendResponse(false, 'Security verification required.');
+}
+
// Validate and sanitize inputs
$name = validateInput($_POST['name'] ?? '', 'text');
$email = validateInput($_POST['email'] ?? '', 'email');
@@ -182,9 +233,24 @@ $spamKeywords = [
'no obligation', 'free money', 'make money fast', 'work from home', 'get rich',
'investment opportunity', 'credit repair', 'refinance', 'consolidate debt',
'weight loss', 'miracle cure', 'lose weight', 'adult content', 'porn',
- 'sex', 'dating', 'singles', 'webcam', 'escort', 'massage'
+ 'sex', 'dating', 'singles', 'webcam', 'escort', 'massage', 'crypto', 'nft',
+ 'blockchain', 'earn money', 'passive income', 'financial freedom', 'mlm',
+ 'network marketing', 'online casino', 'gambling', 'betting', 'binary options'
];
+// Check for disposable email domains
+$disposableEmailDomains = [
+ 'mailinator.com', 'guerrillamail.com', '10minutemail.com', 'temp-mail.org',
+ 'throwawaymail.com', 'yopmail.com', 'mailnesia.com', 'trashmail.com',
+ 'maildrop.cc', 'mailcatch.com', 'tempmail.com', 'email-fake.com',
+ 'fakeinbox.com', 'sharklasers.com', 'guerrillamailblock.com'
+];
+
+$emailDomain = substr(strrchr($email, "@"), 1);
+if (in_array(strtolower($emailDomain), $disposableEmailDomains)) {
+ sendResponse(false, 'Please use a valid business email address.');
+}
+
$messageContent = strtolower($message . ' ' . $name . ' ' . $company);
foreach ($spamKeywords as $keyword) {
if (strpos($messageContent, $keyword) !== false) {
@@ -209,6 +275,39 @@ if (isset($_SESSION['form_start_time'])) {
}
}
+// Check for XMLHttpRequest header (JavaScript submission)
+if (!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
+ // Log direct POST attempt
+ $logEntry = date('Y-m-d H:i:s') . " - DIRECT POST attempt from " . $_SERVER['REMOTE_ADDR'] . "\n";
+ file_put_contents('logs/contact-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
+ sendResponse(false, 'Invalid submission method');
+}
+
+// Verify interaction token (human behavior verification)
+if (isset($_POST['interaction_token']) && !empty($_POST['interaction_token'])) {
+ $tokenData = @json_decode(base64_decode($_POST['interaction_token']), true);
+ if ($tokenData && isset($tokenData['score'])) {
+ if ($tokenData['score'] < 30) {
+ // Log low interaction score
+ $logEntry = date('Y-m-d H:i:s') . " - LOW INTERACTION SCORE: " . $tokenData['score'] . " from " . $_SERVER['REMOTE_ADDR'] . "\n";
+ file_put_contents('logs/contact-errors.log', $logEntry, FILE_APPEND | LOCK_EX);
+ sendResponse(false, 'Please complete the form normally');
+ }
+ }
+}
+
+// Verify form timestamp (prevent replay attacks)
+if (isset($_POST['form_timestamp'])) {
+ $formTimestamp = intval($_POST['form_timestamp']);
+ $currentTime = time() * 1000; // Convert to milliseconds
+ $timeDiff = $currentTime - $formTimestamp;
+
+ // Form older than 1 hour or from the future
+ if ($timeDiff > 3600000 || $timeDiff < 0) {
+ sendResponse(false, 'Form session expired. Please refresh and try again.');
+ }
+}
+
// Update rate limit counter
$ip = $_SERVER['REMOTE_ADDR'];
$key = 'contact_' . md5($ip);
diff --git a/index.php b/index.php
index 88e2640..578b8e3 100644
--- a/index.php
+++ b/index.php
@@ -98,6 +98,10 @@ $twitter_card_image = "https://ukdataservices.co.uk/assets/images/ukds-main-logo
+
+
+
+
@@ -920,6 +924,7 @@ $twitter_card_image = "https://ukdataservices.co.uk/assets/images/ukds-main-logo
+
@@ -985,6 +990,26 @@ $twitter_card_image = "https://ukdataservices.co.uk/assets/images/ukds-main-logo
+
+
+
+