Initial push
This commit is contained in:
59
lib/security/rateLimit.ts
Normal file
59
lib/security/rateLimit.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* In-memory rate limiter (per IP, per minute window).
|
||||
* For production at scale, replace with Redis-backed solution.
|
||||
*/
|
||||
|
||||
interface RateLimitEntry {
|
||||
count: number;
|
||||
windowStart: number;
|
||||
}
|
||||
|
||||
const store = new Map<string, RateLimitEntry>();
|
||||
const WINDOW_MS = 60_000; // 1 minute
|
||||
|
||||
// Cleanup stale entries every 5 minutes to prevent memory leaks
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of store.entries()) {
|
||||
if (now - entry.windowStart > WINDOW_MS * 2) {
|
||||
store.delete(key);
|
||||
}
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
export function checkRateLimit(
|
||||
ip: string,
|
||||
limit: number = parseInt(process.env.RATE_LIMIT_RPM ?? "100"),
|
||||
): { allowed: boolean; remaining: number; resetAt: number } {
|
||||
const now = Date.now();
|
||||
const entry = store.get(ip);
|
||||
|
||||
if (!entry || now - entry.windowStart > WINDOW_MS) {
|
||||
store.set(ip, { count: 1, windowStart: now });
|
||||
return { allowed: true, remaining: limit - 1, resetAt: now + WINDOW_MS };
|
||||
}
|
||||
|
||||
if (entry.count >= limit) {
|
||||
return {
|
||||
allowed: false,
|
||||
remaining: 0,
|
||||
resetAt: entry.windowStart + WINDOW_MS,
|
||||
};
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: limit - entry.count,
|
||||
resetAt: entry.windowStart + WINDOW_MS,
|
||||
};
|
||||
}
|
||||
|
||||
/** Extract real IP from request (handles proxies). */
|
||||
export function getClientIp(request: Request): string {
|
||||
return (
|
||||
request.headers.get("x-real-ip") ??
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
60
lib/security/sanitize.ts
Normal file
60
lib/security/sanitize.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Input sanitization utilities.
|
||||
* Prevents XSS, command injection, and path traversal attacks.
|
||||
*/
|
||||
|
||||
/** Shell metacharacters that must not appear in RCON commands sent to the OS. */
|
||||
const SHELL_UNSAFE = /[;&|`$(){}[\]<>\\'"*?!#~]/;
|
||||
|
||||
/**
|
||||
* Validate a Minecraft RCON command.
|
||||
* Commands go to the MC RCON protocol (not a shell), but we still strip
|
||||
* shell metacharacters as a defense-in-depth measure.
|
||||
*/
|
||||
export function sanitizeRconCommand(cmd: string): string {
|
||||
if (typeof cmd !== "string") throw new Error("Command must be a string");
|
||||
const trimmed = cmd.trim();
|
||||
if (trimmed.length === 0) throw new Error("Command cannot be empty");
|
||||
if (trimmed.length > 32767) throw new Error("Command too long");
|
||||
// RCON commands start with / or a bare word — never allow OS-level metacharacters
|
||||
if (SHELL_UNSAFE.test(trimmed)) {
|
||||
throw new Error("Command contains forbidden characters");
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize a file system path relative to a base directory.
|
||||
* Prevents path traversal (e.g. "../../etc/passwd").
|
||||
*/
|
||||
export function sanitizeFilePath(
|
||||
inputPath: string,
|
||||
baseDir: string,
|
||||
): string {
|
||||
const path = require("node:path");
|
||||
const resolved = path.resolve(baseDir, inputPath);
|
||||
if (!resolved.startsWith(path.resolve(baseDir))) {
|
||||
throw new Error("Path traversal detected");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML tags from user-provided strings to prevent stored XSS.
|
||||
* Use this before storing free-text fields in the database.
|
||||
*/
|
||||
export function stripHtml(input: string): string {
|
||||
return input.replace(/<[^>]*>/g, "");
|
||||
}
|
||||
|
||||
/** Validate a Minecraft UUID (8-4-4-4-12 hex). */
|
||||
export function isValidMcUuid(uuid: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
uuid,
|
||||
);
|
||||
}
|
||||
|
||||
/** Validate a Minecraft username (3-16 chars, alphanumeric + underscore). */
|
||||
export function isValidMcUsername(name: string): boolean {
|
||||
return /^[a-zA-Z0-9_]{3,16}$/.test(name);
|
||||
}
|
||||
Reference in New Issue
Block a user