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.
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:
- 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 underfrontend/dist/frontend/assets/public/images/uploads/, filling the volume and producing a denial of service. - MIME allowlist is based on the client-supplied
Content-Typeheader. The checkmimeTypeMap[file.mimetype]consults the attacker-controlled multipart header rather than performing a magic-number check (e.g.file-type). An attacker can claimimage/pngwhile uploading arbitrary bytes. - 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 viaapp.use(express.static(path.resolve('frontend/dist/frontend'))). Even though the saved filename receives a.png/.jpgextension derived from the MIME map (so RCE-style double-extension or.phpuploads 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.
- Authenticate to the Juice Shop instance to obtain a JWT.
- 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.
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.
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.
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.
- CWE-434
- CWE-400
- CWE-770
- OWASP A04:2021 Insecure Design