diff --git a/src/routes/contact/+page.server.ts b/src/routes/contact/+page.server.ts index d15d9ab..f974722 100644 --- a/src/routes/contact/+page.server.ts +++ b/src/routes/contact/+page.server.ts @@ -12,12 +12,16 @@ const spamPatterns = [ /(?:viagra|cialis|casino|poker|loan|mortgage|credit|debt|free money|make money|work from home)/i, /(?:click here|buy now|limited time|act now|urgent|asap)/i, /(?:[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}){2,}/i, // Multiple email addresses + /(?:http|https|www\.)/i, // URLs in message (common in spam) + /(?:discount|sale|offer|promo|coupon|deal|cheap|affordable)/i, // Sales spam ]; // Common spam keywords const spamKeywords = [ 'seo', 'backlink', 'guest post', 'increase traffic', 'boost sales', - 'crypto', 'bitcoin', 'investment opportunity', 'get rich quick' + 'crypto', 'bitcoin', 'investment opportunity', 'get rich quick', + 'weight loss', 'diet pill', 'miracle', 'guaranteed', 'risk free', + 'click here', 'visit our website', 'check out', 'limited offer' ]; // Rate limiting: track submissions per IP (in-memory, resets on server restart) @@ -60,9 +64,9 @@ function containsSpam(content: string): boolean { } } - // Check for excessive links (more than 3 links is suspicious) + // Check for excessive links (more than 2 links is suspicious) const linkCount = (content.match(/https?:\/\//gi) || []).length; - if (linkCount > 3) { + if (linkCount > 2) { return true; } @@ -77,6 +81,18 @@ function containsSpam(content: string): boolean { } } + // Check for multiple email addresses in message (common spam pattern) + const emailMatches = content.match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi) || []; + if (emailMatches.length > 1) { + // Multiple emails in one message is suspicious + return true; + } + + // Check for very short messages with links (common spam pattern) + if (content.length < 50 && linkCount > 0) { + return true; + } + return false; } @@ -91,36 +107,78 @@ export const actions: Actions = { const honeypot = (formData.get('website') ?? '').toString().trim(); const formLoadTimeStr = formData.get('form_load_time')?.toString(); const fields = { name, email, contact: message }; + const clientIP = getClientAddress(); + const userAgent = request.headers.get('user-agent') || 'unknown'; + + // Log all form submission attempts for debugging + console.log('Form submission attempt:', { + ip: clientIP, + email: email.substring(0, 20) + '...', // Partial email for privacy + hasHoneypot: !!honeypot, + formLoadTime: formLoadTimeStr || 'missing', + userAgent: userAgent.substring(0, 50) + }); // SPAM PROTECTION CHECKS // 1. Honeypot check - if filled, it's a bot + // Silently drop the submission to avoid teaching bots they hit a trap if (honeypot) { - console.warn('Spam detected: Honeypot field filled', { ip: getClientAddress() }); - return fail(400, { error: 'Invalid submission. Please try again.', fields }); + console.warn('🚫 Spam detected: Honeypot field filled', { + ip: clientIP, + honeypotValue: honeypot, + email, + userAgent + }); + // Return success response to avoid teaching bots + throw redirect(303, '/success'); } // 2. Rate limiting check - const clientIP = getClientAddress(); if (!checkRateLimit(clientIP)) { - console.warn('Spam detected: Rate limit exceeded', { ip: clientIP }); + console.warn('🚫 Spam detected: Rate limit exceeded', { + ip: clientIP, + email, + userAgent + }); return fail(429, { error: 'Too many submissions. Please try again later.', fields }); } // 3. Time-based validation - check if form was filled too quickly (< 3 seconds) - if (formLoadTimeStr) { - const formLoadTime = parseInt(formLoadTimeStr, 10); - const submitTime = Date.now(); - const timeSpent = submitTime - formLoadTime; - - // If form was submitted in less than 3 seconds, it's likely a bot - if (timeSpent < 3000) { - console.warn('Spam detected: Form submitted too quickly', { - ip: clientIP, - timeSpent: `${timeSpent}ms` - }); - return fail(400, { error: 'Please take your time filling out the form.', fields }); - } + // If timestamp is missing, treat as suspicious (might be direct POST without form) + if (!formLoadTimeStr || formLoadTimeStr === '0') { + console.warn('🚫 Spam detected: Missing or invalid form timestamp', { + ip: clientIP, + email, + formLoadTimeStr: formLoadTimeStr || 'missing', + userAgent + }); + return fail(400, { error: 'Invalid form submission. Please refresh the page and try again.', fields }); + } + + const formLoadTime = parseInt(formLoadTimeStr, 10); + if (isNaN(formLoadTime) || formLoadTime <= 0) { + console.warn('🚫 Spam detected: Invalid form timestamp format', { + ip: clientIP, + email, + formLoadTimeStr, + userAgent + }); + return fail(400, { error: 'Invalid form submission. Please refresh the page and try again.', fields }); + } + + const submitTime = Date.now(); + const timeSpent = submitTime - formLoadTime; + + // If form was submitted in less than 3 seconds, it's likely a bot + if (timeSpent < 3000) { + console.warn('🚫 Spam detected: Form submitted too quickly', { + ip: clientIP, + email, + timeSpent: `${timeSpent}ms`, + userAgent + }); + return fail(400, { error: 'Please take your time filling out the form.', fields }); } // 4. Basic validation @@ -135,7 +193,12 @@ export const actions: Actions = { // 5. Content spam detection const fullContent = `${name} ${email} ${message}`.toLowerCase(); if (containsSpam(fullContent)) { - console.warn('Spam detected: Content analysis flagged submission', { ip: clientIP }); + console.warn('🚫 Spam detected: Content analysis flagged submission', { + ip: clientIP, + email, + messagePreview: message.substring(0, 100), + userAgent + }); return fail(400, { error: 'Your message contains content that appears to be spam. Please revise and try again.', fields }); } @@ -193,6 +256,15 @@ export const actions: Actions = { console.error(`Brevo API error ${response.status}: ${errorText}`); return fail(502, { error: 'Sending failed. Please try again later.', fields }); } + + // Log successful submission + console.log('✅ Form submission successful:', { + ip: clientIP, + email: email.substring(0, 20) + '...', + timeSpent: `${timeSpent}ms`, + messageLength: message.length + }); + // Success - redirect to thank you page } catch (err) { console.error('Brevo API request failed', err); diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 6bf0bc2..6ab9cde 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -13,9 +13,11 @@ let formLoadTime = 0; // Track when form becomes visible for time-based spam detection - $: if (contactFormVisible && formLoadTime === 0) { - formLoadTime = Date.now(); - } else if (!contactFormVisible) { + $: if (contactFormVisible) { + if (formLoadTime === 0) { + formLoadTime = Date.now(); + } + } else { // Reset timestamp when form is closed formLoadTime = 0; } @@ -111,17 +113,31 @@ - +

Disclaimer: I will only use the data you submit here (name, email, message) to respond. I will not pass it on to any third party. If I don't hear from you I will delete the data and keep no records of it.

{#if form?.error} - + {/if}
@@ -185,4 +201,12 @@ color: var(--color-text); opacity: .8; } + /* Honeypot field - hidden from users and assistive tech */ + .hp-field { + position: absolute; + left: -9999px; + height: 0; + overflow: hidden; + visibility: hidden; + } \ No newline at end of file