You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Harden the file/media upload path. Book files and thumbnails are accepted through an unconfigured Multer instance (no size cap, no type filter) and streamed to Cloudinary as raw files, and paid book PDFs are then re-proxied through the API server on every read. Add upload constraints, server-side type/size validation, and move to Cloudinary signed/authenticated delivery so paid content isn't just a public secure_url and the API server stops proxying large binaries. Today an authenticated user can upload an arbitrarily large file of any type, and a purchased book's URL, once known, is a bearer link.
Current state
Multer is completely unconstrained: const upload = multer({ storage: multer.memoryStorage() }) — no limits, no fileFilter (src/middlewares/upload.js). Any authenticated caller can POST an arbitrarily large buffer into server memory. Combined with express.json({ limit: "10mb" }) in app.js, the multipart path has no equivalent guard.
createBook (src/controllers/books/bookController.js) uploads req.files.file[0].buffer to Cloudinary with resource_type: "raw" and no validation that it's actually a PDF/allowed type or within a size budget. Thumbnails likewise aren't validated as images. It stores the resulting secure_url on Book.fileUrl.
Paid content protection leaks: streamBookPreview gates access (owner/purchaser/free) then does axios.get(book.fileUrl, { responseType: "stream" }) and pipes the file through the API server on every read. The underlying Cloudinary URL is a public, unsigned, non-expiring link — anyone who obtains it bypasses the gate entirely, and the proxy puts full-file egress on the app server.
Avatar uploads (userController.updateUser) go straight to Cloudinary with a 10s timeout but no type/size validation either.
Cloudinary is configured with cloud_name/api_key/api_secret (src/utils/cloudinary.js), so signed uploads and time-limited signed delivery URLs are available but unused.
What to build
Constrain Multer (src/middlewares/upload.js): add limits (per-file size caps — e.g. images vs raw book files — and field count) and a fileFilter that allowlists MIME types per field (thumbnail: image/*; file: application/pdf/epub). Reject oversized/disallowed uploads with a clean 400 (handle Multer's LIMIT_FILE_SIZE error in the error handler).
Server-side content validation: don't trust the client MIME — sniff the buffer's magic bytes to confirm it's actually the claimed type before uploading to Cloudinary. Reject mismatches.
Signed private delivery for paid content: upload paid book files as authenticated/private Cloudinary resources and generate short-lived signed delivery URLs on demand after the entitlement check, instead of storing a public secure_url and proxying bytes. streamBookPreview (or a new GET /api/books/:id/access-url) returns a time-limited signed URL to the entitled user rather than piping the file through the server. Free books can keep public URLs.
Signed direct uploads (optional but recommended): expose a POST /api/uploads/signature endpoint that returns a Cloudinary upload signature so large files can go browser → Cloudinary directly (matching the pattern courses already use — course media is uploaded client-side and only URLs hit the backend), removing the server-memory buffering for books too.
Consistency: apply the same type/size validation to avatar uploads in userController.updateUser.
Acceptance criteria
Multer enforces per-field size limits and a MIME allowlist; oversized or wrong-type uploads return a clean 400 (not an unhandled 500), including Multer LIMIT_FILE_SIZE.
Upload handlers verify the actual file bytes (magic-number sniff) match the allowed type before sending to Cloudinary; spoofed content types are rejected.
Paid book files are stored as private/authenticated Cloudinary resources; entitled users receive a short-lived signed delivery URL, and the raw file is no longer proxied through the API server on every read (or, if proxied, only via a signed, expiring URL).
A signed-upload-signature endpoint exists (auth-gated) enabling direct browser→Cloudinary uploads for large files.
Avatar uploads enforce image-only type and a size cap.
Jest + supertest tests cover: rejection of oversized and wrong-type uploads, magic-byte mismatch rejection, and that a signed access URL is only issued to an entitled user (unentitled → 403).
Pointers
src/middlewares/upload.js (unconstrained Multer), src/controllers/books/bookController.js (createBook raw upload, streamBookPreview proxy + public fileUrl), src/controllers/userController.js (updateUser avatar upload), src/utils/cloudinary.js (configured client — signed upload/delivery available), src/models/Book.js (fileUrl/image), src/middlewares/errorHandler.js (add Multer error mapping), app.js (express.json limit for reference).
Gotchas: resource_type: "raw" Cloudinary URLs are public by default — private delivery requires type: "authenticated"/"private" plus signed URLs. Course media already flows browser→Cloudinary (backend gets only URLs); books can follow the same pattern. CI runs node --check on all files and boots the server, so keep the middleware import-safe. PRs target dev.
Difficulty
Medium — no protocol work, but it combines upload validation (limits, MIME allowlist, magic-byte sniffing), a shift to signed/private Cloudinary delivery for paid content, and a direct-upload signing endpoint, with error-handling and entitlement care.
🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the dev branch. Quality bar: CI must stay green.
💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0
Summary
Harden the file/media upload path. Book files and thumbnails are accepted through an unconfigured Multer instance (no size cap, no type filter) and streamed to Cloudinary as raw files, and paid book PDFs are then re-proxied through the API server on every read. Add upload constraints, server-side type/size validation, and move to Cloudinary signed/authenticated delivery so paid content isn't just a public secure_url and the API server stops proxying large binaries. Today an authenticated user can upload an arbitrarily large file of any type, and a purchased book's URL, once known, is a bearer link.
Current state
const upload = multer({ storage: multer.memoryStorage() })— nolimits, nofileFilter(src/middlewares/upload.js). Any authenticated caller can POST an arbitrarily large buffer into server memory. Combined withexpress.json({ limit: "10mb" })inapp.js, the multipart path has no equivalent guard.createBook(src/controllers/books/bookController.js) uploadsreq.files.file[0].bufferto Cloudinary withresource_type: "raw"and no validation that it's actually a PDF/allowed type or within a size budget. Thumbnails likewise aren't validated as images. It stores the resultingsecure_urlonBook.fileUrl.streamBookPreviewgates access (owner/purchaser/free) then doesaxios.get(book.fileUrl, { responseType: "stream" })and pipes the file through the API server on every read. The underlying Cloudinary URL is a public, unsigned, non-expiring link — anyone who obtains it bypasses the gate entirely, and the proxy puts full-file egress on the app server.userController.updateUser) go straight to Cloudinary with a 10s timeout but no type/size validation either.cloud_name/api_key/api_secret(src/utils/cloudinary.js), so signed uploads and time-limited signed delivery URLs are available but unused.What to build
src/middlewares/upload.js): addlimits(per-file size caps — e.g. images vs raw book files — and field count) and afileFilterthat allowlists MIME types per field (thumbnail: image/*;file: application/pdf/epub). Reject oversized/disallowed uploads with a clean400(handle Multer'sLIMIT_FILE_SIZEerror in the error handler).secure_urland proxying bytes.streamBookPreview(or a newGET /api/books/:id/access-url) returns a time-limited signed URL to the entitled user rather than piping the file through the server. Free books can keep public URLs.POST /api/uploads/signatureendpoint that returns a Cloudinary upload signature so large files can go browser → Cloudinary directly (matching the pattern courses already use — course media is uploaded client-side and only URLs hit the backend), removing the server-memory buffering for books too.userController.updateUser.Acceptance criteria
400(not an unhandled 500), including MulterLIMIT_FILE_SIZE.403).Pointers
src/middlewares/upload.js(unconstrained Multer),src/controllers/books/bookController.js(createBookraw upload,streamBookPreviewproxy + publicfileUrl),src/controllers/userController.js(updateUseravatar upload),src/utils/cloudinary.js(configured client — signed upload/delivery available),src/models/Book.js(fileUrl/image),src/middlewares/errorHandler.js(add Multer error mapping),app.js(express.jsonlimit for reference).DELETE /api/books/:id), [Enhancement] Protect unauthenticated Stellar wallet lookup/enumeration endpoints #10 (wallet endpoint auth). No existing issue covers upload hardening or signed media delivery.resource_type: "raw"Cloudinary URLs are public by default — private delivery requirestype: "authenticated"/"private"plus signed URLs. Course media already flows browser→Cloudinary (backend gets only URLs); books can follow the same pattern. CI runsnode --checkon all files and boots the server, so keep the middleware import-safe. PRs targetdev.Difficulty
Medium — no protocol work, but it combines upload validation (limits, MIME allowlist, magic-byte sniffing), a shift to signed/private Cloudinary delivery for paid content, and a direct-upload signing endpoint, with error-handling and entitlement care.
🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the
devbranch. Quality bar: CI must stay green.💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0