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

+ +
+
+

reCAPTCHA Failures

+
+
+
+

Direct POST Attempts

+
+
+
+

Rate Limit Hits

+
+
+
+

Low Interaction Scores

+
+
+
+

Blocked IPs

+
+
+
+

Successful Submissions

+
+
+
+ +
+

Hourly Spam Distribution

+
+ 0 ? ($count / $maxHourly * 100) : 0; + ?> +
+
:00
+
+
+
+ +
+
+ +
+

Top Spam Source IPs

+ + + + + + + + + + $count): ?> + + + + + + + +
IP AddressAttemptsStatus
+ Blocked' : 'Active'; + ?> +
+
+ +
+

Recent Error Log (Last 20)

+ + + + + + + + + + + + + + + +
TimestampError
+
+ +
+

Recent Successful Submissions (Last 10)

+ + + + + + + + + + + + + + + +
TimestampDetails
+
+
+ + \ 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 + + + +