Summary
The library's internal default filename generator correctly uses crypto.randomBytes(16) for cryptographically secure filenames (in storage/disk.js:6-9):
function getFilename (req, file, cb) {
crypto.randomBytes(16, function (err, raw) {
cb(err, err ? undefined : raw.toString('hex'))
})
}
However, the README's custom diskStorage example teaches a pattern using Math.random():
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
cb(null, file.fieldname + '-' + uniqueSuffix)
}
This is the example developers copy when they need custom filenames (e.g., preserving file extensions, adding field-based prefixes) — which is almost every production use case.
Why this matters
-
Math.random() is not cryptographically secure. V8's xorshift128+ PRNG state can be recovered from a small number of outputs (reference). Math.round(Math.random() * 1E9) provides ~30 bits of entropy per filename.
-
Date.now() narrows the window. If an attacker knows approximately when a file was uploaded (within ~1 second), Date.now() contributes ~0 bits of additional unpredictability, leaving only the Math.random() component.
-
Filename enumeration enables unauthorized access. If uploads are served from a web-accessible directory — a common Express pattern — an attacker can enumerate possible filenames to access other users' uploads without authorization.
The library already knows the right way to do this (its own default uses crypto.randomBytes). The README just teaches the wrong pattern.
Reproduction
// README pattern: predictable
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
// Example output: "avatar-1711817423456-482917364"
// Attacker who knows the ~1s upload window only needs to try ~1B values
// Library default: secure
const crypto = require('crypto')
crypto.randomBytes(16, (err, raw) => {
console.log(raw.toString('hex'))
// Example: "a3f8c2e91b7d40f5e6a2c8d3b1e7f9a0"
// 128 bits of entropy — infeasible to enumerate
})
Suggested fix
Update the README example to match the library's own approach:
const crypto = require('crypto')
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '/tmp/my-uploads')
},
filename: function (req, file, cb) {
crypto.randomBytes(16, function (err, raw) {
if (err) return cb(err)
cb(null, file.fieldname + '-' + raw.toString('hex'))
})
}
})
This is a one-line change with zero API impact — the filename callback already supports async via the callback pattern.
Impact assessment
| Factor |
Detail |
| Severity |
Medium — default is secure; only affects custom diskStorage users |
| Scope |
Broad — the README is the canonical reference for custom filenames; this pattern is widely copied in tutorials and Stack Overflow answers |
| Fix effort |
Minimal — README-only change, no code modification needed |
Secondary note: examples missing file size limits
The basic examples all use multer({ dest: 'uploads/' }) without setting limits.fileSize. While the README's limits section mentions DoS protection, none of the example code demonstrates it. Adding limits: { fileSize: 5 * 1024 * 1024 } (or similar) to at least one example would help developers remember to set resource limits.
Summary
The library's internal default filename generator correctly uses
crypto.randomBytes(16)for cryptographically secure filenames (instorage/disk.js:6-9):However, the README's custom
diskStorageexample teaches a pattern usingMath.random():This is the example developers copy when they need custom filenames (e.g., preserving file extensions, adding field-based prefixes) — which is almost every production use case.
Why this matters
Math.random()is not cryptographically secure. V8's xorshift128+ PRNG state can be recovered from a small number of outputs (reference).Math.round(Math.random() * 1E9)provides ~30 bits of entropy per filename.Date.now()narrows the window. If an attacker knows approximately when a file was uploaded (within ~1 second),Date.now()contributes ~0 bits of additional unpredictability, leaving only theMath.random()component.Filename enumeration enables unauthorized access. If uploads are served from a web-accessible directory — a common Express pattern — an attacker can enumerate possible filenames to access other users' uploads without authorization.
The library already knows the right way to do this (its own default uses
crypto.randomBytes). The README just teaches the wrong pattern.Reproduction
Suggested fix
Update the README example to match the library's own approach:
This is a one-line change with zero API impact — the filename callback already supports async via the callback pattern.
Impact assessment
Secondary note: examples missing file size limits
The basic examples all use
multer({ dest: 'uploads/' })without settinglimits.fileSize. While the README's limits section mentions DoS protection, none of the example code demonstrates it. Addinglimits: { fileSize: 5 * 1024 * 1024 }(or similar) to at least one example would help developers remember to set resource limits.