DumbAssets is a small self-hosted inventory manager built on Node.js/Express. When you remove an attachment from an asset (a photo, a receipt, a manual), the front end calls POST /api/delete-file with the path of the file to delete. The catch: that path comes from the client and is never validated (server.js):
app.post('/api/delete-file', (req, res) => {
const { path: filePath } = req.body; // chemin fourni par le client, brut
if (!filePath) return res.status(400).json({ error: 'No file path provided' });
// path.join résout les "../" → rien n'empêche de remonter hors du dossier de l'appli
const absPath = path.join(__dirname, filePath.startsWith('/') ? filePath.substring(1) : filePath);
fs.unlink(absPath, (err) => { // ...et on supprime, sans vérifier le dossier final
if (err) {
if (err.code === 'ENOENT') return res.json({ message: 'File already deleted' });
return res.status(500).json({ error: 'Failed to delete file' });
}
res.json({ message: 'File deleted' });
});
});
Normally filePath looks like /Images/photo.jpg. But since path.join normalizes the ../, all it takes is sending:
POST /api/delete-file
Content-Type: application/json
{"path": "../server.js"}
…for absPath to point one level up and for fs.unlink to wipe server.js. Same story for ../package.json, the data files, and so on: anything the process is allowed to touch. And since the PIN (DUMBASSETS_PIN) is disabled by default, no authentication is required at all.
The same flaw exists on a second path: during an asset update, the filesToDelete array is passed to deleteAssetFiles, which loops over deleteAssetFileAsync, same logic, same missing check:
const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
const fullPath = path.join(DATA_DIR, cleanPath); // toujours vulnérable aux "../"
fs.unlink(fullPath, (err) => { /* ... */ });
Links
- CVE: CVE-2026-45230
- Repository: DumbWareio/DumbAssets
- Advisory: VulnCheck
- Fix: PR #136