53 lines
1.8 KiB
TypeScript
53 lines
1.8 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
export async function GET(
|
|
req: NextRequest,
|
|
{ params }: { params: { filename: string } }
|
|
) {
|
|
try {
|
|
const rawFilename = params.filename;
|
|
|
|
// 1. Strict filename validation: alphanumeric + hyphens/underscores + safe image extensions only
|
|
const validFilenameRegex = /^[a-zA-Z0-9_-]+\.(jpg|jpeg|png|webp)$/i;
|
|
if (!rawFilename || !validFilenameRegex.test(rawFilename)) {
|
|
return new NextResponse("Bad Request: Invalid or disallowed image filename", { status: 400 });
|
|
}
|
|
|
|
const filename = path.basename(rawFilename);
|
|
const imagesDir = path.resolve(process.cwd(), "data", "images");
|
|
const filePath = path.resolve(imagesDir, filename);
|
|
|
|
// 2. Prevent path traversal outside data/images directory
|
|
if (!filePath.startsWith(imagesDir)) {
|
|
return new NextResponse("Access Denied", { status: 403 });
|
|
}
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
return new NextResponse("Image Not Found", { status: 404 });
|
|
}
|
|
|
|
const fileBuffer = fs.readFileSync(filePath);
|
|
const ext = path.extname(filename).toLowerCase();
|
|
|
|
let contentType = "image/png";
|
|
if (ext === ".jpg" || ext === ".jpeg") contentType = "image/jpeg";
|
|
else if (ext === ".webp") contentType = "image/webp";
|
|
|
|
return new NextResponse(fileBuffer, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": contentType,
|
|
"X-Content-Type-Options": "nosniff",
|
|
"Content-Security-Policy": "default-src 'none'; sandbox",
|
|
"Content-Disposition": `inline; filename="${filename}"`,
|
|
"Cache-Control": "public, max-age=31536000, immutable",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("[API Image Serve] Error reading file:", error);
|
|
return new NextResponse("Server Error", { status: 500 });
|
|
}
|
|
}
|