Security audit improvements: authentication, path traversal protection, file validation, rate limiting, security headers
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { requireAdminAuth } from '@/lib/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -63,6 +64,10 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { puzzleId } = await request.json();
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { rateLimit } from '@/lib/rateLimit';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Rate limiting: 5 login attempts per minute
|
||||
const rateLimitError = rateLimit(request, { windowMs: 60000, maxRequests: 5 });
|
||||
if (rateLimitError) return rateLimitError;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { password } = await request.json();
|
||||
// Default is hash for 'admin123'
|
||||
|
||||
@@ -8,8 +8,28 @@ export async function GET(
|
||||
) {
|
||||
try {
|
||||
const { filename } = await params;
|
||||
|
||||
// Security: Prevent path traversal attacks
|
||||
// Only allow alphanumeric, hyphens, underscores, and dots
|
||||
const safeFilenamePattern = /^[a-zA-Z0-9_\-\.]+\.mp3$/;
|
||||
if (!safeFilenamePattern.test(filename)) {
|
||||
return new NextResponse('Invalid filename', { status: 400 });
|
||||
}
|
||||
|
||||
// Additional check: ensure no path separators
|
||||
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
||||
return new NextResponse('Invalid filename', { status: 400 });
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), 'public/uploads', filename);
|
||||
|
||||
// Security: Verify the resolved path is still within uploads directory
|
||||
const uploadsDir = path.join(process.cwd(), 'public/uploads');
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
if (!resolvedPath.startsWith(uploadsDir)) {
|
||||
return new NextResponse('Forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await stat(filePath);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { requireAdminAuth } from '@/lib/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -16,6 +17,10 @@ interface CategorizeResult {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
if (!OPENROUTER_API_KEY) {
|
||||
return Response.json(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { requireAdminAuth } from '@/lib/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -21,6 +22,10 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { name, subtitle } = await request.json();
|
||||
|
||||
@@ -43,6 +48,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id } = await request.json();
|
||||
|
||||
@@ -62,6 +71,10 @@ export async function DELETE(request: Request) {
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id, name, subtitle } = await request.json();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { writeFile, unlink } from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { parseBuffer } from 'music-metadata';
|
||||
import { isDuplicateSong } from '@/lib/fuzzyMatch';
|
||||
import { requireAdminAuth } from '@/lib/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -42,6 +43,10 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
@@ -52,6 +57,29 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Security: Validate file size (max 50MB)
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return NextResponse.json({
|
||||
error: `File too large. Maximum size is 50MB, got ${(file.size / 1024 / 1024).toFixed(2)}MB`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Security: Validate MIME type
|
||||
const allowedMimeTypes = ['audio/mpeg', 'audio/mp3'];
|
||||
if (!allowedMimeTypes.includes(file.type)) {
|
||||
return NextResponse.json({
|
||||
error: `Invalid file type. Expected MP3, got ${file.type}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Security: Validate file extension
|
||||
if (!file.name.toLowerCase().endsWith('.mp3')) {
|
||||
return NextResponse.json({
|
||||
error: 'Invalid file extension. Only .mp3 files are allowed'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
// Validate and extract metadata from file
|
||||
@@ -214,6 +242,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id, title, artist, releaseYear, genreIds, specialIds } = await request.json();
|
||||
|
||||
@@ -289,6 +321,10 @@ export async function PUT(request: Request) {
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id } = await request.json();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PrismaClient, Special } from '@prisma/client';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { requireAdminAuth } from '@/lib/auth';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -16,6 +17,10 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
const { name, subtitle, maxAttempts = 7, unlockSteps = '[2,4,7,11,16,30,60]', launchDate, endDate, curator } = await request.json();
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||
@@ -35,6 +40,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
const { id } = await request.json();
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
@@ -44,6 +53,10 @@ export async function DELETE(request: Request) {
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
// Check authentication
|
||||
const authError = await requireAdminAuth(request as any);
|
||||
if (authError) return authError;
|
||||
|
||||
const { id, name, subtitle, maxAttempts, unlockSteps, launchDate, endDate, curator } = await request.json();
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
|
||||
Reference in New Issue
Block a user