From a7189dd2a8a1a5389c69484f4e105d3ab988f36e Mon Sep 17 00:00:00 2001 From: Sparths <70455072+Sparths@users.noreply.github.com> Date: Fri, 23 May 2025 06:54:42 +0000 Subject: [PATCH 1/5] sachen --- .env.local.backup | 17 + SECURITY_CHECKLIST.md | 41 +++ app/api/admin/verify/route.tsx | 45 ++- app/api/auth/csrf/route.tsx | 0 app/api/auth/session/route.tsx | 0 app/api/comments/route.tsx | 137 ++------ app/api/project-requests/route.tsx | 159 +++------ app/api/users/route.tsx | 61 +--- hooks/use-csrf.ts | 0 lib/security/csrf-protection.ts | 173 +++++++++ lib/security/rate-limiter-config.ts | 194 ++++++++++ lib/security/sanitization.ts | 101 ++++++ lib/security/secure-token.ts | 74 ++++ lib/security/security-headers.ts | 137 ++++++++ lib/security/session-manager.ts | 247 +++++++++++++ middleware.ts | 150 +++----- package-lock.json | 526 +++++++++++++++++++++++++++- package.json | 6 +- test-security.sh | 23 ++ 19 files changed, 1664 insertions(+), 427 deletions(-) create mode 100644 .env.local.backup create mode 100644 SECURITY_CHECKLIST.md create mode 100644 app/api/auth/csrf/route.tsx create mode 100644 app/api/auth/session/route.tsx create mode 100644 hooks/use-csrf.ts create mode 100644 lib/security/csrf-protection.ts create mode 100644 lib/security/rate-limiter-config.ts create mode 100644 lib/security/sanitization.ts create mode 100644 lib/security/secure-token.ts create mode 100644 lib/security/security-headers.ts create mode 100644 lib/security/session-manager.ts create mode 100755 test-security.sh diff --git a/.env.local.backup b/.env.local.backup new file mode 100644 index 0000000..1ce73ac --- /dev/null +++ b/.env.local.backup @@ -0,0 +1,17 @@ +POSTGRES_URL="postgres://postgres.hxijjvnsrxynegthasmh:pcE680j67rhFfDRr@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=require&supa=base-pooler.x" +POSTGRES_USER="postgres" +POSTGRES_HOST="db.hxijjvnsrxynegthasmh.supabase.co" +SUPABASE_JWT_SECRET="998Vo+CP+u2rNnH6fv9w4KWGb+8I1P9bXjrM2tKiFE5YcIlQ1Tr+NBNPNGw1de2drhvoXX0dSZJZ84toPkxBtA==" +NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imh4aWpqdm5zcnh5bmVndGhhc21oIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDc3Mzk5NDcsImV4cCI6MjA2MzMxNTk0N30.zWILyiJA1K5L0w2rynsWqhMVU6jySaal5lC9IBO05bc" +POSTGRES_PRISMA_URL="postgres://postgres.hxijjvnsrxynegthasmh:pcE680j67rhFfDRr@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=require&supa=base-pooler.x" +POSTGRES_PASSWORD="pcE680j67rhFfDRr" +POSTGRES_DATABASE="postgres" +SUPABASE_URL="https://hxijjvnsrxynegthasmh.supabase.co" +NEXT_PUBLIC_SUPABASE_URL="https://hxijjvnsrxynegthasmh.supabase.co" +SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imh4aWpqdm5zcnh5bmVndGhhc21oIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0NzczOTk0NywiZXhwIjoyMDYzMzE1OTQ3fQ.poV3TJW7eJULdQ_yYHhygUPbEMYW-SSrIVWSsnRXRAk" +POSTGRES_URL_NON_POOLING="postgres://postgres.hxijjvnsrxynegthasmh:pcE680j67rhFfDRr@aws-0-eu-central-1.pooler.supabase.com:5432/postgres?sslmode=require" +DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/1326548524657016842/rJrqRnK7lZQKKh_kzT2bz_rjBx6T0BDz8vym4SVeMt-sLXl4PaMycrYuM8pbbr1JNpSm" +NEXT_PUBLIC_SITE_URL = "https://orange-engine-q6gpjxwwqg9h9pw5-3001.app.github.dev" + +AUTHORIZED_ADMINS=Sparths,sparths,f8adc96a-496f-412b-af15-20bd3cd66b3c +NEXT_PUBLIC_AUTHORIZED_ADMINS=Sparths,sparths \ No newline at end of file diff --git a/SECURITY_CHECKLIST.md b/SECURITY_CHECKLIST.md new file mode 100644 index 0000000..72fdec8 --- /dev/null +++ b/SECURITY_CHECKLIST.md @@ -0,0 +1,41 @@ +# Security Implementation Checklist + +## Files to Update +- [ ] Copy secure-token.ts content from artifact +- [ ] Copy sanitization.ts content from artifact +- [ ] Copy rate-limiter-config.ts content from artifact +- [ ] Copy csrf-protection.ts content from artifact +- [ ] Copy session-manager.ts content from artifact +- [ ] Copy security-headers.ts content from artifact +- [ ] Update middleware.ts +- [ ] Create use-csrf.ts hook +- [ ] Create csrf API route +- [ ] Create session API route + +## API Routes to Update +- [ ] /app/api/admin/verify/route.tsx +- [ ] /app/api/project-requests/route.tsx +- [ ] /app/api/users/route.tsx +- [ ] /app/api/comments/route.tsx +- [ ] /app/api/ratings/route.tsx +- [ ] /app/api/badges/route.tsx + +## Frontend Components to Update +- [ ] CommentSection.tsx - Add CSRF headers +- [ ] RatingSystem.tsx - Add CSRF headers +- [ ] AuthContext.tsx - Remove localStorage usage +- [ ] UserProjectRequests.tsx - Add CSRF headers +- [ ] ProfileForm.tsx - Add CSRF headers + +## Testing +- [ ] Test rate limiting +- [ ] Test CSRF protection +- [ ] Test XSS prevention +- [ ] Test admin authentication +- [ ] Test session management + +## Deployment +- [ ] Set environment variables in production +- [ ] Enable HTTPS +- [ ] Test all features in production +- [ ] Monitor security logs diff --git a/app/api/admin/verify/route.tsx b/app/api/admin/verify/route.tsx index 425a4e7..678a9e4 100644 --- a/app/api/admin/verify/route.tsx +++ b/app/api/admin/verify/route.tsx @@ -1,5 +1,7 @@ import { NextResponse } from "next/server"; import { createClient } from '@supabase/supabase-js'; +import { createSecureAdminToken, verifySecureAdminToken } from '@/lib/security/secure-token'; +import { sanitizeInput } from '@/lib/security/sanitization'; // Admin verification with service role const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; @@ -19,11 +21,6 @@ const getAuthorizedAdmins = (): string[] => { return adminList.split(',').map(admin => admin.trim()); }; -// Input sanitization -const sanitizeInput = (input: string): string => { - return input.replace(/[<>]/g, '').trim(); -}; - export async function POST(request: Request) { try { const body = await request.json(); @@ -52,15 +49,8 @@ export async function POST(request: Request) { if (authorizedAdmins.includes(sanitizedUserId)) { console.log("User found in direct admin list:", sanitizedUserId); - // Create admin session token for subsequent requests - const adminSession = { - userId: sanitizedUserId, - timestamp: Date.now(), - verified: true, - isAdmin: true - }; - - const adminToken = Buffer.from(JSON.stringify(adminSession)).toString('base64'); + // Create secure admin session token + const adminToken = createSecureAdminToken(sanitizedUserId); return NextResponse.json({ success: true, @@ -108,15 +98,8 @@ export async function POST(request: Request) { if (isAdmin) { console.log("User verified as admin"); - // Create admin session token for subsequent requests - const adminSession = { - userId: sanitizedUserId, - timestamp: Date.now(), - verified: true, - isAdmin: true - }; - - const adminToken = Buffer.from(JSON.stringify(adminSession)).toString('base64'); + // Create secure admin session token + const adminToken = createSecureAdminToken(sanitizedUserId); return NextResponse.json({ success: true, @@ -160,12 +143,26 @@ export async function POST(request: Request) { } } -// Optional: GET method to check current admin status +// GET method to check current admin status export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const userId = searchParams.get("userId"); + // Check for admin token in Authorization header + const authHeader = request.headers.get('authorization'); + if (authHeader && authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7); + const verification = verifySecureAdminToken(token); + + if (verification.isValid) { + return NextResponse.json({ + isAdmin: true, + userId: verification.userId + }); + } + } + if (!userId) { return NextResponse.json( { error: "User ID is required" }, diff --git a/app/api/auth/csrf/route.tsx b/app/api/auth/csrf/route.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/api/auth/session/route.tsx b/app/api/auth/session/route.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/api/comments/route.tsx b/app/api/comments/route.tsx index 5941aef..18b128b 100644 --- a/app/api/comments/route.tsx +++ b/app/api/comments/route.tsx @@ -1,18 +1,9 @@ import { NextResponse } from "next/server"; import supabase from "@/lib/supabase"; import { generateUUID } from "@/lib/utils-uuid"; -import { rateLimit, createRateLimitHeaders } from "@/lib/rate-limiter"; - -// Input validation and sanitization -const sanitizeInput = (input: string): string => { - // Remove potentially dangerous characters and trim - return input - .replace(/[<>]/g, '') // Remove < and > to prevent XSS - .replace(/javascript:/gi, '') // Remove javascript: protocol - .replace(/on\w+=/gi, '') // Remove event handlers - .trim(); -}; +import { sanitizeInput } from "@/lib/security/sanitization"; +// Validation const validateComment = (text: string): boolean => { return text.length >= 1 && text.length <= 2000; }; @@ -30,26 +21,6 @@ const validateUserId = (userId: string): boolean => { // GET: Fetch comments export async function GET(request: Request) { try { - // Apply rate limiting with better limits - const rateLimitResult = await rateLimit(request, 'comments'); - const rateLimitHeaders = createRateLimitHeaders(rateLimitResult); - - if (!rateLimitResult.success) { - return NextResponse.json( - { - error: "Too many requests", - retryAfter: rateLimitResult.reset - }, - { - status: 429, - headers: { - ...rateLimitHeaders, - 'Retry-After': rateLimitResult.reset?.toString() || '300' - } - } - ); - } - const { searchParams } = new URL(request.url); const projectName = searchParams.get("project"); const userId = searchParams.get("userId"); @@ -61,14 +32,14 @@ export async function GET(request: Request) { if (sanitizedProjectName && !validateProjectName(sanitizedProjectName)) { return NextResponse.json( { error: "Invalid project name" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } if (sanitizedUserId && !validateUserId(sanitizedUserId)) { return NextResponse.json( { error: "Invalid user ID" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } @@ -91,11 +62,11 @@ export async function GET(request: Request) { console.error("Error getting comments:", error); return NextResponse.json( { error: "Failed to get comments" }, - { status: 500, headers: rateLimitHeaders } + { status: 500 } ); } - return NextResponse.json(comments || [], { headers: rateLimitHeaders }); + return NextResponse.json(comments || []); } catch (error) { console.error("Error getting comments:", error); return NextResponse.json( @@ -108,26 +79,6 @@ export async function GET(request: Request) { // POST: Create a new comment export async function POST(request: Request) { try { - // Apply rate limiting - const rateLimitResult = await rateLimit(request, 'comments'); - const rateLimitHeaders = createRateLimitHeaders(rateLimitResult); - - if (!rateLimitResult.success) { - return NextResponse.json( - { - error: "Too many comments. Please wait before posting again.", - retryAfter: rateLimitResult.reset - }, - { - status: 429, - headers: { - ...rateLimitHeaders, - 'Retry-After': rateLimitResult.reset?.toString() || '300' - } - } - ); - } - const body = await request.json(); const { projectName, userId, text, parentId } = body; @@ -135,7 +86,7 @@ export async function POST(request: Request) { if (!projectName || !userId || !text) { return NextResponse.json( { error: "Project name, user ID, and comment text are required" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } @@ -149,21 +100,21 @@ export async function POST(request: Request) { if (!validateProjectName(sanitizedProjectName)) { return NextResponse.json( { error: "Invalid project name" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } if (!validateUserId(sanitizedUserId)) { return NextResponse.json( { error: "Invalid user ID" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } if (!validateComment(sanitizedText)) { return NextResponse.json( { error: "Comment must be between 1 and 2000 characters" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } @@ -177,7 +128,7 @@ export async function POST(request: Request) { if (userError || !user) { return NextResponse.json( { error: "User authentication failed" }, - { status: 401, headers: rateLimitHeaders } + { status: 401 } ); } @@ -192,7 +143,7 @@ export async function POST(request: Request) { if (parentError || !parentComment) { return NextResponse.json( { error: "Parent comment not found" }, - { status: 404, headers: rateLimitHeaders } + { status: 404 } ); } } @@ -208,7 +159,7 @@ export async function POST(request: Request) { if (recentComments && recentComments.length >= 10) { // Increased from 5 to 10 return NextResponse.json( { error: "Too many recent comments. Please wait before posting again." }, - { status: 429, headers: rateLimitHeaders } + { status: 429 } ); } @@ -224,7 +175,7 @@ export async function POST(request: Request) { if (duplicateComment && duplicateComment.length > 0) { return NextResponse.json( { error: "Duplicate comment detected" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } @@ -247,7 +198,7 @@ export async function POST(request: Request) { console.error("Error inserting comment:", insertError); return NextResponse.json( { error: "Failed to create comment" }, - { status: 500, headers: rateLimitHeaders } + { status: 500 } ); } @@ -284,7 +235,7 @@ export async function POST(request: Request) { // Don't fail the comment creation if points award fails } - return NextResponse.json(newComment, { headers: rateLimitHeaders }); + return NextResponse.json(newComment); } catch (error) { console.error("Error creating comment:", error); return NextResponse.json( @@ -297,22 +248,6 @@ export async function POST(request: Request) { // PUT: Update a comment (like/unlike/edit) export async function PUT(request: Request) { try { - const rateLimitResult = await rateLimit(request, 'comments'); - const rateLimitHeaders = createRateLimitHeaders(rateLimitResult); - - if (!rateLimitResult.success) { - return NextResponse.json( - { error: "Too many requests" }, - { - status: 429, - headers: { - ...rateLimitHeaders, - 'Retry-After': rateLimitResult.reset?.toString() || '60' - } - } - ); - } - const body = await request.json(); const { commentId, userId, action, text } = body; @@ -320,7 +255,7 @@ export async function PUT(request: Request) { if (!commentId || !userId || !action) { return NextResponse.json( { error: "Comment ID, user ID, and action are required" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } @@ -333,14 +268,14 @@ export async function PUT(request: Request) { if (!["like", "unlike", "edit"].includes(sanitizedAction)) { return NextResponse.json( { error: "Invalid action" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } if (sanitizedAction === "edit" && (!sanitizedText || !validateComment(sanitizedText))) { return NextResponse.json( { error: "Valid comment text is required for edit action" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } @@ -354,7 +289,7 @@ export async function PUT(request: Request) { if (getCommentError || !comment) { return NextResponse.json( { error: "Comment not found" }, - { status: 404, headers: rateLimitHeaders } + { status: 404 } ); } @@ -374,7 +309,7 @@ export async function PUT(request: Request) { if (comment.user_id !== sanitizedUserId) { return NextResponse.json( { error: "Not authorized to edit this comment" }, - { status: 403, headers: rateLimitHeaders } + { status: 403 } ); } @@ -396,11 +331,11 @@ export async function PUT(request: Request) { console.error("Error updating comment:", updateError); return NextResponse.json( { error: "Failed to update comment" }, - { status: 500, headers: rateLimitHeaders } + { status: 500 } ); } - return NextResponse.json(updatedComment, { headers: rateLimitHeaders }); + return NextResponse.json(updatedComment); } catch (error) { console.error("Error updating comment:", error); return NextResponse.json( @@ -413,22 +348,6 @@ export async function PUT(request: Request) { // DELETE: Delete a comment export async function DELETE(request: Request) { try { - const rateLimitResult = await rateLimit(request, 'comments'); - const rateLimitHeaders = createRateLimitHeaders(rateLimitResult); - - if (!rateLimitResult.success) { - return NextResponse.json( - { error: "Too many requests" }, - { - status: 429, - headers: { - ...rateLimitHeaders, - 'Retry-After': rateLimitResult.reset?.toString() || '60' - } - } - ); - } - const { searchParams } = new URL(request.url); const commentId = searchParams.get("id"); const userId = searchParams.get("userId"); @@ -436,7 +355,7 @@ export async function DELETE(request: Request) { if (!commentId || !userId) { return NextResponse.json( { error: "Comment ID and User ID are required" }, - { status: 400, headers: rateLimitHeaders } + { status: 400 } ); } @@ -454,14 +373,14 @@ export async function DELETE(request: Request) { if (getCommentError || !comment) { return NextResponse.json( { error: "Comment not found" }, - { status: 404, headers: rateLimitHeaders } + { status: 404 } ); } if (comment.user_id !== sanitizedUserId) { return NextResponse.json( { error: "Not authorized to delete this comment" }, - { status: 403, headers: rateLimitHeaders } + { status: 403 } ); } @@ -475,7 +394,7 @@ export async function DELETE(request: Request) { console.error("Error deleting comment:", deleteError); return NextResponse.json( { error: "Failed to delete comment" }, - { status: 500, headers: rateLimitHeaders } + { status: 500 } ); } @@ -490,7 +409,7 @@ export async function DELETE(request: Request) { // Continue execution } - return NextResponse.json({ success: true }, { headers: rateLimitHeaders }); + return NextResponse.json({ success: true }); } catch (error) { console.error("Error deleting comment:", error); return NextResponse.json( diff --git a/app/api/project-requests/route.tsx b/app/api/project-requests/route.tsx index 1761cf6..eb139f3 100644 --- a/app/api/project-requests/route.tsx +++ b/app/api/project-requests/route.tsx @@ -2,7 +2,8 @@ import { NextResponse } from "next/server"; import supabase from "@/lib/supabase"; import { generateUUID } from "@/lib/utils-uuid"; import { createClient } from '@supabase/supabase-js'; -import { rateLimit } from "@/lib/rate-limiter"; +import { sanitizeInput, sanitizeURL } from "@/lib/security/sanitization"; +import { verifySecureAdminToken } from "@/lib/security/secure-token"; const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL; @@ -13,14 +14,13 @@ const adminSupabase = supabaseServiceKey ? createClient(supabaseUrl, supabaseServiceKey) : supabase; -// Input validation helpers -const sanitizeInput = (input: string): string => { - return input.replace(/[<>]/g, '').trim(); -}; - +// Enhanced validation functions const validateGitHubUrl = (url: string): boolean => { + const sanitizedUrl = sanitizeURL(url); + if (!sanitizedUrl) return false; + const githubRegex = /^https:\/\/github\.com\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+\/?$/; - return githubRegex.test(url); + return githubRegex.test(sanitizedUrl); }; const validateTitle = (title: string): boolean => { @@ -35,38 +35,39 @@ const validateReason = (reason: string): boolean => { return reason.length >= 10 && reason.length <= 1000; }; -// Simple admin verification for tokens +// Secure admin verification const verifyAdminAccess = async (request: Request): Promise<{ isValid: boolean; user?: { id: string }; + error?: string; }> => { - // This is a simplified version - in production you'd want proper verification try { const authHeader = request.headers.get('authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { - return { isValid: false }; + return { isValid: false, error: 'Missing authorization header' }; } - - // For legacy users with actual tokens, you'd verify the JWT here - // For now, we'll return false to force admin token usage - return { isValid: false }; - } catch { - return { isValid: false }; + const token = authHeader.substring(7); + + // Verify admin token using secure method + const verification = verifySecureAdminToken(token); + + if (!verification.isValid) { + return { isValid: false, error: verification.error || 'Invalid token' }; + } + + return { + isValid: true, + user: { id: verification.userId! } + }; + } catch (error) { + console.error("Admin verification error:", error); + return { isValid: false, error: 'Verification failed' }; } }; export async function POST(request: Request) { try { - // Apply rate limiting - const rateLimitResult = await rateLimit(request, 'project_requests'); - if (!rateLimitResult.success) { - return NextResponse.json( - { error: "Too many project submissions. Please try again later." }, - { status: 429 } - ); - } - const body = await request.json(); const { title, githubLink, description, reason, userId } = body; @@ -78,9 +79,9 @@ export async function POST(request: Request) { ); } - // Sanitize inputs + // Sanitize inputs with enhanced sanitization const sanitizedTitle = sanitizeInput(title); - const sanitizedGithubLink = sanitizeInput(githubLink); + const sanitizedGithubLink = sanitizeURL(githubLink) || ''; const sanitizedDescription = sanitizeInput(description); const sanitizedReason = sanitizeInput(reason); const sanitizedUserId = sanitizeInput(userId); @@ -143,7 +144,7 @@ export async function POST(request: Request) { ); } - // Check recent submissions to prevent spam + // Check recent submissions to prevent spam with per-user rate limiting const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); const { data: recentRequests } = await supabase .from('project_requests') @@ -183,7 +184,7 @@ export async function POST(request: Request) { ); } - // Send to Discord (if webhook is configured) + // Send to Discord (if webhook is configured) - in try-catch to not fail main request if (DISCORD_WEBHOOK_URL) { try { const discordMessage = { @@ -269,54 +270,13 @@ export async function GET(request: Request) { // For admin requests, verify the admin token console.log("Admin request detected"); - const authHeader = request.headers.get('authorization'); - - if (!authHeader || !authHeader.startsWith('Bearer ')) { + const adminVerification = await verifyAdminAccess(request); + if (!adminVerification.isValid) { return NextResponse.json( - { error: "Admin authorization required" }, - { status: 401 } + { error: adminVerification.error || "Admin access required" }, + { status: 403 } ); } - - const token = authHeader.substring(7); - - // Verify admin token - let adminUserId = null; - try { - // Try to decode admin token first - const decoded = Buffer.from(token, 'base64').toString('utf-8'); - const tokenData = JSON.parse(decoded); - - // Check if it's a valid admin token (not expired, has admin flag) - if (tokenData.isAdmin && tokenData.userId && tokenData.timestamp) { - const tokenAge = Date.now() - tokenData.timestamp; - const oneHour = 60 * 60 * 1000; - - if (tokenAge < oneHour) { // Token valid for 1 hour - adminUserId = tokenData.userId; - console.log("Valid admin token for user:", adminUserId); - } else { - console.log("Admin token expired"); - return NextResponse.json( - { error: "Admin token expired" }, - { status: 401 } - ); - } - } else { - throw new Error("Not an admin token"); - } - } catch { - // If admin token fails, try legacy admin verification - console.log("Admin token failed, trying legacy verification"); - const adminVerification = await verifyAdminAccess(request as Request); - if (!adminVerification.isValid) { - return NextResponse.json( - { error: "Admin access required" }, - { status: 403 } - ); - } - adminUserId = adminVerification.user?.id; - } console.log("Admin verification passed, fetching all requests"); @@ -403,55 +363,14 @@ export async function PUT(request: Request) { ); } - // Verify admin access using the same token logic as GET - const authHeader = request.headers.get('authorization'); - - if (!authHeader || !authHeader.startsWith('Bearer ')) { + // Verify admin access using secure token + const adminVerification = await verifyAdminAccess(request); + if (!adminVerification.isValid) { return NextResponse.json( - { error: "Admin authorization required" }, - { status: 401 } + { error: adminVerification.error || "Admin access required" }, + { status: 403 } ); } - - const token = authHeader.substring(7); - - // Verify admin token - let adminUserId = null; - try { - // Try to decode admin token first - const decoded = Buffer.from(token, 'base64').toString('utf-8'); - const tokenData = JSON.parse(decoded); - - // Check if it's a valid admin token (not expired, has admin flag) - if (tokenData.isAdmin && tokenData.userId && tokenData.timestamp) { - const tokenAge = Date.now() - tokenData.timestamp; - const oneHour = 60 * 60 * 1000; - - if (tokenAge < oneHour) { // Token valid for 1 hour - adminUserId = tokenData.userId; - console.log("Valid admin token for user:", adminUserId); - } else { - console.log("Admin token expired"); - return NextResponse.json( - { error: "Admin token expired" }, - { status: 401 } - ); - } - } else { - throw new Error("Not an admin token"); - } - } catch { - // If admin token fails, try legacy admin verification - console.log("Admin token failed, trying legacy verification"); - const adminVerification = await verifyAdminAccess(request as Request); - if (!adminVerification.isValid) { - return NextResponse.json( - { error: "Admin access required" }, - { status: 403 } - ); - } - adminUserId = adminVerification.user?.id; - } // Sanitize inputs const sanitizedRequestId = sanitizeInput(requestId); diff --git a/app/api/users/route.tsx b/app/api/users/route.tsx index cee9d95..bca5957 100644 --- a/app/api/users/route.tsx +++ b/app/api/users/route.tsx @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import crypto from "crypto"; import supabase from "@/lib/supabase"; -import { rateLimit } from "@/lib/rate-limiter"; +import { sanitizeInput, isValidEmail, isValidUsername } from "@/lib/security/sanitization"; interface Badge { id: string; @@ -11,17 +11,7 @@ interface Badge { points: number; } -// Input validation helpers -const validateEmail = (email: string): boolean => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email) && email.length <= 254; -}; - -const validateUsername = (username: string): boolean => { - const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/; - return usernameRegex.test(username); -}; - +// Validate password const validatePassword = (password: string): boolean => { // At least 8 characters, one uppercase, one lowercase, one number, one special char const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,128}$/; @@ -32,11 +22,6 @@ const validateDisplayName = (displayName: string): boolean => { return displayName.trim().length >= 1 && displayName.length <= 50; }; -// Sanitize input to prevent XSS -const sanitizeInput = (input: string): string => { - return input.replace(/[<>]/g, '').trim(); -}; - // Hash password securely const hashPassword = (password: string, salt: string): string => { return crypto.pbkdf2Sync(password, salt, 100000, 64, "sha512").toString("hex"); @@ -50,15 +35,6 @@ const generateSalt = (): string => { // Create user (Registration) - NOT EXPORTED async function createUser(request: Request) { try { - // Apply rate limiting - const rateLimitResult = await rateLimit(request, 'users_create'); - if (!rateLimitResult.success) { - return NextResponse.json( - { error: "Too many registration attempts" }, - { status: 429 } - ); - } - const body = await request.json(); const { username, password, email, displayName } = body; @@ -74,14 +50,14 @@ async function createUser(request: Request) { const sanitizedEmail = sanitizeInput(email); const sanitizedDisplayName = displayName ? sanitizeInput(displayName) : sanitizedUsername; - if (!validateUsername(sanitizedUsername)) { + if (!isValidUsername(sanitizedUsername)) { return NextResponse.json( { error: "Username must be 3-30 characters, alphanumeric, underscore, or hyphen only" }, { status: 400 } ); } - if (!validateEmail(sanitizedEmail)) { + if (!isValidEmail(sanitizedEmail)) { return NextResponse.json( { error: "Invalid email format" }, { status: 400 } @@ -189,15 +165,6 @@ async function createUser(request: Request) { // Login - NOT EXPORTED async function loginUser(request: Request) { try { - // Apply rate limiting - const rateLimitResult = await rateLimit(request, 'users_login'); - if (!rateLimitResult.success) { - return NextResponse.json( - { error: "Too many login attempts" }, - { status: 429 } - ); - } - const body = await request.json(); const { username, password } = body; @@ -210,7 +177,7 @@ async function loginUser(request: Request) { const sanitizedUsername = sanitizeInput(username); - if (!validateUsername(sanitizedUsername)) { + if (!isValidUsername(sanitizedUsername)) { return NextResponse.json( { error: "Invalid credentials" }, { status: 401 } @@ -269,15 +236,6 @@ async function loginUser(request: Request) { // Update user (with proper authorization) - NOT EXPORTED async function updateUser(request: Request) { try { - // Apply rate limiting - const rateLimitResult = await rateLimit(request, 'users_update'); - if (!rateLimitResult.success) { - return NextResponse.json( - { error: "Too many update attempts" }, - { status: 429 } - ); - } - const body = await request.json(); const { id, displayName, currentPassword, newPassword } = body; @@ -471,15 +429,6 @@ async function checkBadges(request: Request) { // GET: Retrieve users (with proper authorization) export async function GET(request: Request) { try { - // Apply rate limiting - const rateLimitResult = await rateLimit(request, 'users_get'); - if (!rateLimitResult.success) { - return NextResponse.json( - { error: "Too many requests" }, - { status: 429 } - ); - } - const { searchParams } = new URL(request.url); const userId = searchParams.get("id"); const username = searchParams.get("username"); diff --git a/hooks/use-csrf.ts b/hooks/use-csrf.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/security/csrf-protection.ts b/lib/security/csrf-protection.ts new file mode 100644 index 0000000..32a60b2 --- /dev/null +++ b/lib/security/csrf-protection.ts @@ -0,0 +1,173 @@ +// lib/csrf-protection.ts +import crypto from 'crypto'; +import { cookies } from 'next/headers'; +import React from 'react'; + +const CSRF_SECRET = process.env.CSRF_SECRET || crypto.randomBytes(32).toString('hex'); +const CSRF_TOKEN_LENGTH = 32; +const CSRF_COOKIE_NAME = '__csrf'; +const CSRF_HEADER_NAME = 'x-csrf-token'; + +// Generate CSRF token +export function generateCSRFToken(): string { + const token = crypto.randomBytes(CSRF_TOKEN_LENGTH).toString('hex'); + const timestamp = Date.now().toString(); + const signature = crypto + .createHmac('sha256', CSRF_SECRET) + .update(`${token}:${timestamp}`) + .digest('hex'); + + return Buffer.from(JSON.stringify({ + token, + timestamp, + signature + })).toString('base64'); +} + +// Verify CSRF token +export function verifyCSRFToken(token: string): boolean { + try { + const decoded = Buffer.from(token, 'base64').toString('utf-8'); + const { token: rawToken, timestamp, signature } = JSON.parse(decoded); + + // Check token age (24 hours) + const tokenAge = Date.now() - parseInt(timestamp); + if (tokenAge > 24 * 60 * 60 * 1000) { + return false; + } + + // Verify signature + const expectedSignature = crypto + .createHmac('sha256', CSRF_SECRET) + .update(`${rawToken}:${timestamp}`) + .digest('hex'); + + return crypto.timingSafeEqual( + Buffer.from(signature, 'hex'), + Buffer.from(expectedSignature, 'hex') + ); + } catch { + return false; + } +} + +// Middleware to check CSRF token +export async function checkCSRF(request: Request): Promise<{ + valid: boolean; + error?: string; +}> { + // Skip CSRF check for safe methods + if (['GET', 'HEAD', 'OPTIONS'].includes(request.method)) { + return { valid: true }; + } + + // Get token from header + const headerToken = request.headers.get(CSRF_HEADER_NAME); + if (!headerToken) { + return { valid: false, error: 'Missing CSRF token' }; + } + + // Verify token + if (!verifyCSRFToken(headerToken)) { + return { valid: false, error: 'Invalid CSRF token' }; + } + + return { valid: true }; +} + +// React hook for CSRF token +export function useCSRFToken(): { + token: string; + headers: Record; +} { + const [token, setToken] = React.useState(''); + + React.useEffect(() => { + // Get or generate CSRF token + const existingToken = getCookie(CSRF_COOKIE_NAME); + if (existingToken && verifyCSRFToken(existingToken)) { + setToken(existingToken); + } else { + const newToken = generateCSRFToken(); + setCookie(CSRF_COOKIE_NAME, newToken, { + httpOnly: false, // Needs to be accessible by JavaScript + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 24 * 60 * 60 // 24 hours + }); + setToken(newToken); + } + }, []); + + return { + token, + headers: { + [CSRF_HEADER_NAME]: token + } + }; +} + +// Helper functions for cookies +function getCookie(name: string): string | undefined { + if (typeof window === 'undefined') return undefined; + + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + return parts.pop()?.split(';').shift(); + } + return undefined; +} + +function setCookie(name: string, value: string, options: { + httpOnly?: boolean; + secure?: boolean; + sameSite?: 'strict' | 'lax' | 'none'; + maxAge?: number; + path?: string; +}) { + if (typeof window === 'undefined') return; + + let cookie = `${name}=${value}`; + + if (options.maxAge) { + cookie += `; max-age=${options.maxAge}`; + } + + if (options.path) { + cookie += `; path=${options.path}`; + } else { + cookie += '; path=/'; + } + + if (options.secure) { + cookie += '; secure'; + } + + if (options.sameSite) { + cookie += `; samesite=${options.sameSite}`; + } + + document.cookie = cookie; +} + +// API route wrapper with CSRF protection +export function withCSRFProtection( + handler: (req: Request) => Promise +): (req: Request) => Promise { + return async (req: Request) => { + const csrfCheck = await checkCSRF(req); + + if (!csrfCheck.valid) { + return new Response( + JSON.stringify({ error: csrfCheck.error || 'CSRF validation failed' }), + { + status: 403, + headers: { 'Content-Type': 'application/json' } + } + ); + } + + return handler(req); + }; +} \ No newline at end of file diff --git a/lib/security/rate-limiter-config.ts b/lib/security/rate-limiter-config.ts new file mode 100644 index 0000000..afe0cf7 --- /dev/null +++ b/lib/security/rate-limiter-config.ts @@ -0,0 +1,194 @@ +// lib/rate-limiter-config.ts +import { LRUCache } from 'lru-cache'; + +interface RateLimitRecord { + count: number; + resetTime: number; +} + +interface RateLimitConfig { + maxRequests: number; + windowMs: number; + skipSuccessfulRequests?: boolean; + skipFailedRequests?: boolean; +} + +// Use LRU cache instead of Map for better memory management +const rateLimitStore = new LRUCache({ + max: 10000, // Maximum number of items in cache + ttl: 60 * 60 * 1000, // 1 hour TTL +}); + +// Reasonable rate limiting configurations +export const RATE_LIMIT_CONFIGS: Record = { + // Authentication endpoints - strict limits + 'users_create': { + maxRequests: 3, + windowMs: 15 * 60 * 1000 // 3 registrations per 15 minutes per IP + }, + 'users_login': { + maxRequests: 5, + windowMs: 15 * 60 * 1000, // 5 login attempts per 15 minutes + skipSuccessfulRequests: true // Don't count successful logins + }, + 'users_update': { + maxRequests: 10, + windowMs: 5 * 60 * 1000 // 10 updates per 5 minutes + }, + + // Read operations - more lenient + 'users_get': { + maxRequests: 100, + windowMs: 5 * 60 * 1000 // 100 gets per 5 minutes + }, + + // Content creation - moderate limits + 'project_requests': { + maxRequests: 5, + windowMs: 60 * 60 * 1000 // 5 project requests per hour + }, + 'comments': { + maxRequests: 20, + windowMs: 5 * 60 * 1000 // 20 comments per 5 minutes + }, + 'ratings': { + maxRequests: 30, + windowMs: 60 * 60 * 1000 // 30 ratings per hour + }, + + // Admin endpoints - special handling + 'admin_verify': { + maxRequests: 10, + windowMs: 60 * 60 * 1000 // 10 verifications per hour + }, + + // Default for other endpoints + 'default': { + maxRequests: 60, + windowMs: 60 * 1000 // 60 requests per minute + } +}; + +// Enhanced rate limiting with distributed support +export async function rateLimit( + request: Request, + action: string, + identifier?: string // Allow custom identifier (e.g., user ID) +): Promise<{ + success: boolean; + reset?: number; + remaining?: number; + limit?: number; +}> { + const ip = getClientIP(request); + const key = identifier ? `${identifier}:${action}` : `${ip}:${action}`; + const now = Date.now(); + + const config = RATE_LIMIT_CONFIGS[action] || RATE_LIMIT_CONFIGS.default; + let record = rateLimitStore.get(key); + + if (!record || now > record.resetTime) { + // First request or window has reset + record = { + count: 1, + resetTime: now + config.windowMs + }; + rateLimitStore.set(key, record); + + return { + success: true, + remaining: config.maxRequests - 1, + limit: config.maxRequests, + reset: Math.ceil((record.resetTime - now) / 1000) + }; + } + + if (record.count >= config.maxRequests) { + return { + success: false, + remaining: 0, + limit: config.maxRequests, + reset: Math.ceil((record.resetTime - now) / 1000) + }; + } + + // Increment count + record.count++; + rateLimitStore.set(key, record); + + return { + success: true, + remaining: config.maxRequests - record.count, + limit: config.maxRequests, + reset: Math.ceil((record.resetTime - now) / 1000) + }; +} + +// Get client IP with better detection +function getClientIP(request: Request): string { + const headers = request.headers; + + // Check various headers in order of preference + const ipHeaders = [ + 'cf-connecting-ip', // Cloudflare + 'x-real-ip', // Nginx proxy + 'x-forwarded-for', // Standard proxy header + 'x-client-ip', // Some proxies + 'x-cluster-client-ip', // Some load balancers + 'forwarded', // RFC 7239 + ]; + + for (const header of ipHeaders) { + const value = headers.get(header); + if (value) { + // Handle x-forwarded-for which may contain multiple IPs + const ip = value.split(',')[0].trim(); + if (isValidIP(ip)) { + return ip; + } + } + } + + return 'unknown'; +} + +// Validate IP address +function isValidIP(ip: string): boolean { + // Basic IPv4 validation + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; + if (ipv4Regex.test(ip)) { + const parts = ip.split('.'); + return parts.every(part => { + const num = parseInt(part, 10); + return num >= 0 && num <= 255; + }); + } + + // Basic IPv6 validation + const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; + return ipv6Regex.test(ip); +} + +// Helper to create rate limit headers +export function createRateLimitHeaders(result: { + remaining?: number; + limit?: number; + reset?: number; +}): Record { + const headers: Record = {}; + + if (result.limit !== undefined) { + headers['X-RateLimit-Limit'] = result.limit.toString(); + } + + if (result.remaining !== undefined) { + headers['X-RateLimit-Remaining'] = result.remaining.toString(); + } + + if (result.reset !== undefined) { + headers['X-RateLimit-Reset'] = result.reset.toString(); + headers['X-RateLimit-Reset-After'] = result.reset.toString(); + } + + return headers; +} \ No newline at end of file diff --git a/lib/security/sanitization.ts b/lib/security/sanitization.ts new file mode 100644 index 0000000..5aba00e --- /dev/null +++ b/lib/security/sanitization.ts @@ -0,0 +1,101 @@ +// lib/sanitization.ts +import DOMPurify from 'isomorphic-dompurify'; + +// Enhanced sanitization function that prevents XSS +export function sanitizeInput(input: string): string { + if (typeof input !== 'string') return ''; + + // Remove any HTML tags and dangerous content + let sanitized = DOMPurify.sanitize(input, { + ALLOWED_TAGS: [], // No HTML tags allowed + ALLOWED_ATTR: [], + KEEP_CONTENT: true + }); + + // Additional sanitization + sanitized = sanitized + .replace(/javascript:/gi, '') // Remove javascript: protocol + .replace(/on\w+\s*=/gi, '') // Remove event handlers + .replace(/[<>]/g, '') // Remove angle brackets + .replace(/&#/g, '') // Remove HTML entities + .replace(/\\/g, '\\\\') // Escape backslashes + .replace(/'/g, ''') // Escape single quotes + .replace(/"/g, '"') // Escape double quotes + .trim(); + + return sanitized; +} + +// Sanitize HTML content (for displaying user content safely) +export function sanitizeHTML(html: string): string { + if (typeof html !== 'string') return ''; + + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre'], + ALLOWED_ATTR: ['href', 'target', 'rel'], + ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i, + ADD_ATTR: ['target'], // Allow target attribute + FORBID_TAGS: ['style', 'script', 'iframe', 'form', 'input'], + FORBID_ATTR: ['style', 'onerror', 'onload', 'onclick'] + }); +} + +// Validate and sanitize URLs +export function sanitizeURL(url: string): string | null { + if (typeof url !== 'string') return null; + + try { + const parsed = new URL(url); + + // Only allow http and https protocols + if (!['http:', 'https:'].includes(parsed.protocol)) { + return null; + } + + // Prevent javascript: and data: URLs + if (url.match(/^(javascript|data|vbscript|file):/i)) { + return null; + } + + return parsed.href; + } catch { + return null; + } +} + +// Escape for SQL (though you should use parameterized queries) +export function escapeSQLString(str: string): string { + if (typeof str !== 'string') return ''; + + return str + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\x00/g, '\\0') + .replace(/\x1a/g, '\\Z'); +} + +// Validate email more securely +export function isValidEmail(email: string): boolean { + if (typeof email !== 'string') return false; + + // More comprehensive email regex + const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + + return emailRegex.test(email) && email.length <= 254; +} + +// Validate username more securely +export function isValidUsername(username: string): boolean { + if (typeof username !== 'string') return false; + + // Only allow alphanumeric, underscore, and hyphen + const usernameRegex = /^[a-zA-Z0-9_-]{3,30}$/; + + // Check for common SQL injection patterns + const sqlPatterns = /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE)\b)|(-{2})|\/\*|\*\/|;/i; + + return usernameRegex.test(username) && !sqlPatterns.test(username); +} \ No newline at end of file diff --git a/lib/security/secure-token.ts b/lib/security/secure-token.ts new file mode 100644 index 0000000..5e71a0d --- /dev/null +++ b/lib/security/secure-token.ts @@ -0,0 +1,74 @@ +// lib/secure-token.ts +import crypto from 'crypto'; + +const TOKEN_SECRET = process.env.ADMIN_TOKEN_SECRET || 'default-secret-change-this'; +const TOKEN_EXPIRY = 60 * 60 * 1000; // 1 hour + +interface TokenPayload { + userId: string; + isAdmin: boolean; + timestamp: number; +} + +export function createSecureAdminToken(userId: string): string { + const payload: TokenPayload = { + userId, + isAdmin: true, + timestamp: Date.now() + }; + + const payloadString = JSON.stringify(payload); + const signature = crypto + .createHmac('sha256', TOKEN_SECRET) + .update(payloadString) + .digest('hex'); + + const token = Buffer.from(JSON.stringify({ + payload: payloadString, + signature + })).toString('base64'); + + return token; +} + +export function verifySecureAdminToken(token: string): { + isValid: boolean; + userId?: string; + error?: string; +} { + try { + const decoded = Buffer.from(token, 'base64').toString('utf-8'); + const { payload: payloadString, signature } = JSON.parse(decoded); + + // Verify signature + const expectedSignature = crypto + .createHmac('sha256', TOKEN_SECRET) + .update(payloadString) + .digest('hex'); + + if (!crypto.timingSafeEqual( + Buffer.from(signature, 'hex'), + Buffer.from(expectedSignature, 'hex') + )) { + return { isValid: false, error: 'Invalid signature' }; + } + + // Parse and validate payload + const payload: TokenPayload = JSON.parse(payloadString); + + // Check expiry + const tokenAge = Date.now() - payload.timestamp; + if (tokenAge > TOKEN_EXPIRY) { + return { isValid: false, error: 'Token expired' }; + } + + // Check admin flag + if (!payload.isAdmin) { + return { isValid: false, error: 'Not an admin token' }; + } + + return { isValid: true, userId: payload.userId }; + } catch (error) { + return { isValid: false, error: 'Invalid token format' }; + } +} \ No newline at end of file diff --git a/lib/security/security-headers.ts b/lib/security/security-headers.ts new file mode 100644 index 0000000..9876d46 --- /dev/null +++ b/lib/security/security-headers.ts @@ -0,0 +1,137 @@ +// lib/security-headers.ts +import crypto from 'crypto'; +import { NextRequest, NextResponse } from 'next/server'; + +// Generate nonce for CSP +export function generateNonce(): string { + return crypto.randomBytes(16).toString('base64'); +} + +// Security headers configuration +export function applySecurityHeaders( + response: NextResponse, + request: NextRequest, + nonce?: string +): NextResponse { + // Generate CSP nonce if not provided + const cspNonce = nonce || generateNonce(); + + // Content Security Policy - Strict + const csp = [ + "default-src 'self'", + `script-src 'self' 'nonce-${cspNonce}' https://cdnjs.cloudflare.com https://va.vercel-scripts.com`, + "style-src 'self' 'unsafe-inline'", // Unfortunately needed for Tailwind + "img-src 'self' data: https: blob:", + "font-src 'self' data:", + "connect-src 'self' https://api.github.com https://*.supabase.co wss://*.supabase.co https://vitals.vercel-insights.com", + "frame-src 'none'", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "upgrade-insecure-requests", + "block-all-mixed-content", + "manifest-src 'self'" + ].join('; '); + + response.headers.set('Content-Security-Policy', csp); + + // Security headers + response.headers.set('X-Frame-Options', 'DENY'); + response.headers.set('X-Content-Type-Options', 'nosniff'); + response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + response.headers.set('X-XSS-Protection', '0'); // Disabled in favor of CSP + response.headers.set('X-DNS-Prefetch-Control', 'off'); + response.headers.set('X-Download-Options', 'noopen'); + response.headers.set('X-Permitted-Cross-Domain-Policies', 'none'); + + // Strict Transport Security (HSTS) + if (process.env.NODE_ENV === 'production') { + response.headers.set( + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains; preload' + ); + } + + // Permissions Policy (formerly Feature Policy) + response.headers.set( + 'Permissions-Policy', + 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), accelerometer=(), gyroscope=()' + ); + + // Remove server information + response.headers.delete('Server'); + response.headers.delete('X-Powered-By'); + + // Add CSP nonce to response for use in scripts + response.headers.set('X-CSP-Nonce', cspNonce); + + return response; +} + +// Middleware configuration for specific routes +export function getSecurityHeadersForRoute(pathname: string): Record { + const baseHeaders = { + 'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'X-XSS-Protection': '0' + }; + + // API routes - additional restrictions + if (pathname.startsWith('/api/')) { + return { + ...baseHeaders, + 'Content-Type': 'application/json', + 'X-Content-Type-Options': 'nosniff', + 'Cache-Control': 'no-store, no-cache, must-revalidate, private' + }; + } + + // Admin routes - extra security + if (pathname.startsWith('/admin/')) { + return { + ...baseHeaders, + 'Cache-Control': 'no-store, no-cache, must-revalidate, private', + 'X-Robots-Tag': 'noindex, nofollow' + }; + } + + return baseHeaders; +} + +// React component to inject CSP nonce +export function SecurityHeaders({ nonce }: { nonce: string }) { + return ( +