From f2b7ff11be838c5393eae0a1fa2154ba1b4e1051 Mon Sep 17 00:00:00 2001 From: "mark.gilbert@prominic.net" Date: Wed, 1 Oct 2025 02:09:17 +0000 Subject: [PATCH] fix: resolve CodeQL security issues for URL redirects and CORS - Add isLocalUrl validation utility to prevent open redirect vulnerabilities - Validate req.originalUrl before directory trailing slash redirects - Validate req.path before legacy endpoint redirects (create-folder, search) - Fix permissive CORS configuration when whitelist is empty - Add security warnings for rejected redirects and CORS misconfigurations Resolves 4 CodeQL security findings: - Server-side URL redirect at routes/fileServer.js:231 - Server-side URL redirect at routes/fileServer.js:1082 - Server-side URL redirect at routes/fileServer.js:1086 - Permissive CORS configuration at app.js:72 --- app.js | 11 +++++++++-- config/paths.js | 9 +++++++++ routes/fileServer.js | 22 +++++++++++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 768d040..c43fd65 100644 --- a/app.js +++ b/app.js @@ -59,13 +59,20 @@ const startServer = async () => { let origin; if (corsConfig.allow_origin === true) { - origin = corsConfig.whitelist; + if (corsConfig.whitelist && corsConfig.whitelist.length > 0) { + origin = corsConfig.whitelist; + } else { + origin = false; + logger.warn( + 'CORS allow_origin is true but whitelist is empty. Blocking all CORS requests for security.' + ); + } } else if (corsConfig.allow_origin === false) { origin = false; } else if (corsConfig.allow_origin === 'specific') { origin = corsConfig.whitelist; } else { - origin = corsConfig.allow_origin; // fallback for other values + origin = corsConfig.allow_origin; } const corsOptions = { diff --git a/config/paths.js b/config/paths.js index c774e22..579059c 100644 --- a/config/paths.js +++ b/config/paths.js @@ -11,3 +11,12 @@ export const getSecurePath = requestPath => { return fullPath; }; + +export const isLocalUrl = urlPath => { + try { + const url = new URL(urlPath, 'https://localhost'); + return url.origin === 'https://localhost'; + } catch { + return false; + } +}; diff --git a/routes/fileServer.js b/routes/fileServer.js index d483853..16e0bd2 100644 --- a/routes/fileServer.js +++ b/routes/fileServer.js @@ -5,7 +5,7 @@ import { join, basename, extname, resolve } from 'path'; import { Op } from 'sequelize'; import auth from 'basic-auth'; import escapeHtml from 'escape-html'; -import { SERVED_DIR, getSecurePath } from '../config/paths.js'; +import { SERVED_DIR, getSecurePath, isLocalUrl } from '../config/paths.js'; import { authenticateDownloads, authenticateUploads, @@ -228,6 +228,12 @@ router.get('*splat', authenticateDownloads, async (req, res) => { const redirectPath = req.originalUrl.endsWith('/') ? req.originalUrl : `${req.originalUrl}/`; + + if (!isLocalUrl(redirectPath)) { + logger.warn('Rejected potentially unsafe redirect', { path: redirectPath }); + return res.status(400).send('Invalid redirect path'); + } + return res.redirect(301, redirectPath); } return handleDirectoryListing(req, res, fullPath, requestPath); @@ -1085,12 +1091,22 @@ router.post('*splat', (req, res, next) => { res.status(200).send('Authenticated'); }); } else if (req.query.action === 'create-folder') { - // Redirect to new endpoint const newPath = `${req.path}/folders`; + + if (!isLocalUrl(newPath)) { + logger.warn('Rejected potentially unsafe redirect', { path: newPath }); + return res.status(400).send('Invalid redirect path'); + } + return res.redirect(307, newPath); } else if (req.query.action === 'search') { - // Redirect to new endpoint const newPath = `${req.path}/search`; + + if (!isLocalUrl(newPath)) { + logger.warn('Rejected potentially unsafe redirect', { path: newPath }); + return res.status(400).send('Invalid redirect path'); + } + return res.redirect(307, newPath); } return next();