agentggagentgg
Back to all findings
HIGHconfirmedfile-upload-validationinsufficient-upload-validationc6f571d03021

multer uploadToDisk has no fileSize limit and writes to a publicly served directory

The `uploadToDisk` multer instance used by `POST /rest/memories` enforces no `limits.fileSize` and persists files into the web-served static assets directory, allowing disk-exhaustion DoS by uploading arbitrarily large files.

Fileserver.ts
Lines431451
Confidence
70%
File statusvalidated
Details

At the bottom of server.ts the uploadToDisk multer instance is configured without any limits option:

const uploadToDisk = multer({
  storage: multer.diskStorage({
    destination: (req: Request, file: any, cb: any) => {
      const isValid = mimeTypeMap[file.mimetype]
      let error: Error | null = new Error('Invalid mime type')
      if (isValid) {
        error = null
      }
      cb(error, path.resolve('frontend/dist/frontend/assets/public/images/uploads/'))
    },
    filename: (req: Request, file: any, cb: any) => {
      const name = security.sanitizeFilename(file.originalname)
        .toLowerCase()
        .split(' ')
        .join('-')
      const ext = mimeTypeMap[file.mimetype]
      cb(null, name + '-' + Date.now() + '.' + ext)
    }
  })
})

This instance is wired into the authenticated /rest/memories route:

app.post('/rest/memories', uploadToDisk.single('image'), ensureFileIsPassed, security.appendUserId(), metrics.observeFileUploadMetricsMiddleware(), utils.asyncHandler(addMemory()))

Problems with this configuration relative to the checklist:

  1. No per-file or per-request size limit. There is no limits: { fileSize: ... } option. Multer's default is unbounded, so an authenticated user can stream gigabyte-scale uploads that are written to local disk under frontend/dist/frontend/assets/public/images/uploads/, filling the volume and producing a denial of service.
  2. MIME allowlist is based on the client-supplied Content-Type header. The check mimeTypeMap[file.mimetype] consults the attacker-controlled multipart header rather than performing a magic-number check (e.g. file-type). An attacker can claim image/png while uploading arbitrary bytes.
  3. Files are persisted to a directory that is served as static assets. The destination resolves under frontend/dist/frontend/assets/public/images/uploads/, which is exposed via app.use(express.static(path.resolve('frontend/dist/frontend'))). Even though the saved filename receives a .png/.jpg extension derived from the MIME map (so RCE-style double-extension or .php uploads are mitigated), arbitrary content (e.g. SVG-shaped or HTML-shaped bytes) is still reachable via a predictable URL.

The forced extension and Date.now() suffix reduce the path-traversal/RCE blast radius, so the primary impact is the missing size limit causing disk-fill DoS on a host-served path.

Proof of concept
  1. Authenticate to the Juice Shop instance to obtain a JWT.
  2. Send a multipart POST whose body is a very large random payload declared as image/png:
POST /rest/memories HTTP/1.1
Host: target
Authorization: Bearer <jwt>
Content-Type: multipart/form-data; boundary=X

--X
Content-Disposition: form-data; name="caption"

hi
--X
Content-Disposition: form-data; name="image"; filename="big.png"
Content-Type: image/png

<...many GB of arbitrary bytes...>
--X--

Because uploadToDisk has no limits.fileSize, multer streams the entire body to frontend/dist/frontend/assets/public/images/uploads/big-<timestamp>.png. Repeating this exhausts disk space, taking the server offline. The same request also demonstrates that the MIME allowlist is bypassable by simply setting Content-Type: image/png while the body is not actually a PNG.

Impact

Any authenticated user can upload arbitrarily large files to the server's local disk under the web-served static assets directory. Repeated uploads exhaust disk space, causing a denial of service for the Juice Shop process and any other service sharing the volume (logs, sqlite, etc.). Additionally, the MIME allowlist is enforced only against the client-supplied header, so arbitrary content can be stored at a predictable, publicly fetchable URL under /assets/public/images/uploads/ — useful as an attacker-controlled hosting surface even though the file extension is forced to png/jpg.

Validation
confirmed

The uploadToDisk = multer({ storage: multer.diskStorage({...}) }) definition omits limits.fileSize entirely (compare to uploadToMemory two lines above which sets limits: { fileSize: 200000 }), and its destination frontend/dist/frontend/assets/public/images/uploads/ is exposed via app.use(express.static(path.resolve('frontend/dist/frontend'))). The /rest/memories route streams the body to disk in uploadToDisk.single('image') before any auth check, enabling disk-exhaustion DoS as described. MIME validation is also limited to the client-controlled file.mimetype header, confirming the secondary issue.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H
Base score: 7.1 · HIGH

The sink is reached via an unauthenticated-from-network HTTP POST to /rest/memories, which goes through verify.jwtChallenges() and security.appendUserId() — the latter expects a JWT, so a basic authenticated user is required (PR:L), matching the PoC's "Authenticate to obtain a JWT" step. The uploadToDisk multer instance defined at server.ts lines 431–451 has no limits.fileSize and streams to local disk, so a single attacker action with no victim interaction can fill the volume, taking the Juice Shop process and co-resident services (logs, sqlite) offline → A:H. The MIME allowlist is enforced only against the attacker-controlled Content-Type header in mimeTypeMap[file.mimetype], letting an attacker stash arbitrary bytes at a predictable, publicly-served URL under /assets/public/images/uploads/, which is a limited integrity impact (I:L) — the forced .png/.jpg extension and Date.now() suffix prevent overwriting existing files or RCE-style double-extension tricks, and no confidential data is disclosed (C:N). Scope stays Unchanged because the disk-fill and content-hosting impact remain in the same web application's security authority.

References