From 44cff5bf4156d66c0c8611c758d29977dc5b3e1d Mon Sep 17 00:00:00 2001 From: Gronod Date: Sat, 16 May 2026 16:26:53 +0100 Subject: [PATCH] fix(security #15): read API keys from process.env at request time Module-level const assignments (SONARR_API_KEY, RADARR_API_KEY, SABNZBD_API_KEY, EMBY_URL, EMBY_API_KEY) captured values at startup and would not pick up rotated credentials without a restart. Replaced all module-level captures in emby.js, sabnzbd.js, sonarr.js, radarr.js, and dashboard.js with inline process.env reads at each call site. A process restart is still needed for dotenv-loaded values but environment-injected vars (Docker, Kubernetes) are re-read live. --- server/routes/dashboard.js | 10 ++++------ server/routes/emby.js | 23 ++++++++++------------- server/routes/radarr.js | 19 ++++++++----------- server/routes/sabnzbd.js | 11 ++++------- server/routes/sonarr.js | 19 ++++++++----------- 5 files changed, 34 insertions(+), 48 deletions(-) diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 0564107..3e7576a 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -9,8 +9,6 @@ const { pollAllServices, getLastPollTimings, POLLING_ENABLED } = require('../uti const { getSonarrInstances, getRadarrInstances } = require('../utils/config'); const sanitizeError = require('../utils/sanitizeError'); -const EMBY_URL = process.env.EMBY_URL; -const EMBY_API_KEY = process.env.EMBY_API_KEY; // Helper function to extract poster/cover art URL from a movie or series object function getCoverArt(item) { @@ -102,8 +100,8 @@ async function getEmbyUsers() { const cached = cache.get('emby:users'); if (cached) return cached; try { - const response = await axios.get(`${EMBY_URL}/Users`, { - headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } + const response = await axios.get(`${process.env.EMBY_URL}/Users`, { + headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY } }); // Build map: both raw lowercase and sanitized form -> display name const map = new Map(); @@ -624,8 +622,8 @@ router.get('/user-summary', requireAuth, async (req, res) => { const radarrInstances = getRadarrInstances(); // Get all Emby users - const usersResponse = await axios.get(`${EMBY_URL}/Users`, { - headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } + const usersResponse = await axios.get(`${process.env.EMBY_URL}/Users`, { + headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY } }); // Get all series, movies, and tags from all instances diff --git a/server/routes/emby.js b/server/routes/emby.js index da69e12..f349e7f 100644 --- a/server/routes/emby.js +++ b/server/routes/emby.js @@ -4,16 +4,13 @@ const router = express.Router(); const requireAuth = require('../middleware/requireAuth'); const sanitizeError = require('../utils/sanitizeError'); -const EMBY_URL = process.env.EMBY_URL; -const EMBY_API_KEY = process.env.EMBY_API_KEY; - router.use(requireAuth); // Get active sessions router.get('/sessions', async (req, res) => { try { - const response = await axios.get(`${EMBY_URL}/Sessions`, { - headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } + const response = await axios.get(`${process.env.EMBY_URL}/Sessions`, { + headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY } }); res.json(response.data); } catch (error) { @@ -24,8 +21,8 @@ router.get('/sessions', async (req, res) => { // Get user by ID router.get('/users/:id', async (req, res) => { try { - const response = await axios.get(`${EMBY_URL}/Users/${req.params.id}`, { - headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } + const response = await axios.get(`${process.env.EMBY_URL}/Users/${req.params.id}`, { + headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY } }); res.json(response.data); } catch (error) { @@ -36,8 +33,8 @@ router.get('/users/:id', async (req, res) => { // Get all users router.get('/users', async (req, res) => { try { - const response = await axios.get(`${EMBY_URL}/Users`, { - headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } + const response = await axios.get(`${process.env.EMBY_URL}/Users`, { + headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY } }); res.json(response.data); } catch (error) { @@ -48,8 +45,8 @@ router.get('/users', async (req, res) => { // Get current user by session ID router.get('/session/:sessionId/user', async (req, res) => { try { - const response = await axios.get(`${EMBY_URL}/Sessions`, { - headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } + const response = await axios.get(`${process.env.EMBY_URL}/Sessions`, { + headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY } }); const session = response.data.find(s => s.Id === req.params.sessionId); @@ -57,8 +54,8 @@ router.get('/session/:sessionId/user', async (req, res) => { return res.status(404).json({ error: 'Session not found' }); } - const userResponse = await axios.get(`${EMBY_URL}/Users/${session.UserId}`, { - headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } + const userResponse = await axios.get(`${process.env.EMBY_URL}/Users/${session.UserId}`, { + headers: { 'X-MediaBrowser-Token': process.env.EMBY_API_KEY } }); res.json(userResponse.data); diff --git a/server/routes/radarr.js b/server/routes/radarr.js index 9c5ced7..e9f348b 100644 --- a/server/routes/radarr.js +++ b/server/routes/radarr.js @@ -4,16 +4,13 @@ const router = express.Router(); const requireAuth = require('../middleware/requireAuth'); const sanitizeError = require('../utils/sanitizeError'); -const RADARR_URL = process.env.RADARR_URL; -const RADARR_API_KEY = process.env.RADARR_API_KEY; - router.use(requireAuth); // Get queue router.get('/queue', async (req, res) => { try { - const response = await axios.get(`${RADARR_URL}/api/v3/queue`, { - headers: { 'X-Api-Key': RADARR_API_KEY } + const response = await axios.get(`${process.env.RADARR_URL}/api/v3/queue`, { + headers: { 'X-Api-Key': process.env.RADARR_API_KEY } }); res.json(response.data); } catch (error) { @@ -24,8 +21,8 @@ router.get('/queue', async (req, res) => { // Get history router.get('/history', async (req, res) => { try { - const response = await axios.get(`${RADARR_URL}/api/v3/history`, { - headers: { 'X-Api-Key': RADARR_API_KEY }, + const response = await axios.get(`${process.env.RADARR_URL}/api/v3/history`, { + headers: { 'X-Api-Key': process.env.RADARR_API_KEY }, params: { pageSize: req.query.pageSize || 50 } }); res.json(response.data); @@ -37,8 +34,8 @@ router.get('/history', async (req, res) => { // Get movie details router.get('/movies/:id', async (req, res) => { try { - const response = await axios.get(`${RADARR_URL}/api/v3/movie/${req.params.id}`, { - headers: { 'X-Api-Key': RADARR_API_KEY } + const response = await axios.get(`${process.env.RADARR_URL}/api/v3/movie/${req.params.id}`, { + headers: { 'X-Api-Key': process.env.RADARR_API_KEY } }); res.json(response.data); } catch (error) { @@ -49,8 +46,8 @@ router.get('/movies/:id', async (req, res) => { // Get all movies with tags router.get('/movies', async (req, res) => { try { - const response = await axios.get(`${RADARR_URL}/api/v3/movie`, { - headers: { 'X-Api-Key': RADARR_API_KEY } + const response = await axios.get(`${process.env.RADARR_URL}/api/v3/movie`, { + headers: { 'X-Api-Key': process.env.RADARR_API_KEY } }); res.json(response.data); } catch (error) { diff --git a/server/routes/sabnzbd.js b/server/routes/sabnzbd.js index 4515f5e..19ff152 100644 --- a/server/routes/sabnzbd.js +++ b/server/routes/sabnzbd.js @@ -4,18 +4,15 @@ const router = express.Router(); const requireAuth = require('../middleware/requireAuth'); const sanitizeError = require('../utils/sanitizeError'); -const SABNZBD_URL = process.env.SABNZBD_URL; -const SABNZBD_API_KEY = process.env.SABNZBD_API_KEY; - router.use(requireAuth); // Get current queue router.get('/queue', async (req, res) => { try { - const response = await axios.get(`${SABNZBD_URL}/api`, { + const response = await axios.get(`${process.env.SABNZBD_URL}/api`, { params: { mode: 'queue', - apikey: SABNZBD_API_KEY, + apikey: process.env.SABNZBD_API_KEY, output: 'json' } }); @@ -28,10 +25,10 @@ router.get('/queue', async (req, res) => { // Get history router.get('/history', async (req, res) => { try { - const response = await axios.get(`${SABNZBD_URL}/api`, { + const response = await axios.get(`${process.env.SABNZBD_URL}/api`, { params: { mode: 'history', - apikey: SABNZBD_API_KEY, + apikey: process.env.SABNZBD_API_KEY, output: 'json', limit: req.query.limit || 50 } diff --git a/server/routes/sonarr.js b/server/routes/sonarr.js index 255f801..889b9ab 100644 --- a/server/routes/sonarr.js +++ b/server/routes/sonarr.js @@ -4,16 +4,13 @@ const router = express.Router(); const requireAuth = require('../middleware/requireAuth'); const sanitizeError = require('../utils/sanitizeError'); -const SONARR_URL = process.env.SONARR_URL; -const SONARR_API_KEY = process.env.SONARR_API_KEY; - router.use(requireAuth); // Get queue router.get('/queue', async (req, res) => { try { - const response = await axios.get(`${SONARR_URL}/api/v3/queue`, { - headers: { 'X-Api-Key': SONARR_API_KEY } + const response = await axios.get(`${process.env.SONARR_URL}/api/v3/queue`, { + headers: { 'X-Api-Key': process.env.SONARR_API_KEY } }); res.json(response.data); } catch (error) { @@ -24,8 +21,8 @@ router.get('/queue', async (req, res) => { // Get history router.get('/history', async (req, res) => { try { - const response = await axios.get(`${SONARR_URL}/api/v3/history`, { - headers: { 'X-Api-Key': SONARR_API_KEY }, + const response = await axios.get(`${process.env.SONARR_URL}/api/v3/history`, { + headers: { 'X-Api-Key': process.env.SONARR_API_KEY }, params: { pageSize: req.query.pageSize || 50 } }); res.json(response.data); @@ -37,8 +34,8 @@ router.get('/history', async (req, res) => { // Get series details router.get('/series/:id', async (req, res) => { try { - const response = await axios.get(`${SONARR_URL}/api/v3/series/${req.params.id}`, { - headers: { 'X-Api-Key': SONARR_API_KEY } + const response = await axios.get(`${process.env.SONARR_URL}/api/v3/series/${req.params.id}`, { + headers: { 'X-Api-Key': process.env.SONARR_API_KEY } }); res.json(response.data); } catch (error) { @@ -49,8 +46,8 @@ router.get('/series/:id', async (req, res) => { // Get all series with tags router.get('/series', async (req, res) => { try { - const response = await axios.get(`${SONARR_URL}/api/v3/series`, { - headers: { 'X-Api-Key': SONARR_API_KEY } + const response = await axios.get(`${process.env.SONARR_URL}/api/v3/series`, { + headers: { 'X-Api-Key': process.env.SONARR_API_KEY } }); res.json(response.data); } catch (error) {