Harden authentication and writable endpoints
This commit is contained in:
parent
c034824338
commit
0a7ee5ff35
33 changed files with 308 additions and 284 deletions
|
|
@ -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
34
app/lib/rate-limit.ts
Normal 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;
|
||||
}
|
||||
28
app/lib/upload-security.ts
Normal file
28
app/lib/upload-security.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue