Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions app/api/send-review/route.js
Original file line number Diff line number Diff line change
@@ -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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

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({
Expand All @@ -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: `
<h2>New Review Received</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Rating:</strong> ${'★'.repeat(rating)}${'☆'.repeat(5 - rating)}</p>
<p><strong>Name:</strong> ${escapeHtml(name)}</p>
<p><strong>Email:</strong> ${escapeHtml(email)}</p>
<p><strong>Rating:</strong> ${'★'.repeat(ratingNum)}${'☆'.repeat(5 - ratingNum)}</p>
<p><strong>Review:</strong></p>
<p>${review}</p>
<p>${escapeHtml(review)}</p>
`,
};

Expand Down
88 changes: 88 additions & 0 deletions security-tests/send-review-input.test.cjs
Original file line number Diff line number Diff line change
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

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>'),
'&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
);
});

it('escapes ampersand', () => {
assert.strictEqual(escapeHtml('Tom & Jerry'), 'Tom &amp; 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);
});
});
});