From 7f0b7834e6b5628d56e211c6844af79100119faf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 20:19:00 +0000 Subject: [PATCH 1/2] feat: Add multer to admin DB backup/restore and improve validation Co-authored-by: mathieumoran1 --- README.md | 6 +++++ package-lock.json | 68 ++++++++++++++++++++++++++++++++++------------- routes/adminDb.js | 42 ++++++++++++++++++++++++++++- routes/users.js | 20 +++++++++++--- 4 files changed, 114 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index cb6c829..ef70b83 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ npm install node index.js ``` +If you're updating an existing install (older `node_modules`), the admin DB backup/restore feature now uses `multer`, so make sure it's installed: + +```bash +npm install multer +``` + Add this to your `.env` (**required**: `MONGO_URI`, `JWT_SECRET`): ```bash diff --git a/package-lock.json b/package-lock.json index 63f42e5..01819a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,13 +94,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -155,9 +155,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -473,6 +473,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -601,13 +616,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -740,6 +757,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -877,23 +909,23 @@ "license": "MIT" }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, diff --git a/routes/adminDb.js b/routes/adminDb.js index 0a013fa..301b7c3 100644 --- a/routes/adminDb.js +++ b/routes/adminDb.js @@ -6,6 +6,7 @@ const path = require("path"); const zlib = require("zlib"); const readline = require("readline"); const multer = require("multer"); +const { Transform } = require("stream"); // Use MongoDB Extended JSON to preserve ObjectId/Date/etc. // bson is a transitive dependency of mongoose (mongodb driver). @@ -20,6 +21,26 @@ const router = express.Router(); const BACKUP_DIR = path.join(process.cwd(), "backups"); const MAX_UPLOAD_BYTES = 250 * 1024 * 1024; // 250MB +// SECURITY: protect against gzip "zip bombs" (small upload -> massive output). +// This caps the *decompressed* stream size during restore. +const MAX_RESTORE_UNZIPPED_BYTES = 1024 * 1024 * 1024; // 1GB + +function createByteLimitStream(maxBytes) { + let seen = 0; + return new Transform({ + transform(chunk, _enc, cb) { + seen += chunk.length; + if (seen > maxBytes) { + const err = new Error("Restore failed: decompressed data is too large"); + // mimic our handler's error shape + err.statusCode = 413; + cb(err); + return; + } + cb(null, chunk); + }, + }); +} function requireAdmin(req) { const token = req.headers.authorization?.split(" ")[1]; @@ -251,12 +272,28 @@ router.post("/db/restore", upload.single("backup"), async (req, res) => { return res.status(400).json({ message: "No backup file uploaded" }); } uploadedPath = file.path; + // Best-effort: refuse obviously wrong file names (UI expects .ndjson.gz). + const original = String(file.originalname || "").trim().toLowerCase(); + if (original && !original.endsWith(".gz")) { + const err = new Error("Invalid backup file (expected a .gz file)"); + err.statusCode = 400; + throw err; + } const db = mongoose.connection.db; const gunzip = zlib.createGunzip(); - const input = fs.createReadStream(uploadedPath).pipe(gunzip); + const byteLimit = createByteLimitStream(MAX_RESTORE_UNZIPPED_BYTES); + const input = fs.createReadStream(uploadedPath).pipe(gunzip).pipe(byteLimit); const rl = readline.createInterface({ input, crlfDelay: Infinity }); + // Ensure stream errors don't become unhandled. + let streamError = null; + input.on("error", (e) => { + streamError = e; + try { + rl.close(); + } catch (_) {} + }); let meta = null; let currentCollection = null; @@ -347,6 +384,9 @@ router.post("/db/restore", upload.single("backup"), async (req, res) => { } await flushBatch(); + if (streamError) { + throw streamError; + } // Cleanup upload after successful restore try { diff --git a/routes/users.js b/routes/users.js index 6486061..21574ce 100644 --- a/routes/users.js +++ b/routes/users.js @@ -169,7 +169,7 @@ router.get("/stats", verifyToken, async (req, res) => { // Register User router.post("/register", async (req, res) => { - const { name, email, password, role, verifoof } = req.body; + const { name, email, password, verifoof } = req.body; const dogCheckEnabled = woof.length > 0; function registerVerification() { @@ -179,6 +179,19 @@ router.post("/register", async (req, res) => { } try { + const nameNorm = typeof name === "string" ? name.trim() : ""; + const emailNorm = typeof email === "string" ? email.trim() : ""; + + if (!nameNorm) { + throw new Error("Le nom est obligatoire"); + } + if (!emailNorm) { + throw new Error("L'email est obligatoire"); + } + // Basic sanity check (not a full RFC validator) + if (!/^\S+@\S+\.\S+$/.test(emailNorm)) { + throw new Error("Email invalide."); + } if (!password) { throw new Error("Le mot de passe est obligatoire et ne peut être indéfini"); } @@ -189,14 +202,15 @@ router.post("/register", async (req, res) => { return res.status(400).json({ message: "Mauvais nom de chien" }); } - const existingUser = await User.findOne({ email }); + const existingUser = await User.findOne({ email: emailNorm }); if (existingUser) { return res.status(409).json({ message: "L'email existe déjà, veuillez en choisir un autre." }); } // Hash password and create user const hashedPassword = await bcrypt.hash(password, 10); - const user = new User({ name, email, password: hashedPassword, role }); + // SECURITY: never accept role escalation from a public endpoint. + const user = new User({ name: nameNorm, email: emailNorm, password: hashedPassword, role: 0 }); await user.save(); res.status(201).json({ message: "L'utilisateur s'est enregistré avec succès !" }); From 20ff670ae2e1ea44fc902ff5c8ed7d332a6b897d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 20:20:07 +0000 Subject: [PATCH 2/2] Remove outdated multer installation instruction Co-authored-by: mathieumoran1 --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index ef70b83..cb6c829 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,6 @@ npm install node index.js ``` -If you're updating an existing install (older `node_modules`), the admin DB backup/restore feature now uses `multer`, so make sure it's installed: - -```bash -npm install multer -``` - Add this to your `.env` (**required**: `MONGO_URI`, `JWT_SECRET`): ```bash