enhanced security and input validation
This commit is contained in:
62
lib/security.ts
Normal file
62
lib/security.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
export function sanitizeHtml(input: string): string {
|
||||
return input
|
||||
.replace(/[<>'"&]/g, (char) => {
|
||||
switch (char) {
|
||||
case '<': return '<'
|
||||
case '>': return '>'
|
||||
case '"': return '"'
|
||||
case "'": return '''
|
||||
case '&': return '&'
|
||||
default: return char
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function validateDomain(domain: string): boolean {
|
||||
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$/
|
||||
return domainRegex.test(domain) && domain.length <= 253
|
||||
}
|
||||
|
||||
export function isValidIPAddress(ip: string): boolean {
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/
|
||||
|
||||
if (ipv4Regex.test(ip)) {
|
||||
return ip.split('.').every(part => {
|
||||
const num = parseInt(part, 10)
|
||||
return num >= 0 && num <= 255
|
||||
})
|
||||
}
|
||||
|
||||
return ipv6Regex.test(ip)
|
||||
}
|
||||
|
||||
export function hashPassword(password: string, salt?: string): { hash: string, salt: string } {
|
||||
const finalSalt = salt || crypto.randomBytes(32).toString('hex')
|
||||
const hash = crypto.pbkdf2Sync(password, finalSalt, 100000, 64, 'sha256').toString('hex')
|
||||
return { hash, salt: finalSalt }
|
||||
}
|
||||
|
||||
export function verifyPassword(password: string, hash: string, salt: string): boolean {
|
||||
const { hash: computedHash } = hashPassword(password, salt)
|
||||
return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(computedHash, 'hex'))
|
||||
}
|
||||
|
||||
export function generateSecureToken(length = 32): string {
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
export function rateLimitKey(req: any): string {
|
||||
const forwarded = req.headers['x-forwarded-for']
|
||||
const ip = forwarded ? forwarded.split(',')[0] : req.connection?.remoteAddress
|
||||
return `rate_limit:${ip || 'unknown'}`
|
||||
}
|
||||
|
||||
export class SecurityError extends Error {
|
||||
constructor(message: string, public code: string) {
|
||||
super(message)
|
||||
this.name = 'SecurityError'
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { sanitizeHtml, validateDomain } from './security'
|
||||
|
||||
export function sanitizeUrl(url: string): string {
|
||||
return url
|
||||
.trim()
|
||||
.replace(/[<>'"]/g, '') // Remove potential XSS characters
|
||||
.substring(0, 2048) // Limit length
|
||||
return sanitizeHtml(url.trim().substring(0, 2048))
|
||||
}
|
||||
|
||||
export function validateEmail(email: string): boolean {
|
||||
|
||||
41
middleware/security.ts
Normal file
41
middleware/security.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export function securityHeaders(req: NextApiRequest, res: NextApiResponse, next: () => void) {
|
||||
// Set security headers
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||
res.setHeader('X-Frame-Options', 'DENY')
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block')
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
|
||||
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()')
|
||||
|
||||
// Prevent information leakage
|
||||
res.removeHeader('X-Powered-By')
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
export function validateContentType(allowedTypes: string[] = ['application/json']) {
|
||||
return (req: NextApiRequest, res: NextApiResponse, next: () => void) => {
|
||||
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
|
||||
const contentType = req.headers['content-type']
|
||||
|
||||
if (!contentType || !allowedTypes.some(type => contentType.includes(type))) {
|
||||
return res.status(415).json({ error: 'Unsupported Media Type' })
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
export function validateRequestSize(maxSize = 1024 * 1024) { // 1MB default
|
||||
return (req: NextApiRequest, res: NextApiResponse, next: () => void) => {
|
||||
const contentLength = req.headers['content-length']
|
||||
|
||||
if (contentLength && parseInt(contentLength) > maxSize) {
|
||||
return res.status(413).json({ error: 'Request entity too large' })
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user