Harden authentication and writable endpoints

This commit is contained in:
makearmy 2026-07-09 22:10:26 -04:00
parent c034824338
commit 0a7ee5ff35
33 changed files with 308 additions and 284 deletions

View file

@ -11,7 +11,7 @@ const PROJECTS_COLLECTION = process.env.DIRECTUS_PROJECTS_COLLECTION || "project
if (!BASE) console.warn("[directus] Missing DIRECTUS_URL");
if (!TOKEN_ADMIN_REGISTER)
console.warn("[directus] Missing DIRECTUS_TOKEN_ADMIN_REGISTER (used for registration)");
console.warn("[directus] Missing DIRECTUS_TOKEN_ADMIN_REGISTER (used for registration and username login)");
export function bytesFromMB(mb: number) {
return Math.round(mb * 1024 * 1024);

34
app/lib/rate-limit.ts Normal file
View file

@ -0,0 +1,34 @@
type Bucket = { count: number; resetAt: number };
const buckets = new Map<string, Bucket>();
const MAX_BUCKETS = 10_000;
export function requestIp(req: Request): string {
// The deployment binds Next.js to localhost behind a reverse proxy. The
// proxy must replace client-supplied forwarding headers for this to be safe.
return (
req.headers.get("x-real-ip") ||
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
"unknown"
);
}
export function rateLimit(key: string, max: number, windowMs: number): boolean {
const now = Date.now();
const current = buckets.get(key);
if (!current || current.resetAt <= now) {
if (buckets.size >= MAX_BUCKETS) {
for (const [bucketKey, bucket] of buckets) {
if (bucket.resetAt <= now) buckets.delete(bucketKey);
}
if (buckets.size >= MAX_BUCKETS) buckets.delete(buckets.keys().next().value as string);
}
buckets.set(key, { count: 1, resetAt: now + windowMs });
return true;
}
if (current.count >= max) return false;
current.count += 1;
return true;
}

View file

@ -0,0 +1,28 @@
import path from "path";
const EXECUTABLE_EXTENSIONS = new Set([
".bat", ".cmd", ".com", ".dll", ".exe", ".hta", ".htm", ".html",
".jar", ".js", ".jsp", ".mjs", ".cjs", ".msi", ".php", ".phtml",
".phar", ".ps1", ".sh", ".vbs", ".war", ".wsf",
]);
export function isExecutableAttachment(filename: string): boolean {
return EXECUTABLE_EXTENSIONS.has(path.extname(filename).toLowerCase());
}
export async function hasValidImageSignature(file: Blob): Promise<boolean> {
const bytes = new Uint8Array(await file.slice(0, 16).arrayBuffer());
if (bytes.length < 3) return false;
const jpeg = bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
const png = bytes.length >= 8 &&
bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a;
const gifHeader = bytes.length >= 6 ? String.fromCharCode(...bytes.slice(0, 6)) : "";
const gif = gifHeader === "GIF87a" || gifHeader === "GIF89a";
const webp = bytes.length >= 12 &&
String.fromCharCode(...bytes.slice(0, 4)) === "RIFF" &&
String.fromCharCode(...bytes.slice(8, 12)) === "WEBP";
return jpeg || png || gif || webp;
}