Resolves three SonarQube security hotspots in backend/src/routes/admin.ts:
Content length limit (x2) — multer() was configured with no limits, so uploads were unbounded on both the diskStorage and multer() calls.
Pseudorandom number generator — stored filenames were built from Date.now() plus Math.round(Math.random() * 1e6).
Changes
Cap every dimension of the multipart body: 6 files, 8 MiB per image, 8 fields, 64 KiB per field. Without limits a single request could fill the uploads volume.
Generate stored filenames with crypto.randomUUID() so paths aren't guessable. Image ordering is unaffected — sort_order already drives it.
Wrap the upload middleware as uploadImages, translating MulterError into 413 (oversized file) or 400 (other limit violations) with the { error } JSON shape the rest of the router uses. app.ts mounts no error-handling middleware, so a limit rejection would otherwise have surfaced as an HTML 500.
Verification
npx tsc --noEmit — clean
npm run test:unit — 16/16 pass
Integration tests not run locally (need the Docker Postgres via npm run db:test:up); they contain no admin-upload coverage. CI will run them.
Resolves three SonarQube security hotspots in `backend/src/routes/admin.ts`:
- **Content length limit** (x2) — `multer()` was configured with no `limits`, so uploads were unbounded on both the `diskStorage` and `multer()` calls.
- **Pseudorandom number generator** — stored filenames were built from `Date.now()` plus `Math.round(Math.random() * 1e6)`.
## Changes
- Cap every dimension of the multipart body: 6 files, 8 MiB per image, 8 fields, 64 KiB per field. Without limits a single request could fill the uploads volume.
- Generate stored filenames with `crypto.randomUUID()` so paths aren't guessable. Image ordering is unaffected — `sort_order` already drives it.
- Wrap the upload middleware as `uploadImages`, translating `MulterError` into `413` (oversized file) or `400` (other limit violations) with the `{ error }` JSON shape the rest of the router uses. `app.ts` mounts no error-handling middleware, so a limit rejection would otherwise have surfaced as an HTML 500.
## Verification
- `npx tsc --noEmit` — clean
- `npm run test:unit` — 16/16 pass
- Integration tests not run locally (need the Docker Postgres via `npm run db:test:up`); they contain no admin-upload coverage. CI will run them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
SonarQube flagged three hotspots in the admin router: multer was
configured with no content length limits, and stored filenames were
derived from Date.now() plus Math.random().
- Cap the multipart body on every dimension: 6 files, 8 MiB per image,
8 fields, 64 KiB per field. Without limits a single request could
fill the uploads volume.
- Generate stored filenames with crypto.randomUUID() so paths are not
predictable. Image ordering is unaffected; sort_order already drives it.
- Wrap the upload middleware to translate MulterError into 413/400 JSON.
The app mounts no error handler, so a limit rejection would otherwise
surface as an HTML 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Resolves three SonarQube security hotspots in
backend/src/routes/admin.ts:multer()was configured with nolimits, so uploads were unbounded on both thediskStorageandmulter()calls.Date.now()plusMath.round(Math.random() * 1e6).Changes
crypto.randomUUID()so paths aren't guessable. Image ordering is unaffected —sort_orderalready drives it.uploadImages, translatingMulterErrorinto413(oversized file) or400(other limit violations) with the{ error }JSON shape the rest of the router uses.app.tsmounts no error-handling middleware, so a limit rejection would otherwise have surfaced as an HTML 500.Verification
npx tsc --noEmit— cleannpm run test:unit— 16/16 passnpm run db:test:up); they contain no admin-upload coverage. CI will run them.🤖 Generated with Claude Code