Skip to content

Commit 776ef29

Browse files
committed
implemented a comprehensive security solution to fix the critical input validation issues
1 parent 091997f commit 776ef29

7 files changed

Lines changed: 272 additions & 26 deletions

File tree

app/api/register/route.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,65 @@ import prisma from "@/utils/db";
22
import bcrypt from "bcryptjs";
33
import { nanoid } from "nanoid";
44
import { NextResponse } from "next/server";
5+
import { registrationSchema } from "@/utils/schema";
6+
import { sanitizeInput, commonValidations } from "@/utils/validation";
7+
import { handleApiError, AppError } from "@/utils/errorHandler";
58

6-
export const POST = async (request: any) => {
7-
const { email, password } = await request.json();
9+
export const POST = async (request: Request) => {
10+
try {
11+
// Get client IP for rate limiting
12+
const clientIP = request.headers.get("x-forwarded-for") ||
13+
request.headers.get("x-real-ip") ||
14+
"unknown";
815

9-
const existingUser = await prisma.user.findFirst({ where: { email } });
16+
// Check rate limit
17+
if (!commonValidations.checkRateLimit(clientIP, 5, 15 * 60 * 1000)) {
18+
throw new AppError("Too many registration attempts. Please try again later.", 429);
19+
}
1020

11-
if (existingUser) {
12-
return new NextResponse("Email is already in use", { status: 400 });
13-
}
21+
const body = await sanitizeInput.validateJsonInput(request);
1422

15-
const hashedPassword = await bcrypt.hash(password, 14);
23+
const validationResult = registrationSchema.safeParse(body);
24+
25+
if (!validationResult.success) {
26+
throw validationResult.error;
27+
}
1628

17-
try {
18-
await prisma.user.create({
29+
const { email, password } = validationResult.data;
30+
31+
const existingUser = await prisma.user.findFirst({
32+
where: { email }
33+
});
34+
35+
if (existingUser) {
36+
throw new AppError("Email is already in use", 400);
37+
}
38+
39+
const hashedPassword = await bcrypt.hash(password, 14);
40+
41+
// Create user with proper error handling
42+
const newUser = await prisma.user.create({
1943
data: {
20-
id: nanoid() + "",
44+
id: nanoid(),
2145
email,
2246
password: hashedPassword,
47+
role: "user",
2348
},
2449
});
25-
return new NextResponse("user is registered", { status: 200 });
26-
} catch (err: any) {
27-
return new NextResponse(err, {
28-
status: 500,
29-
});
50+
51+
// Return success response without sensitive data
52+
return new NextResponse(
53+
JSON.stringify({
54+
message: "User registered successfully",
55+
userId: newUser.id
56+
}),
57+
{
58+
status: 200,
59+
headers: { "Content-Type": "application/json" }
60+
}
61+
);
62+
63+
} catch (error) {
64+
return handleApiError(error);
3065
}
3166
};

app/product/[productSlug]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ const SingleProductPage = async ({ params }: SingleProductPageProps) => {
4949
className="w-auto h-auto"
5050
/>
5151
<div className="flex justify-around mt-5 flex-wrap gap-y-1 max-[500px]:justify-center max-[500px]:gap-x-1">
52-
{images?.map((imageItem: ImageItem) => (
52+
{images?.map((imageItem: ImageItem, key: number) => (
5353
<Image
54-
key={imageItem.imageID}
54+
key={imageItem.imageID + key}
5555
src={`/${imageItem.image}`}
5656
width={100}
5757
height={100}

app/register/page.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const RegisterPage = () => {
2222
const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
2323
return emailRegex.test(email);
2424
};
25+
2526
const handleSubmit = async (e: any) => {
2627
e.preventDefault();
2728
const email = e.target[2].value;
@@ -59,14 +60,27 @@ const RegisterPage = () => {
5960
}),
6061
});
6162

62-
if (res.status === 400) {
63-
toast.error("This email is already registered");
64-
setError("The email already in use");
65-
}
66-
if (res.status === 200) {
63+
const data = await res.json();
64+
65+
if (res.ok) {
6766
setError("");
6867
toast.success("Registration successful");
6968
router.push("/login");
69+
} else {
70+
// Handle different types of errors
71+
if (data.details && Array.isArray(data.details)) {
72+
// Validation errors
73+
const errorMessage = data.details.map((err: any) => err.message).join(", ");
74+
setError(errorMessage);
75+
toast.error(errorMessage);
76+
} else if (data.error) {
77+
// General errors
78+
setError(data.error);
79+
toast.error(data.error);
80+
} else {
81+
setError("Registration failed");
82+
toast.error("Registration failed");
83+
}
7084
}
7185
} catch (error) {
7286
toast.error("Error, try again");

components/SingleProductRating.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ const SingleProductRating = ({ rating }: { rating: number }) => {
2727
return (
2828
<div className="flex text-2xl items-center max-[500px]:justify-center">
2929
{ratingArray &&
30-
ratingArray.map((singleRating) => {
30+
ratingArray.map((singleRating, key: number) => {
3131
return (
32-
<>
32+
<div key={key+"rating"}>
3333
{singleRating === "full star" ? (
3434
<AiFillStar className="text-custom-yellow" />
3535
) : (
3636
<AiOutlineStar className="text-custom-yellow" />
3737
)}
38-
</>
38+
</div>
3939
);
4040
})}
4141
<span className="text-xl ml-1">(3 reviews)</span>

utils/errorHandler.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { NextResponse } from "next/server";
2+
import { ZodError } from "zod";
3+
4+
export class AppError extends Error {
5+
public statusCode: number;
6+
public isOperational: boolean;
7+
8+
constructor(message: string, statusCode: number = 500, isOperational: boolean = true) {
9+
super(message);
10+
this.statusCode = statusCode;
11+
this.isOperational = isOperational;
12+
13+
Error.captureStackTrace(this, this.constructor);
14+
}
15+
}
16+
17+
export const handleApiError = (error: unknown) => {
18+
// Zod validation errors
19+
if (error instanceof ZodError) {
20+
const errors = error.errors.map(err => ({
21+
field: err.path.join('.'),
22+
message: err.message
23+
}));
24+
25+
return new NextResponse(
26+
JSON.stringify({
27+
error: "Validation failed",
28+
details: errors
29+
}),
30+
{
31+
status: 400,
32+
headers: { "Content-Type": "application/json" }
33+
}
34+
);
35+
}
36+
37+
// Custom application errors
38+
if (error instanceof AppError) {
39+
return new NextResponse(
40+
JSON.stringify({ error: error.message }),
41+
{
42+
status: error.statusCode,
43+
headers: { "Content-Type": "application/json" }
44+
}
45+
);
46+
}
47+
48+
// Prisma errors
49+
if (error && typeof error === 'object' && 'code' in error) {
50+
const prismaError = error as any;
51+
52+
switch (prismaError.code) {
53+
case 'P2002':
54+
return new NextResponse(
55+
JSON.stringify({ error: "A record with this information already exists" }),
56+
{
57+
status: 409,
58+
headers: { "Content-Type": "application/json" }
59+
}
60+
);
61+
case 'P2025':
62+
return new NextResponse(
63+
JSON.stringify({ error: "Record not found" }),
64+
{
65+
status: 404,
66+
headers: { "Content-Type": "application/json" }
67+
}
68+
);
69+
default:
70+
break;
71+
}
72+
}
73+
74+
// Generic server error
75+
console.error("Unhandled error:", error);
76+
return new NextResponse(
77+
JSON.stringify({
78+
error: "Internal server error. Please try again later."
79+
}),
80+
{
81+
status: 500,
82+
headers: { "Content-Type": "application/json" }
83+
}
84+
);
85+
};

utils/schema.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { z } from "zod";
2+
import { commonValidations } from "./validation";
23

4+
// Registration schema with comprehensive validation
5+
export const registrationSchema = z.object({
6+
email: commonValidations.email,
7+
password: commonValidations.password,
8+
});
9+
10+
// Login schema (for future use)
11+
export const loginSchema = z.object({
12+
email: commonValidations.email,
13+
password: z.string().min(1, "Password is required"),
14+
});
15+
16+
// Generic validation schema (keeping existing for backward compatibility)
317
const schema = z.object({
418
name: z.string().min(3),
519
email: z.string().email()
620
});
721

8-
922
export default schema;

utils/validation.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { z } from "zod";
2+
3+
// Common validation patterns
4+
export const commonValidations = {
5+
// Email validation with comprehensive checks
6+
email: z
7+
.string()
8+
.min(1, "Email is required")
9+
.max(254, "Email must be no more than 254 characters")
10+
.email("Please provide a valid email address")
11+
.toLowerCase()
12+
.trim()
13+
.refine(
14+
(email) => {
15+
16+
const suspiciousPatterns = [
17+
/<script/i,
18+
/javascript:/i,
19+
/on\w+\s*=/i,
20+
/data:/i,
21+
];
22+
return !suspiciousPatterns.some(pattern => pattern.test(email));
23+
},
24+
"Email contains invalid characters"
25+
),
26+
27+
// Strong password validation
28+
password: z
29+
.string()
30+
.min(8, "Password must be at least 8 characters long")
31+
.max(128, "Password must be no more than 128 characters")
32+
.regex(
33+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
34+
"Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
35+
)
36+
.refine(
37+
(password) => {
38+
// Check for common weak passwords
39+
const commonPasswords = [
40+
"password", "123456", "qwerty", "abc123", "password123",
41+
"admin", "letmein", "welcome", "monkey", "dragon"
42+
];
43+
return !commonPasswords.includes(password.toLowerCase());
44+
},
45+
"Password is too common, please choose a stronger password"
46+
),
47+
48+
// Request size validation
49+
validateRequestSize: (contentLength: number | null) => {
50+
const MAX_REQUEST_SIZE = 1024 * 1024; // 1MB limit
51+
if (contentLength && contentLength > MAX_REQUEST_SIZE) {
52+
throw new Error("Request payload too large");
53+
}
54+
},
55+
56+
// Rate limiting helper (basic implementation)
57+
rateLimit: new Map<string, { count: number; resetTime: number }>(),
58+
59+
checkRateLimit: (identifier: string, maxRequests: number = 5, windowMs: number = 15 * 60 * 1000) => {
60+
const now = Date.now();
61+
const userLimit = commonValidations.rateLimit.get(identifier);
62+
63+
if (!userLimit || now > userLimit.resetTime) {
64+
commonValidations.rateLimit.set(identifier, { count: 1, resetTime: now + windowMs });
65+
return true;
66+
}
67+
68+
if (userLimit.count >= maxRequests) {
69+
return false;
70+
}
71+
72+
userLimit.count++;
73+
return true;
74+
}
75+
};
76+
77+
// Sanitization helpers
78+
export const sanitizeInput = {
79+
// Remove potentially dangerous characters
80+
sanitizeString: (input: string): string => {
81+
return input
82+
.replace(/[<>]/g, '') // Remove < and >
83+
.replace(/javascript:/gi, '') // Remove javascript: protocol
84+
.replace(/on\w+\s*=/gi, '') // Remove event handlers
85+
.trim();
86+
},
87+
88+
// Validate and sanitize JSON input
89+
validateJsonInput: async (request: Request) => {
90+
const contentLength = request.headers.get("content-length");
91+
commonValidations.validateRequestSize(contentLength ? parseInt(contentLength) : null);
92+
93+
try {
94+
return await request.json();
95+
} catch (error) {
96+
throw new Error("Invalid JSON format");
97+
}
98+
}
99+
};

0 commit comments

Comments
 (0)