tfrere's picture
tfrere HF Staff
feat(backend): rate-limit /api/upload to prevent anonymous abuse
5576f9a
Raw
History Blame Contribute Delete
2.7 kB
import { Router, type Request, type Response, type NextFunction } from "express";
import { writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { randomUUID } from "crypto";
import multer from "multer";
import { extractToken } from "../auth.js";
import { isHfStorageEnabled, setUserToken, uploadImageToHf } from "../hf-storage.js";
import { getDataDir } from "../utils.js";
// Simple in-memory sliding-window rate limiter.
// The upload route is intentionally not auth-gated (see docs/TESTS.md 4.1.3),
// so we rely on per-IP bucketing to prevent anonymous abuse.
const UPLOAD_WINDOW_MS = 60_000;
const UPLOAD_MAX_PER_WINDOW = 30;
const uploadBuckets = new Map<string, number[]>();
function rateLimitUpload(req: Request, res: Response, next: NextFunction): void {
const key =
(req.headers["x-forwarded-for"] as string | undefined)?.split(",")[0]?.trim() ||
req.ip ||
req.socket.remoteAddress ||
"unknown";
const now = Date.now();
const cutoff = now - UPLOAD_WINDOW_MS;
const bucket = (uploadBuckets.get(key) || []).filter((ts) => ts > cutoff);
if (bucket.length >= UPLOAD_MAX_PER_WINDOW) {
const retryAfter = Math.ceil((bucket[0] + UPLOAD_WINDOW_MS - now) / 1000);
res.setHeader("Retry-After", String(Math.max(1, retryAfter)));
res.status(429).json({ error: "Too many uploads, please slow down." });
return;
}
bucket.push(now);
uploadBuckets.set(key, bucket);
next();
}
export function createUploadRouter(): Router {
const router = Router();
const uploadMiddleware = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
router.post("/api/upload", rateLimitUpload, uploadMiddleware.single("file"), async (req, res) => {
try {
const file = (req as any).file as Express.Multer.File | undefined;
if (!file) {
res.status(400).json({ error: "No file provided" });
return;
}
const ext = file.originalname.split(".").pop() || "png";
const filename = `${randomUUID()}.${ext}`;
const userToken = extractToken(req.headers.cookie) ?? undefined;
if (userToken) setUserToken(userToken);
if (isHfStorageEnabled()) {
const url = await uploadImageToHf(file.buffer, filename, userToken);
res.json({ url });
return;
}
const uploadsDir = join(getDataDir(), "uploads");
mkdirSync(uploadsDir, { recursive: true });
writeFileSync(join(uploadsDir, filename), file.buffer);
res.json({ url: `/uploads/${filename}` });
} catch (err: any) {
console.error("[upload] error:", err);
res.status(500).json({ error: err.message || "Upload failed" });
}
});
return router;
}