CVE-2026-45230 : DumbAssets, supprimer n'importe quel fichier du serveur

CVE-2026-45230

Path traversal non authentifié dans /api/delete-file de DumbAssets : un ../ suffit pour effacer server.js.

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