fix(security): authorize site uploads and harden tokens, media and request identity
CI / check (push) Successful in 3m15s
CI / deploy (push) Successful in 1m19s
CI / publish-container (push) Successful in 48s

This commit is contained in:
Simo committed 2026-09-13 19:24:43 +02:00
1 parent 52f6d1491f
commit 8abfe352ef
70 files changed
+1609 -204

No files matched your search

+21 -44
View File
@@ -4,53 +4,33 @@ import { mkdir, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { requirePermission } from "@/lib/admin/guard";
import { db, WebsiteSetting } from "@/lib/db";
import { validateSiteImageUpload } from "@/lib/images/site-image-upload";
import { logger } from "@/lib/logger";
import { resolveMediaPath } from "@/lib/media-storage";
import { PERMS } from "@/lib/permissions";
import { siteSettings } from "@/lib/services/site-settings";
const FAVICON_DIR = resolveMediaPath("favicon");
const MAX_SIZE = 2 * 1024 * 1024; // 2MB
const ALLOWED = [
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"image/x-icon",
"image/svg+xml",
];
export async function saveFavicon(
formData: FormData,
): Promise<{ success: boolean; url?: string; error?: string }> {
await requirePermission(PERMS.SETTINGS_EDIT);
try {
const file = formData.get("file") as File | null;
if (!file || file.size === 0)
return { success: false, error: "No file provided" };
if (file.size > MAX_SIZE)
return { success: false, error: "File too large (max 2MB)" };
if (!ALLOWED.includes(file.type))
return {
success: false,
error: "Invalid file type. Allowed: PNG, JPEG, GIF, WebP, ICO, SVG",
};
const mimeExt: Record<string, string> = {
"image/png": "png",
"image/jpeg": "jpg",
"image/gif": "gif",
"image/webp": "webp",
"image/x-icon": "ico",
"image/svg+xml": "svg",
};
const ext = mimeExt[file.type] ?? "png";
const upload = await validateSiteImageUpload(
formData instanceof FormData ? formData.get("file") : null,
{ allowIcon: true },
);
if (!upload.success) return upload;
const ext = upload.extension;
const filename = `favicon-${Date.now()}.${ext}`;
const baseDir = FAVICON_DIR;
const baseDir = resolveMediaPath("favicon");
const filePath = path.resolve(baseDir, filename);
if (!filePath.startsWith(baseDir + path.sep)) {
return { success: false, error: "Invalid path" };
}
const buffer = Buffer.from(await file.arrayBuffer());
const buffer = upload.bytes;
// eslint-disable-next-line security/detect-non-literal-fs-filename
await mkdir(baseDir, { recursive: true });
// eslint-disable-next-line security/detect-non-literal-fs-filename
@@ -85,11 +65,9 @@ export async function saveFavicon(
revalidatePath("/admin/favicon");
return { success: true, url };
} catch (e) {
return {
success: false,
error: e instanceof Error ? e.message : "Unknown error",
};
} catch {
logger.error("Site favicon update failed", { module: "site-images" });
return { success: false, error: "Could not update site favicon" };
}
}
@@ -97,10 +75,11 @@ export async function deleteFavicon(): Promise<{
success: boolean;
error?: string;
}> {
await requirePermission(PERMS.SETTINGS_EDIT);
try {
const oldUrl = await siteSettings.get("cms_favicon");
if (oldUrl?.startsWith("/api/media/favicon/")) {
const baseDir = FAVICON_DIR;
const baseDir = resolveMediaPath("favicon");
const oldName = oldUrl.replace("/api/media/favicon/", "");
if (!oldName.includes("..") && !oldName.includes("/")) {
const oldPath = path.resolve(baseDir, oldName);
@@ -127,10 +106,8 @@ export async function deleteFavicon(): Promise<{
revalidatePath("/admin/favicon");
return { success: true };
} catch (e) {
return {
success: false,
error: e instanceof Error ? e.message : "Unknown error",
};
} catch {
logger.error("Site favicon update failed", { module: "site-images" });
return { success: false, error: "Could not update site favicon" };
}
}