From 077b3d0b440880cfed3b4cd9fe256e20304f011e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:08:00 +0000 Subject: [PATCH 1/3] Initial plan From 1aa8679c03d5b14ec0a78683edfde3ac51be5a90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:16:10 +0000 Subject: [PATCH 2/3] Fix security vulnerabilities: HTML injection, input validation, error leaking, rate limiting - Add HTML escaping (escapeHtml) to sanitize user input in email templates in both server/index.js and netlify/functions/contact.js to prevent HTML injection/XSS attacks - Add input validation (required fields check) to server/index.js - Add server-side email format validation to both server files - Fix internal error message leaking to client in server/index.js (now returns generic error message) - Add request body size limit (1kb) to server/index.js - Add rate limiting to server/index.js (matching netlify function) Co-authored-by: kulharshit21 <124128807+kulharshit21@users.noreply.github.com> --- netlify/functions/contact.js | 40 +++++++++++++++++---- server/index.js | 69 +++++++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/netlify/functions/contact.js b/netlify/functions/contact.js index 9e6de2f..31d3865 100644 --- a/netlify/functions/contact.js +++ b/netlify/functions/contact.js @@ -1,5 +1,15 @@ import nodemailer from 'nodemailer'; +// HTML-escape user input to prevent HTML injection in emails +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + // Simple in-memory rate limiting (resets on function cold start) const rateLimitMap = new Map(); const RATE_LIMIT_WINDOW = 60000; // 1 minute @@ -53,6 +63,22 @@ export async function handler(event) { }; } + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return { + statusCode: 400, + headers, + body: JSON.stringify({ ok: false, error: 'Invalid email address' }), + }; + } + + // Sanitize user inputs + const safeName = escapeHtml(name); + const safeEmail = escapeHtml(email); + const safeSubject = escapeHtml(subject); + const safeMessage = escapeHtml(message).replace(/\n/g, '
'); + const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: +process.env.SMTP_PORT, @@ -68,13 +94,13 @@ export async function handler(event) { from: `Portfolio Contact <${process.env.SMTP_USER}>`, to: process.env.RECIPIENT, replyTo: email, - subject: `[Portfolio] ${subject} (from ${name})`, + subject: `[Portfolio] ${safeSubject} (from ${safeName})`, html: `

New message from your portfolio

-

Name: ${name}

-

Email: ${email}

-

Subject: ${subject}

-

Message:
${message.replace(/\n/g, '
')}

+

Name: ${safeName}

+

Email: ${safeEmail}

+

Subject: ${safeSubject}

+

Message:
${safeMessage}

`, }); @@ -87,13 +113,13 @@ export async function handler(event) {

Message Received

-

Dear ${name},

+

Dear ${safeName},

Thank you for taking the time to reach out through my portfolio website. I have successfully received your message and truly appreciate your interest.

Your Message Details:

-

Subject: ${subject}

+

Subject: ${safeSubject}

I make it a priority to respond to all inquiries promptly and will get back to you as soon as possible, typically within 24-48 hours.

diff --git a/server/index.js b/server/index.js index ca67713..74a636f 100644 --- a/server/index.js +++ b/server/index.js @@ -10,7 +10,21 @@ dotenv.config({ path: join(__dirname, '.env') }); const app = express(); app.use(cors()); -app.use(express.json()); +app.use(express.json({ limit: '1kb' })); + +// HTML-escape user input to prevent HTML injection in emails +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// Simple in-memory rate limiting +const rateLimitMap = new Map(); +const RATE_LIMIT_WINDOW = 60000; // 1 minute const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -23,17 +37,56 @@ const transporter = nodemailer.createTransport({ }); app.post('/api/contact', async (req, res) => { + // Rate limiting + const clientIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown'; + const now = Date.now(); + const lastSubmission = rateLimitMap.get(clientIP); + + if (lastSubmission && (now - lastSubmission) < RATE_LIMIT_WINDOW) { + return res.status(429).json({ + ok: false, + error: 'Too many requests. Please wait a minute before submitting again.' + }); + } + + rateLimitMap.set(clientIP, now); + + // Clean up old entries + for (const [ip, timestamp] of rateLimitMap.entries()) { + if (now - timestamp > 300000) { + rateLimitMap.delete(ip); + } + } + const { name, email, subject, message } = req.body; + + // Input validation + if (!name || !email || !subject || !message) { + return res.status(400).json({ ok: false, error: 'All fields are required' }); + } + + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ ok: false, error: 'Invalid email address' }); + } + + // Sanitize user inputs + const safeName = escapeHtml(name); + const safeEmail = escapeHtml(email); + const safeSubject = escapeHtml(subject); + const safeMessage = escapeHtml(message).replace(/\n/g, '
'); + try { // Send notification email to yourself await transporter.sendMail({ from: `Portfolio Contact <${process.env.SMTP_USER}>`, to: process.env.RECIPIENT, - subject: `[Portfolio] ${subject} (from ${name})`, + subject: `[Portfolio] ${safeSubject} (from ${safeName})`, html: ` -

Name: ${name}

-

Email: ${email}

-

Message:
${message.replace(/\n/g, '
')}

+

Name: ${safeName}

+

Email: ${safeEmail}

+

Message:
${safeMessage}

` }); @@ -46,13 +99,13 @@ app.post('/api/contact', async (req, res) => {

Message Received

-

Dear ${name},

+

Dear ${safeName},

Thank you for taking the time to reach out through my portfolio website. I have successfully received your message and truly appreciate your interest.

Your Message Details:

-

Subject: ${subject}

+

Subject: ${safeSubject}

I make it a priority to respond to all inquiries promptly and will get back to you as soon as possible, typically within 24-48 hours.

@@ -82,7 +135,7 @@ app.post('/api/contact', async (req, res) => { res.json({ ok: true }); } catch (error) { console.error(error); - res.status(500).json({ ok: false, error: error.message }); + res.status(500).json({ ok: false, error: 'Failed to send email. Please try again later.' }); } }); From 1b0b500f31afef25d027f734e7f3d94ae2fbfa02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:17:06 +0000 Subject: [PATCH 3/3] Address code review: improve email regex, add named cleanup constant Co-authored-by: kulharshit21 <124128807+kulharshit21@users.noreply.github.com> --- netlify/functions/contact.js | 2 +- server/index.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/netlify/functions/contact.js b/netlify/functions/contact.js index 31d3865..bdb2e2e 100644 --- a/netlify/functions/contact.js +++ b/netlify/functions/contact.js @@ -64,7 +64,7 @@ export async function handler(event) { } // Email format validation - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const emailRegex = /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(email)) { return { statusCode: 400, diff --git a/server/index.js b/server/index.js index 74a636f..a4f0b1a 100644 --- a/server/index.js +++ b/server/index.js @@ -25,6 +25,7 @@ function escapeHtml(str) { // Simple in-memory rate limiting const rateLimitMap = new Map(); const RATE_LIMIT_WINDOW = 60000; // 1 minute +const CLEANUP_THRESHOLD = 5 * RATE_LIMIT_WINDOW; // 5 minutes const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -53,7 +54,7 @@ app.post('/api/contact', async (req, res) => { // Clean up old entries for (const [ip, timestamp] of rateLimitMap.entries()) { - if (now - timestamp > 300000) { + if (now - timestamp > CLEANUP_THRESHOLD) { rateLimitMap.delete(ip); } } @@ -66,7 +67,7 @@ app.post('/api/contact', async (req, res) => { } // Email format validation - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const emailRegex = /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(email)) { return res.status(400).json({ ok: false, error: 'Invalid email address' }); }