From c85ba24424877e98b7c2b2a68aa35ea80ed3f9d6 Mon Sep 17 00:00:00 2001 From: harrydbarnes <145344818+harrydbarnes@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:06:50 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20Fi?= =?UTF-8?q?x=20Path=20Traversal=20and=20Prototype=20Pollution=20vulnerabil?= =?UTF-8?q?ities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolved a Path Traversal issue in `server.js` static file server. The endpoint could be bypassed by using URL encoded paths (`%2e%2e`). The path is now properly decoded and resolved against `DIST_DIR`. - Resolved a Prototype Pollution issue in the `/sonos-store` endpoint in `server.js` and `vite.config.js`. Incoming JSON updates were merged via object spread `{ ...existing, ...updates }`. It now explicitly filters `__proto__`, `constructor`, and `prototype` keys from untrusted configurations. --- server.js | 23 +++++++++++++++++++---- vite.config.js | 11 ++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 4a19321..563ed2c 100644 --- a/server.js +++ b/server.js @@ -38,11 +38,17 @@ const MIME = { // ── Static file server with SPA fallback ────────────────────────────────────── async function serveStatic(req, res) { - const urlPath = req.url.split('?')[0] - const filePath = path.join(DIST_DIR, urlPath) + let urlPath = req.url.split('?')[0] + try { + urlPath = decodeURIComponent(urlPath) + } catch { + // ignore invalid encoding + } + const resolvedDist = path.resolve(DIST_DIR) + const filePath = path.resolve(DIST_DIR, '.' + urlPath) // Security: prevent path traversal outside dist/ - if (!filePath.startsWith(DIST_DIR)) { + if (!filePath.startsWith(resolvedDist + path.sep) && filePath !== resolvedDist) { res.statusCode = 403 res.end('Forbidden') return @@ -103,9 +109,18 @@ function handleStore(req, res) { const existing = await fs.promises.readFile(DATA_FILE, 'utf8') .then(raw => JSON.parse(raw)) .catch(() => ({})) + + // Security: Prevent prototype pollution + for (const key in updates) { + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue + } + existing[key] = updates[key] + } + await fs.promises.writeFile( DATA_FILE, - JSON.stringify({ ...existing, ...updates }, null, 2), + JSON.stringify(existing, null, 2), ) res.statusCode = 200 res.setHeader('Content-Type', 'application/json') diff --git a/vite.config.js b/vite.config.js index d8269ea..b03ecd8 100644 --- a/vite.config.js +++ b/vite.config.js @@ -52,7 +52,16 @@ function sonosStorePlugin() { const existing = await fs.promises.readFile(DATA_FILE, 'utf8') .then(raw => JSON.parse(raw)) .catch(() => ({})) - await fs.promises.writeFile(DATA_FILE, JSON.stringify({ ...existing, ...updates }, null, 2)) + + // Security: Prevent prototype pollution + for (const key in updates) { + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue + } + existing[key] = updates[key] + } + + await fs.promises.writeFile(DATA_FILE, JSON.stringify(existing, null, 2)) res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ ok: true }))