fix(security): authorize site uploads and harden tokens, media and request identity

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

+1 -1
View File
@@ -29,7 +29,7 @@ import {
isValidVerificationToken,
sendVerification,
verificationToken,
} from "./email-verify";
} from "@/lib/auth/email-verification";
beforeEach(() => {
vi.clearAllMocks();
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -3,7 +3,7 @@
import { count, eq } from "drizzle-orm";
import { after } from "next/server";
import { z } from "zod";
import { sendVerification } from "@/actions/email-verify";
import { sendVerification } from "@/lib/auth/email-verification";
import { hashPassword } from "@/lib/auth/password";
import { invalidateKey } from "@/lib/cached-db";
import { db, User } from "@/lib/db";
File diff suppressed because it is too large. Load diff
+15 -22
View File
@@ -3,37 +3,32 @@
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
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 MEDIA_DIR = resolveMediaPath("logo");
export async function saveLogo(
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) return { success: false, error: "No file provided" };
const ext =
file.type === "image/png"
? "png"
: file.type === "image/gif"
? "gif"
: file.type === "image/jpeg"
? "jpg"
: file.type === "image/webp"
? "webp"
: "png";
const upload = await validateSiteImageUpload(
formData instanceof FormData ? formData.get("file") : null,
);
if (!upload.success) return upload;
const ext = upload.extension;
const filename = `logo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
const baseDir = MEDIA_DIR;
const baseDir = resolveMediaPath("logo");
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
@@ -50,10 +45,8 @@ export async function saveLogo(
revalidatePath("/", "layout");
return { success: true, url };
} catch (e) {
return {
success: false,
error: e instanceof Error ? e.message : "Unknown error",
};
} catch {
logger.error("Site logo update failed", { module: "site-images" });
return { success: false, error: "Could not update site logo" };
}
}
+23
View File
@@ -0,0 +1,23 @@
import { beforeEach, expect, it, vi } from "vitest";
const set = vi.hoisted(() => vi.fn());
vi.mock("next/headers", () => ({ cookies: async () => ({ set }) }));
import { setLocaleCookie } from "./set-locale";
beforeEach(() => vi.clearAllMocks());
it.each(["../../private", "xx", "en\r\nSet-Cookie:bad=1", "", "EN", null, 42])(
"rejects an unsupported locale without writing cookies: %s",
async (value) => {
await setLocaleCookie(value as string);
expect(set).not.toHaveBeenCalled();
},
);
it.each(["en", "it", "nl"])("keeps supported locale %s", async (value) => {
await setLocaleCookie(value);
expect(set).toHaveBeenCalledWith(
"NEXT_LOCALE",
value,
expect.objectContaining({ sameSite: "strict", secure: true }),
);
});
+2
View File
@@ -1,8 +1,10 @@
"use server";
import { cookies } from "next/headers";
import { isSupportedLocale } from "@/i18n/locales";
export async function setLocaleCookie(code: string): Promise<void> {
if (typeof code !== "string" || !isSupportedLocale(code)) return;
const store = await cookies();
store.set("NEXT_LOCALE", code, {
path: "/",
File diff suppressed because it is too large. Load diff