DumbAssets est un petit gestionnaire d’inventaire auto-hébergé en Node.js/Express. Quand on retire une pièce jointe d’un asset (photo, reçu, manuel), le front appelle POST /api/delete-file avec le chemin du fichier à effacer. Sauf que ce chemin vient du client et n’est jamais vérifié (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' });
});
});
Normalement filePath ressemble à /Images/photo.jpg. Mais comme path.join normalise les ../, il suffit d’envoyer :
POST /api/delete-file
Content-Type: application/json
{"path": "../server.js"}
…pour qu’absPath pointe un cran au-dessus et que fs.unlink efface server.js. Pareil pour ../package.json, les fichiers de données, etc. : tout ce que le process a le droit de toucher. Et le PIN (DUMBASSETS_PIN) étant désactivé par défaut, aucune authentification n’est demandée.
Le même défaut existe sur un second chemin : lors d’une mise à jour d’asset, le tableau filesToDelete est passé à deleteAssetFiles, qui appelle en boucle deleteAssetFileAsync, même logique, même absence de contrôle :
const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
const fullPath = path.join(DATA_DIR, cleanPath); // toujours vulnérable aux "../"
fs.unlink(fullPath, (err) => { /* ... */ });
Liens
- CVE : CVE-2026-45230
- Dépôt : DumbWareio/DumbAssets
- Advisory : VulnCheck
- Correctif : PR #136