diff --git a/app/api/send-review/route.js b/app/api/send-review/route.js index 3efbc7b55..6a37b1ecc 100755 --- a/app/api/send-review/route.js +++ b/app/api/send-review/route.js @@ -1,9 +1,50 @@ import { NextResponse } from 'next/server'; import nodemailer from 'nodemailer'; +function escapeHtml(str) { + if (str == null) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function isValidEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + export async function POST(request) { const { name, email, review, rating, to } = await request.json(); + // Validate required fields + if (!name || typeof name !== 'string' || name.trim().length === 0) { + return NextResponse.json( + { success: false, error: 'Name is required' }, + { status: 400 } + ); + } + if (!email || !isValidEmail(email)) { + return NextResponse.json( + { success: false, error: 'A valid email address is required' }, + { status: 400 } + ); + } + if (!review || typeof review !== 'string' || review.trim().length === 0) { + return NextResponse.json( + { success: false, error: 'Review text is required' }, + { status: 400 } + ); + } + const ratingNum = Number(rating); + if (!Number.isInteger(ratingNum) || ratingNum < 1 || ratingNum > 5) { + return NextResponse.json( + { success: false, error: 'Rating must be an integer between 1 and 5' }, + { status: 400 } + ); + } + try { // Create transporter const transporter = nodemailer.createTransport({ @@ -18,14 +59,14 @@ export async function POST(request) { const mailOptions = { from: process.env.EMAIL_USER, to: to || 'routsohan2006@gmail.com', // Default to your email - subject: `New Review Submission from ${name}`, + subject: `New Review Submission from ${escapeHtml(name)}`, html: `

New Review Received

-

Name: ${name}

-

Email: ${email}

-

Rating: ${'★'.repeat(rating)}${'☆'.repeat(5 - rating)}

+

Name: ${escapeHtml(name)}

+

Email: ${escapeHtml(email)}

+

Rating: ${'★'.repeat(ratingNum)}${'☆'.repeat(5 - ratingNum)}

Review:

-

${review}

+

${escapeHtml(review)}

`, }; diff --git a/security-tests/send-review-input.test.cjs b/security-tests/send-review-input.test.cjs new file mode 100644 index 000000000..25c91a1b5 --- /dev/null +++ b/security-tests/send-review-input.test.cjs @@ -0,0 +1,88 @@ +'use strict'; + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +function escapeHtml(str) { + if (str == null) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function isValidEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +function validateRating(rating) { + const ratingNum = Number(rating); + return Number.isInteger(ratingNum) && ratingNum >= 1 && ratingNum <= 5; +} + +describe('send-review API input validation', () => { + describe('escapeHtml', () => { + it('escapes script tag XSS payload', () => { + assert.strictEqual( + escapeHtml(''), + '<script>alert("xss")</script>' + ); + }); + + it('escapes ampersand', () => { + assert.strictEqual(escapeHtml('Tom & Jerry'), 'Tom & Jerry'); + }); + + it('returns empty string for null', () => { + assert.strictEqual(escapeHtml(null), ''); + }); + }); + + describe('isValidEmail', () => { + it('accepts valid email', () => { + assert.strictEqual(isValidEmail('user@domain.com'), true); + }); + + it('rejects missing @', () => { + assert.strictEqual(isValidEmail('invalid-email'), false); + }); + + it('rejects empty string', () => { + assert.strictEqual(isValidEmail(''), false); + }); + }); + + describe('validateRating', () => { + it('accepts rating 1 through 5', () => { + for (let r = 1; r <= 5; r++) { + assert.strictEqual(validateRating(r), true, `rating ${r} should be valid`); + } + }); + + it('rejects rating below 1', () => { + assert.strictEqual(validateRating(0), false); + assert.strictEqual(validateRating(-1), false); + }); + + it('rejects rating above 5', () => { + assert.strictEqual(validateRating(6), false); + assert.strictEqual(validateRating(100), false); + }); + + it('rejects non-integer floats', () => { + assert.strictEqual(validateRating(3.5), false); + assert.strictEqual(validateRating(2.9), false); + }); + + it('rejects string values outside range', () => { + assert.strictEqual(validateRating('10'), false); + assert.strictEqual(validateRating('-1'), false); + }); + + it('rejects non-numeric strings', () => { + assert.strictEqual(validateRating('abc'), false); + }); + }); +});