Security audit improvements: authentication, path traversal protection, file validation, rate limiting, security headers

This commit is contained in:
Hördle Bot
2025-11-24 09:34:54 +01:00
parent 0f7d66c619
commit 2d6481a42f
11 changed files with 287 additions and 15 deletions

27
lib/auth.ts Normal file
View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from 'next/server';
/**
* Authentication middleware for admin API routes
* Verifies that the request includes a valid admin session token
*/
export async function requireAdminAuth(request: NextRequest): Promise<NextResponse | null> {
const authHeader = request.headers.get('x-admin-auth');
if (!authHeader || authHeader !== 'authenticated') {
return NextResponse.json(
{ error: 'Unauthorized - Admin authentication required' },
{ status: 401 }
);
}
return null; // Auth successful
}
/**
* Helper to verify admin password
*/
export async function verifyAdminPassword(password: string): Promise<boolean> {
const bcrypt = await import('bcryptjs');
const adminPasswordHash = process.env.ADMIN_PASSWORD || '$2b$10$SHOt9G1qUNIvHoWre7499.eEtp5PtOII0daOQGNV.dhDEuPmOUdsq';
return bcrypt.compare(password, adminPasswordHash);
}

76
lib/rateLimit.ts Normal file
View File

@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server';
/**
* Rate limiting configuration
* Simple in-memory rate limiter for API endpoints
*/
interface RateLimitEntry {
count: number;
resetTime: number;
}
const rateLimitMap = new Map<string, RateLimitEntry>();
// Clean up old entries every 5 minutes
setInterval(() => {
const now = Date.now();
for (const [key, entry] of rateLimitMap.entries()) {
if (now > entry.resetTime) {
rateLimitMap.delete(key);
}
}
}, 5 * 60 * 1000);
export interface RateLimitConfig {
windowMs: number; // Time window in milliseconds
maxRequests: number; // Maximum requests per window
}
/**
* Rate limiting middleware
* @param request - The incoming request
* @param config - Rate limit configuration
* @returns NextResponse with 429 status if rate limit exceeded, null otherwise
*/
export function rateLimit(
request: NextRequest,
config: RateLimitConfig = { windowMs: 60000, maxRequests: 100 }
): NextResponse | null {
// Get client identifier (IP address or fallback)
const identifier =
request.headers.get('x-forwarded-for')?.split(',')[0] ||
request.headers.get('x-real-ip') ||
'unknown';
const now = Date.now();
const entry = rateLimitMap.get(identifier);
if (!entry || now > entry.resetTime) {
// Create new entry or reset expired entry
rateLimitMap.set(identifier, {
count: 1,
resetTime: now + config.windowMs
});
return null;
}
if (entry.count >= config.maxRequests) {
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{
status: 429,
headers: {
'Retry-After': retryAfter.toString(),
'X-RateLimit-Limit': config.maxRequests.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': new Date(entry.resetTime).toISOString()
}
}
);
}
// Increment counter
entry.count++;
return null;
}