fix(security): authorize site uploads and harden tokens, media and request identity
This commit is contained in:
1 parent
52f6d1491f
commit
8abfe352ef
70 files changed
+1609
-204
No files matched your search
@@ -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
@@ -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";
|
||||
|
||||
+21
-44
File diff suppressed because it is too large.
Load diff
+15
-22
@@ -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" };
|
||||
}
|
||||
}
|
||||
@@ -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 }),
|
||||
);
|
||||
});
|
||||
@@ -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
Reference in new issue
Block a user