581 lines
15 KiB
Markdown
581 lines
15 KiB
Markdown
# 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):**
|
|
```javascript
|
|
// src/helper/accountContext/AccountProvider.js
|
|
localStorage.setItem("role", JSON.stringify(data?.role))
|
|
```
|
|
|
|
**✅ Recommended:**
|
|
```javascript
|
|
// 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):**
|
|
```javascript
|
|
// 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:**
|
|
```javascript
|
|
// 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):**
|
|
```javascript
|
|
// src/utils/customFunctions/TextLimit.js
|
|
const sanitizeAndTrustHtml = (htmlString) => {
|
|
return { __html: htmlString }; // NO SANITIZATION!
|
|
};
|
|
```
|
|
|
|
**✅ Recommended:**
|
|
```javascript
|
|
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:**
|
|
```javascript
|
|
// 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:**
|
|
```javascript
|
|
// 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:**
|
|
```javascript
|
|
// 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:**
|
|
```javascript
|
|
// next.config.js
|
|
env: {
|
|
API_PROD_URL: "https://fastkart-admin-json.vercel.app/api/",
|
|
}
|
|
```
|
|
|
|
**✅ Recommended:**
|
|
|
|
Create `.env.local`:
|
|
```env
|
|
# 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`:
|
|
```env
|
|
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`:
|
|
```javascript
|
|
/** @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`:
|
|
```gitignore
|
|
.env
|
|
.env*.local
|
|
.env.production
|
|
```
|
|
|
|
---
|
|
|
|
## 🛠️ Error Handling
|
|
|
|
### Centralized Error Handler
|
|
|
|
**✅ Create a centralized error handler:**
|
|
```javascript
|
|
// 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:**
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// __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
|
|
|
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
|
- [Next.js Security Headers](https://nextjs.org/docs/app/api-reference/next-config-js/headers)
|
|
- [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|
|
- [DOMPurify Documentation](https://github.com/cure53/DOMPurify)
|
|
|
|
---
|
|
|
|
**Last Updated:** January 2026
|