15 KiB
15 KiB
Security Best Practices Guide
🔐 Authentication & Authorization
Current Issues
The application currently stores sensitive data in localStorage and has incomplete authentication checks.
Recommended Implementation
1. Secure Token Storage
❌ Current (Insecure):
// src/helper/accountContext/AccountProvider.js
localStorage.setItem("role", JSON.stringify(data?.role))
✅ Recommended:
// Use httpOnly cookies set by the server
// Client-side code should NOT directly access tokens
// Server-side API route (example)
export async function POST(request) {
const { email, password } = await request.json();
// Authenticate user
const user = await authenticateUser(email, password);
if (user) {
// Set httpOnly cookie
const response = NextResponse.json({ success: true, user });
response.cookies.set('auth_token', user.token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 1 week
path: '/'
});
return response;
}
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
2. Client-Side Permission Checks
❌ Current (Incomplete):
// src/layout/index.js
useEffect(() => {
const securePaths = mounted && ConvertPermissionArr(data1?.permissions);
if (mounted && !securePaths.find((item) => item?.name == replacePath(path?.split("/")[2]))) {
router.push(`/403`);
}
}, [data1]);
✅ Recommended:
// Create a custom hook for permission checks
// src/utils/hooks/usePermission.js
import { useContext, useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import AccountContext from '@/helper/accountContext';
export function usePermission(requiredPermission) {
const { accountData } = useContext(AccountContext);
const router = useRouter();
const pathname = usePathname();
const [isAuthorized, setIsAuthorized] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!accountData) {
setIsLoading(true);
return;
}
const userPermissions = accountData?.permissions || [];
const hasPermission = userPermissions.some(
(perm) => perm.name === requiredPermission
);
if (!hasPermission) {
const lang = pathname.split('/')[1] || 'en';
router.push(`/${lang}/403`);
setIsAuthorized(false);
} else {
setIsAuthorized(true);
}
setIsLoading(false);
}, [accountData, requiredPermission, router, pathname]);
return { isAuthorized, isLoading };
}
// Usage in components
function ProductPage() {
const { isAuthorized, isLoading } = usePermission('product.view');
if (isLoading) {
return <Loader />;
}
if (!isAuthorized) {
return null; // Will redirect via hook
}
return <ProductList />;
}
🛡️ XSS Prevention
HTML Sanitization
❌ Current (Vulnerable):
// src/utils/customFunctions/TextLimit.js
const sanitizeAndTrustHtml = (htmlString) => {
return { __html: htmlString }; // NO SANITIZATION!
};
✅ Recommended:
import DOMPurify from 'isomorphic-dompurify';
const sanitizeAndTrustHtml = (htmlString) => {
// Configure DOMPurify for your needs
const config = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target'],
ALLOW_DATA_ATTR: false
};
return { __html: DOMPurify.sanitize(htmlString, config) };
};
// Even better: avoid dangerouslySetInnerHTML when possible
const TextLimit = ({ value, maxLength }) => {
if (!value) return '';
let summarizedValue = value.substring(0, maxLength);
if (value.length > maxLength) {
summarizedValue += '...';
}
// If it's plain text, just render it
if (!containsHtmlTags(value)) {
return <div>{summarizedValue}</div>;
}
// Only use dangerouslySetInnerHTML for trusted, sanitized content
const sanitizedValue = sanitizeAndTrustHtml(summarizedValue);
return <div dangerouslySetInnerHTML={sanitizedValue} />;
};
User Input Validation
✅ Always validate and sanitize user input:
// src/utils/validation/inputSanitization.js
export function sanitizeInput(input) {
if (typeof input !== 'string') return input;
// Remove potentially dangerous characters
return input
.replace(/[<>]/g, '') // Remove < and >
.trim();
}
export function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
export function sanitizeFileName(filename) {
// Remove path traversal attempts
return filename
.replace(/\.\./g, '')
.replace(/[\/\\]/g, '')
.trim();
}
// Usage in forms
function handleSubmit(values) {
const sanitizedValues = {
name: sanitizeInput(values.name),
email: values.email, // Already validated by Yup
description: sanitizeInput(values.description)
};
// Submit sanitized values
await submitForm(sanitizedValues);
}
🔒 API Security
Request Validation
✅ Implement proper request validation:
// src/app/api/products/route.js
import { NextResponse } from 'next/server';
import { z } from 'zod';
// Define schema for validation
const productSchema = z.object({
name: z.string().min(1).max(255),
price: z.number().positive(),
description: z.string().max(5000).optional(),
category_id: z.number().int().positive()
});
export async function POST(request) {
try {
// Verify authentication
const token = request.cookies.get('auth_token')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Parse and validate request body
const body = await request.json();
const validatedData = productSchema.parse(body);
// Check permissions
const user = await verifyToken(token);
if (!user.permissions.includes('product.create')) {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
// Process request
const product = await createProduct(validatedData);
return NextResponse.json({ success: true, product });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
);
}
console.error('API Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Rate Limiting
✅ Implement rate limiting:
// src/middleware.js
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
export async function middleware(request) {
// Rate limit API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.ip ?? '127.0.0.1';
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString()
}
}
);
}
}
return NextResponse.next();
}
🔐 Environment Variables
Secure Configuration
❌ Current:
// next.config.js
env: {
API_PROD_URL: "https://fastkart-admin-json.vercel.app/api/",
}
✅ Recommended:
Create .env.local:
# Public variables (accessible in browser)
NEXT_PUBLIC_API_URL=http://localhost:3000/api
NEXT_PUBLIC_APP_NAME=FastKart Admin
# Private variables (server-side only)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-super-secret-key-change-this
API_SECRET_KEY=another-secret-key
# Third-party services
STRIPE_SECRET_KEY=sk_test_...
SENDGRID_API_KEY=SG...
Create .env.production:
NEXT_PUBLIC_API_URL=https://api.fastkart.com
DATABASE_URL=postgresql://prod-user:prod-pass@prod-host:5432/prod-db
JWT_SECRET=production-secret-key
Update next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
// Remove hardcoded env variables
// They will be loaded from .env files automatically
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'fastkart-admin-json.vercel.app',
},
],
},
};
module.exports = nextConfig;
⚠️ Important: Add to .gitignore:
.env
.env*.local
.env.production
🛠️ Error Handling
Centralized Error Handler
✅ Create a centralized error handler:
// src/utils/errorHandler.js
import { toast } from 'react-toastify';
export class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
export function handleApiError(error) {
// Log error for debugging (use proper logging service in production)
if (process.env.NODE_ENV === 'development') {
console.error('API Error:', error);
}
// Send to error tracking service (Sentry, etc.)
if (process.env.NODE_ENV === 'production') {
// Sentry.captureException(error);
}
// User-friendly error messages
const status = error?.response?.status;
const message = error?.response?.data?.message;
switch (status) {
case 400:
toast.error(message || 'Invalid request. Please check your input.');
break;
case 401:
toast.error('Your session has expired. Please login again.');
// Redirect to login
window.location.href = '/en/auth/login';
break;
case 403:
toast.error('You do not have permission to perform this action.');
break;
case 404:
toast.error('The requested resource was not found.');
break;
case 422:
toast.error(message || 'Validation error. Please check your input.');
break;
case 429:
toast.error('Too many requests. Please try again later.');
break;
case 500:
case 502:
case 503:
toast.error('Server error. Please try again later.');
break;
default:
toast.error(message || 'An unexpected error occurred.');
}
return Promise.reject(error);
}
// Usage in axios interceptor
// src/utils/axiosUtils/index.js
import { handleApiError } from '../errorHandler';
const client = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
Accept: "application/json",
},
});
client.interceptors.response.use(
(response) => response,
(error) => handleApiError(error)
);
📝 Logging
Structured Logging
✅ Implement proper logging:
// src/utils/logger.js
const isDevelopment = process.env.NODE_ENV === 'development';
class Logger {
info(message, meta = {}) {
if (isDevelopment) {
console.log('[INFO]', message, meta);
}
// Send to logging service in production
}
warn(message, meta = {}) {
if (isDevelopment) {
console.warn('[WARN]', message, meta);
}
// Send to logging service
}
error(message, error, meta = {}) {
if (isDevelopment) {
console.error('[ERROR]', message, error, meta);
}
// Send to error tracking service (Sentry, etc.)
}
debug(message, meta = {}) {
if (isDevelopment) {
console.debug('[DEBUG]', message, meta);
}
}
}
export const logger = new Logger();
// Usage
import { logger } from '@/utils/logger';
function fetchProducts() {
logger.info('Fetching products', { page: 1, limit: 10 });
try {
const products = await api.getProducts();
logger.info('Products fetched successfully', { count: products.length });
return products;
} catch (error) {
logger.error('Failed to fetch products', error, { page: 1 });
throw error;
}
}
🧪 Testing Security
Security Test Examples
// __tests__/security/xss.test.js
import { render, screen } from '@testing-library/react';
import TextLimit from '@/utils/customFunctions/TextLimit';
describe('XSS Prevention', () => {
it('should sanitize malicious HTML', () => {
const maliciousInput = '<script>alert("XSS")</script>Hello';
render(<TextLimit value={maliciousInput} maxLength={100} />);
// Script tag should be removed
expect(screen.queryByText(/script/i)).not.toBeInTheDocument();
expect(screen.getByText(/Hello/i)).toBeInTheDocument();
});
it('should handle img onerror attacks', () => {
const maliciousInput = '<img src=x onerror="alert(1)">';
render(<TextLimit value={maliciousInput} maxLength={100} />);
// onerror should be removed
const img = screen.queryByRole('img');
expect(img?.getAttribute('onerror')).toBeNull();
});
});
// __tests__/security/auth.test.js
describe('Authentication', () => {
it('should redirect unauthenticated users', async () => {
// Mock no auth token
document.cookie = 'auth_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC';
const { result } = renderHook(() => usePermission('product.view'));
await waitFor(() => {
expect(result.current.isAuthorized).toBe(false);
});
});
});
📚 Additional Resources
Last Updated: January 2026