diff --git a/public/css/contact.css b/public/css/contact.css index 7014366..019cac2 100644 --- a/public/css/contact.css +++ b/public/css/contact.css @@ -10,7 +10,8 @@ label { font-weight: 600; - flex: 0 0 120px; /* fixed label width */ + flex: 0 0 120px; + /* fixed label width */ margin-right: 0.5rem; text-align: right; } @@ -22,6 +23,7 @@ margin-bottom: 1rem; flex-wrap: nowrap; } + .form-group input, .form-group textarea { padding: 0.5em; @@ -33,12 +35,14 @@ flex: 1 1 auto; min-width: 200px; } + .form-group.message-group textarea { flex: 1 1 auto; min-width: 250px; min-height: 120px; resize: vertical; } + .form-group.message-group { flex-direction: row; align-items: center; @@ -68,13 +72,7 @@ button:hover { background-color: #005fa3; } -/* -label { - flex: 0 0 120px; - font-weight: 600; - text-align: right; -} -*/ + label.required::after { content: " *"; color: #d33; @@ -83,15 +81,19 @@ font-size: 1.1em; vertical-align: super; font-family: sans-serif; -} +} + .required-note { color: #d33; font-size: 0.9rem; margin-top: 0.5rem; font-family: sans-serif; } + @media (max-width: 600px) { - form, .form-group { + + form, + .form-group { flex-direction: column; align-items: stretch; } @@ -102,7 +104,89 @@ text-align: left; } - input, textarea { + input, + textarea { width: 100%; } } + +.h-captcha { + margin: 1rem 0; + display: flex; + justify-content: center; +} + +.honeypot { + position: absolute !important; + left: -9999px !important; + opacity: 0 !important; + pointer-events: none !important; +} + +/* +.char-counter { + text-align: right; + font-size: 0.9em; + color: #666; + margin-top: 5px; +} */ + +.char-counter { + text-align: right; + font-size: 0.9em; + color: #666; + margin-top: 5px; + margin-right: 0.5rem; +} + +.security-notice { + margin-top: 20px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 5px; + border-left: 4px solid #007bff; +} + +.security-notice details { + cursor: pointer; +} + +.security-notice summary { + font-weight: bold; + color: #007bff; + outline: none; +} + +.security-content { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #dee2e6; +} + +.security-content ul { + margin: 10px 0; + padding-left: 20px; +} + +.security-content li { + margin: 5px 0; +} + +#submitBtn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.form-group.message-group { + position: relative; +} + +.form-group input:invalid, +.form-group textarea:invalid { + border-color: #dc3545; +} + +.form-group input:valid, +.form-group textarea:valid { + border-color: #28a745; +} diff --git a/public/js/forensicTracker.js b/public/js/forensicTracker.js new file mode 100644 index 0000000..3618bc6 --- /dev/null +++ b/public/js/forensicTracker.js @@ -0,0 +1,331 @@ +// Balanced forensic tracker - focused on threat detection +(function() { + 'use strict'; + + // Configuration + const config = { + enableBehaviorTracking: true, + enableFingerprinting: true, + enableNetworkDetection: true, + maxDataSize: 10000, // Limit data collection + debugMode: false + }; + + // Data collection object + const forensicData = { + timestamp: Date.now(), + sessionId: generateSessionId(), + pageLoadTime: Date.now(), + formStartTime: null, + formCompletionTime: null, + device: {}, + network: {}, + behavior: {}, + security: {} + }; + + // Generate session ID + function generateSessionId() { + return 'sess_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); + } + + // Basic device fingerprinting (less invasive) + function collectDeviceInfo() { + try { + forensicData.device = { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language, + languages: navigator.languages, + cookieEnabled: navigator.cookieEnabled, + onLine: navigator.onLine, + screen: { + width: screen.width, + height: screen.height, + colorDepth: screen.colorDepth, + pixelDepth: screen.pixelDepth + }, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + timezoneOffset: new Date().getTimezoneOffset(), + // Basic hardware info (less detailed than before) + hardwareConcurrency: navigator.hardwareConcurrency || 0, + memory: navigator.deviceMemory || 0, + // Remove canvas fingerprinting - too invasive + // Keep simple WebGL detection + webgl: !!window.WebGLRenderingContext, + webgl2: !!window.WebGL2RenderingContext + }; + } catch (e) { + console.warn('Device info collection failed:', e); + } + } + + // Network detection (simplified) + function detectNetworkInfo() { + try { + // Basic connection info + if (navigator.connection) { + forensicData.network.connection = { + effectiveType: navigator.connection.effectiveType, + type: navigator.connection.type, + downlink: navigator.connection.downlink, + rtt: navigator.connection.rtt, + saveData: navigator.connection.saveData + }; + } + + // Simplified WebRTC detection (just check for VPN/proxy indicators) + if (config.enableNetworkDetection && window.RTCPeerConnection) { + detectWebRTCInfo(); + } + } catch (e) { + console.warn('Network detection failed:', e); + } + } + + // Simplified WebRTC detection + function detectWebRTCInfo() { + try { + const pc = new RTCPeerConnection({ + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + }); + + pc.createDataChannel('test'); + pc.createOffer().then(offer => pc.setLocalDescription(offer)); + + const timeout = setTimeout(() => { + pc.close(); + forensicData.network.webrtc = { status: 'timeout' }; + }, 3000); + + pc.onicecandidate = function(event) { + if (event.candidate) { + const candidate = event.candidate.candidate; + if (candidate.includes('192.168.') || candidate.includes('10.') || candidate.includes('172.')) { + forensicData.network.webrtc = { + hasLocalIP: true, + candidate: candidate.substring(0, 50) // Limit data + }; + } + clearTimeout(timeout); + pc.close(); + } + }; + } catch (e) { + forensicData.network.webrtc = { error: 'WebRTC unavailable' }; + } + } + + // Behavior tracking (focused on form interaction) + function trackBehavior() { + let focusCount = 0; + let keystrokes = 0; + let mouseClicks = 0; + let formInteractions = []; + + const form = document.getElementById('contactForm'); + if (!form) return; + + // Track form focus events + form.addEventListener('focusin', function(e) { + focusCount++; + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + formInteractions.push({ + type: 'focus', + field: e.target.name || e.target.id, + timestamp: Date.now() - forensicData.pageLoadTime + }); + } + }); + + // Track form submission timing + form.addEventListener('submit', function() { + forensicData.formCompletionTime = Date.now(); + forensicData.behavior.formTime = forensicData.formCompletionTime - (forensicData.formStartTime || forensicData.pageLoadTime); + }); + + // Track first form interaction + form.addEventListener('input', function() { + if (!forensicData.formStartTime) { + forensicData.formStartTime = Date.now(); + } + keystrokes++; + }); + + // Track clicks (limited) + document.addEventListener('click', function() { + mouseClicks++; + }); + + // Store behavior data + forensicData.behavior = { + focusCount: focusCount, + keystrokes: keystrokes, + mouseClicks: mouseClicks, + formInteractions: formInteractions.slice(-10), // Limit to last 10 + pageTime: 0 // Will be calculated on submit + }; + } + + // Security checks + function performSecurityChecks() { + forensicData.security = { + // Check for automation indicators + webdriver: !!navigator.webdriver, + selenium: !!window.selenium, + phantom: !!window.phantom, + nightmare: !!window.nightmare, + + // Check for debugging + devtools: false, // Will be set by devtools detection + + // Check for common bot indicators + plugins: navigator.plugins.length, + mimeTypes: navigator.mimeTypes.length, + + // Check for headless indicators + headless: isHeadless() + }; + + // Simple devtools detection + detectDevTools(); + } + + // Detect headless browsers + function isHeadless() { + return ( + navigator.webdriver || + !navigator.languages || + navigator.languages.length === 0 || + /HeadlessChrome/.test(navigator.userAgent) || + (!navigator.permissions && !navigator.serviceWorker && !navigator.clipboard) + ); + } + + // Simple devtools detection + function detectDevTools() { + let devtools = false; + const element = new Image(); + Object.defineProperty(element, 'id', { + get: function() { + devtools = true; + return 'devtools-detected'; + } + }); + + setTimeout(() => { + console.log(element); + forensicData.security.devtools = devtools; + }, 100); + } + + // Main collection function + function collectForensicData() { + collectDeviceInfo(); + detectNetworkInfo(); + + if (config.enableBehaviorTracking) { + trackBehavior(); + } + + performSecurityChecks(); + } + + // Inject data into form submission + function injectForensicData() { + const form = document.getElementById('contactForm'); + if (!form) return; + + form.addEventListener('submit', function(e) { + // Calculate final timing + forensicData.behavior.pageTime = Date.now() - forensicData.pageLoadTime; + + // Limit data size + const dataString = JSON.stringify(forensicData); + if (dataString.length > config.maxDataSize) { + console.warn('Forensic data too large, truncating'); + // Keep only essential data + const essentialData = { + timestamp: forensicData.timestamp, + sessionId: forensicData.sessionId, + device: { + userAgent: forensicData.device.userAgent, + platform: forensicData.device.platform, + language: forensicData.device.language + }, + behavior: { + formTime: forensicData.behavior.formTime, + pageTime: forensicData.behavior.pageTime + }, + security: forensicData.security + }; + + // Create or update hidden field + let hiddenField = document.getElementById('forensicData'); + if (!hiddenField) { + hiddenField = document.createElement('input'); + hiddenField.type = 'hidden'; + hiddenField.name = 'clientData'; + hiddenField.id = 'forensicData'; + form.appendChild(hiddenField); + } + hiddenField.value = JSON.stringify(essentialData); + } else { + // Create or update hidden field + let hiddenField = document.getElementById('forensicData'); + if (!hiddenField) { + hiddenField = document.createElement('input'); + hiddenField.type = 'hidden'; + hiddenField.name = 'clientData'; + hiddenField.id = 'forensicData'; + form.appendChild(hiddenField); + } + hiddenField.value = dataString; + } + }); + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + collectForensicData(); + injectForensicData(); + }); + } else { + collectForensicData(); + injectForensicData(); + } + + // Debug mode + if (config.debugMode) { + window.forensicData = forensicData; + console.log('Forensic tracker initialized', forensicData); + } + +})(); +// Character counter +document.addEventListener('DOMContentLoaded', function() { + const messageField = document.getElementById('message'); + const charCount = document.getElementById('char-count'); + const submitBtn = document.getElementById('submitBtn'); + + messageField.addEventListener('input', function() { + const count = this.value.length; + charCount.textContent = count; + + // Visual feedback for character limit + if (count > 1800) { + charCount.style.color = '#e74c3c'; + } else if (count > 1500) { + charCount.style.color = '#f39c12'; + } else { + charCount.style.color = '#27ae60'; + } + }); + + // Form submission feedback + document.getElementById('contactForm').addEventListener('submit', function() { + submitBtn.textContent = 'Sending...'; + submitBtn.disabled = true; + }); +}); diff --git a/src/middleware/index.js b/src/middleware/index.js index 4d4b310..3ebfe92 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -34,7 +34,7 @@ helpers: { section: function (name, options) { this._sections ??= {}; - this._sections[name] = options.fn(this); + this._sections[name] = (this._sections[name] || '') + options.fn(this); return null; }, }, diff --git a/src/routes/contact.js b/src/routes/contact.js index 00519d1..de6d90e 100644 --- a/src/routes/contact.js +++ b/src/routes/contact.js @@ -1,3 +1,44 @@ +// // src/routes/contact.js +// const express = require("express"); +// const router = express.Router(); +// const sendContactMail = require("../utils/sendContactMail"); +// const getBaseContext = require("../utils/baseContext"); +// const formLimiter = require("../utils/formLimiter"); +// const verifyHCaptcha = require("../utils/verifyHCaptcha"); + +// router.post("/contact", formLimiter, async (req, res, next) => { +// try { +// const { name, email, message, hcaptchaToken } = req.body; +// if (!hcaptchaToken) { +// return res.status(400).send("Captcha token missing"); +// } +// const valid = await verifyHCaptcha(hcaptchaToken); +// if (!valid) { +// return res.status(400).send("Captcha verification failed"); +// } +// await sendContactMail({ name, email, message }); +// res.redirect("/contact/thankyou"); +// } catch (err) { +// next(err); +// } +// }); + +// router.get("/contact", async (req, res) => { +// const context = await getBaseContext({ +// csrfToken: res.locals.csrfToken, +// title: "Contact", +// }); +// res.render("pages/contact.handlebars", context); +// }); + +// router.get("/contact/thankyou", async (req, res) => { +// const context = await getBaseContext({ +// title: "Thank You", +// }); +// res.render("pages/thankyou.handlebars", context); +// }); + +// module.exports = router; // src/routes/contact.js const express = require("express"); const router = express.Router(); @@ -5,25 +46,304 @@ const getBaseContext = require("../utils/baseContext"); const formLimiter = require("../utils/formLimiter"); const verifyHCaptcha = require("../utils/verifyHCaptcha"); +const crypto = require("crypto"); +const fs = require("fs").promises; +const path = require("path"); + +// Threat detection patterns +const THREAT_PATTERNS = { + // Common phishing/spam indicators + suspiciousKeywords: [ + 'verify account', 'urgent action', 'suspended account', 'click here', + 'limited time', 'act now', 'confirm identity', 'update payment', + 'security alert', 'unusual activity' + ], + + // Suspicious domains (add known bad actor domains) + suspiciousDomains: [ + 'tempmail.org', '10minutemail.com', 'guerrillamail.com', + 'throwaway.email', 'temp-mail.org' + ], + + // Suspicious patterns + suspiciousPatterns: [ + /https?:\/\/[^\s]+/gi, // URLs in messages + /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, // Email addresses + /\b(?:\d{4}[-\s]?){3}\d{4}\b/g, // Credit card patterns + /\b\d{3}-\d{2}-\d{4}\b/g // SSN patterns + ] +}; + +// Enhanced forensic data collection (focused on threat detection) +function captureSecurityData(req, additionalData = {}) { + const timestamp = new Date().toISOString(); + const requestId = crypto.randomUUID(); + + // Connection and network data + const connectionData = { + ip: req.ip, + ips: req.ips || [], + remoteAddress: req.socket?.remoteAddress, + protocol: req.protocol, + secure: req.secure, + hostname: req.hostname, + originalUrl: req.originalUrl, + encrypted: req.socket?.encrypted || false + }; + + // Security-relevant headers + const securityHeaders = { + userAgent: req.headers['user-agent'], + acceptLanguage: req.headers['accept-language'], + referer: req.headers['referer'], + origin: req.headers['origin'], + xForwardedFor: req.headers['x-forwarded-for'], + xRealIp: req.headers['x-real-ip'], + host: req.headers['host'], + // Check for proxy/VPN indicators + via: req.headers['via'], + xForwardedProto: req.headers['x-forwarded-proto'], + cfConnectingIp: req.headers['cf-connecting-ip'], // Cloudflare + cfIpCountry: req.headers['cf-ipcountry'], + cfRay: req.headers['cf-ray'] + }; + + // Request timing and patterns + const requestData = { + method: req.method, + url: req.url, + path: req.path, + query: req.query, + timestamp: timestamp, + requestStart: req._startTime || Date.now(), + processingTime: Date.now() - (req._startTime || Date.now()) + }; + + // TLS/Security info + let tlsData = null; + if (req.socket && req.socket.encrypted) { + try { + const cipher = req.socket.getCipher ? req.socket.getCipher() : null; + tlsData = { + cipher: cipher, + tlsVersion: req.socket.getProtocol ? req.socket.getProtocol() : null, + authorized: req.socket.authorized + }; + } catch (err) { + tlsData = { error: 'TLS data unavailable' }; + } + } + + return { + requestId, + timestamp, + connection: connectionData, + security: securityHeaders, + request: requestData, + tls: tlsData, + additional: additionalData + }; +} + +// Threat analysis function +function analyzeThreatLevel(formData, securityData) { + let threatScore = 0; + const indicators = []; + + // Check message content for suspicious patterns + const message = formData.message?.toLowerCase() || ''; + const email = formData.email?.toLowerCase() || ''; + const name = formData.name?.toLowerCase() || ''; + + // Suspicious keywords in message + THREAT_PATTERNS.suspiciousKeywords.forEach(keyword => { + if (message.includes(keyword.toLowerCase())) { + threatScore += 3; + indicators.push(`suspicious_keyword: ${keyword}`); + } + }); + + // Check for suspicious email domains + const emailDomain = email.split('@')[1]; + if (emailDomain && THREAT_PATTERNS.suspiciousDomains.includes(emailDomain)) { + threatScore += 5; + indicators.push(`suspicious_email_domain: ${emailDomain}`); + } + + // Check for suspicious patterns in content + THREAT_PATTERNS.suspiciousPatterns.forEach((pattern, index) => { + if (pattern.test(message)) { + threatScore += 2; + indicators.push(`suspicious_pattern_${index}`); + } + }); + + // Check for rapid form submission (potential automation) + if (securityData.additional.clientData?.formTime < 5000) { // Less than 5 seconds + threatScore += 2; + indicators.push('rapid_submission'); + } + + // Check for suspicious user agent + const userAgent = securityData.security.userAgent || ''; + if (!userAgent || userAgent.includes('bot') || userAgent.includes('crawl')) { + threatScore += 3; + indicators.push('suspicious_user_agent'); + } + + // Check for missing referer (direct access) + if (!securityData.security.referer) { + threatScore += 1; + indicators.push('no_referer'); + } + + // Determine threat level + let threatLevel = 'low'; + if (threatScore >= 8) threatLevel = 'high'; + else if (threatScore >= 4) threatLevel = 'medium'; + + return { + score: threatScore, + level: threatLevel, + indicators: indicators, + requiresReview: threatScore >= 4 + }; +} + +// Enhanced logging with threat analysis +async function logSecurityEvent(data, eventType = 'contact_submission') { + try { + const logDir = path.join(__dirname, '..', 'logs', 'security'); + await fs.mkdir(logDir, { recursive: true }); + + const logFile = path.join(logDir, `${eventType}_${new Date().toISOString().split('T')[0]}.log`); + const logEntry = { + ...data, + loggedAt: new Date().toISOString() + }; + + await fs.appendFile(logFile, JSON.stringify(logEntry) + '\n'); + + // Create separate high-threat log + if (data.threatAnalysis?.level === 'high') { + const alertFile = path.join(logDir, `high_threat_${new Date().toISOString().split('T')[0]}.log`); + await fs.appendFile(alertFile, JSON.stringify(logEntry) + '\n'); + } + + } catch (err) { + console.error('Failed to log security event:', err); + } +} + +// Middleware to capture request start time +router.use((req, res, next) => { + req._startTime = Date.now(); + next(); +}); router.post("/contact", formLimiter, async (req, res, next) => { try { - const { name, email, message, hcaptchaToken } = req.body; + const { name, email, message, subject, hcaptchaToken, clientData } = req.body; + + // Capture security data + const securityData = captureSecurityData(req, { + formData: { name, email, message, subject }, + captchaProvided: !!hcaptchaToken, + clientData: clientData, // From client-side + processingStep: 'initial_validation' + }); + + // Analyze threat level + const threatAnalysis = analyzeThreatLevel( + { name, email, message, subject }, + securityData + ); + + // Enhanced logging with threat analysis + await logSecurityEvent({ + ...securityData, + threatAnalysis: threatAnalysis, + formData: { name, email, hasMessage: !!message, hasSubject: !!subject } + }, 'contact_submission'); + + // CAPTCHA validation if (!hcaptchaToken) { + await logSecurityEvent({ + ...securityData, + threatAnalysis: threatAnalysis, + validationResult: 'failed', + failureReason: 'missing_captcha' + }, 'validation_failure'); + return res.status(400).send("Captcha token missing"); } + const valid = await verifyHCaptcha(hcaptchaToken); if (!valid) { + await logSecurityEvent({ + ...securityData, + threatAnalysis: threatAnalysis, + validationResult: 'failed', + failureReason: 'captcha_failed' + }, 'validation_failure'); + return res.status(400).send("Captcha verification failed"); } - await sendContactMail({ name, email, message }); + + // High threat handling + if (threatAnalysis.level === 'high') { + await logSecurityEvent({ + ...securityData, + threatAnalysis: threatAnalysis, + action: 'blocked_high_threat' + }, 'threat_blocked'); + + // Still redirect to thank you to not reveal detection + res.redirect("/contact/thankyou"); + return; + } + + // Send email (but flag for review if medium threat) + const emailData = { name, email, message, subject }; + if (threatAnalysis.level === 'medium') { + emailData.securityFlag = `[SECURITY REVIEW REQUIRED - Score: ${threatAnalysis.score}]`; + } + + await sendContactMail(emailData); + + // Log successful completion + await logSecurityEvent({ + ...securityData, + threatAnalysis: threatAnalysis, + processingResult: 'success', + emailSent: true + }, 'contact_success'); + res.redirect("/contact/thankyou"); + } catch (err) { + const errorData = captureSecurityData(req, { + error: { + message: err.message, + stack: err.stack, + name: err.name + }, + processingStep: 'error_handling' + }); + + await logSecurityEvent(errorData, 'contact_error'); next(err); } }); router.get("/contact", async (req, res) => { + const securityData = captureSecurityData(req, { + pageAccess: 'contact_form', + processingStep: 'page_render' + }); + + await logSecurityEvent(securityData, 'page_access'); + const context = await getBaseContext({ csrfToken: res.locals.csrfToken, title: "Contact", @@ -32,6 +352,13 @@ }); router.get("/contact/thankyou", async (req, res) => { + const securityData = captureSecurityData(req, { + pageAccess: 'thankyou_page', + processingStep: 'page_render' + }); + + await logSecurityEvent(securityData, 'thankyou_access'); + const context = await getBaseContext({ title: "Thank You", }); diff --git a/src/views/pages/contact.handlebars b/src/views/pages/contact.handlebars index 4b9dc47..69f3bbd 100644 --- a/src/views/pages/contact.handlebars +++ b/src/views/pages/contact.handlebars @@ -3,19 +3,22 @@ {{/section}} {{#section "scripts"}} + {{/section}}

{{title}}

-
+ + {{!-- Honeypot fiel for bot detection --}} +
- +
+ pattern="[a-zA-Z0-9-_\.]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*" autocomplete="email" />
@@ -25,9 +28,31 @@ +
+ 0/2000 characters +
-

* Required field

+ +

* Required field

+ +
+
+ Security & Privacy Information +
+

Security Notice: This contact form is protected by security measures including:

+
    +
  • CAPTCHA verification to prevent automated submissions
  • +
  • Rate limiting to prevent spam
  • +
  • Basic technical data collection (IP address, browser type, submission timing)
  • +
  • Content analysis for security purposes
  • +
+

We collect minimal necessary data to ensure form security and prevent abuse. Your personal information is + only used to respond to your message.

+

If you experience any issues with this form, please try refreshing the page.

+
+
+