fix(security #10): sanitize error details to prevent API key leakage
Added server/utils/sanitizeError.js which redacts: - ?apikey= query parameters (SABnzbd passes key in URL) - ?token= query parameters - X-Api-Key / X-MediaBrowser-Token / X-Emby-Authorization header values if they appear in the error message string Applied to all catch blocks in emby.js, sabnzbd.js, sonarr.js, radarr.js, and dashboard.js. Internal error.message still logged server-side (unredacted) for debugging.
This commit is contained in:
@@ -7,6 +7,7 @@ const { mapTorrentToDownload } = require('../utils/qbittorrent');
|
||||
const cache = require('../utils/cache');
|
||||
const { pollAllServices, getLastPollTimings, POLLING_ENABLED } = require('../utils/poller');
|
||||
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;
|
||||
@@ -612,7 +613,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error(`[Dashboard] Error fetching user downloads:`, error.message);
|
||||
console.error(`[Dashboard] Full error:`, error);
|
||||
res.status(500).json({ error: 'Failed to fetch user downloads', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch user downloads', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -686,7 +687,7 @@ router.get('/user-summary', requireAuth, async (req, res) => {
|
||||
|
||||
res.json(Object.values(userDownloads));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch user summary', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch user summary', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const axios = require('axios');
|
||||
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;
|
||||
@@ -16,7 +17,7 @@ router.get('/sessions', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Emby sessions', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch Emby sessions', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +29,7 @@ router.get('/users/:id', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch user details', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch user details', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,7 +41,7 @@ router.get('/users', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch users', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch users', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,7 +63,7 @@ router.get('/session/:sessionId/user', async (req, res) => {
|
||||
|
||||
res.json(userResponse.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch user from session', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch user from session', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const axios = require('axios');
|
||||
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;
|
||||
@@ -16,7 +17,7 @@ router.get('/queue', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr queue', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr queue', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,7 +30,7 @@ router.get('/history', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr history', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr history', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,7 +42,7 @@ router.get('/movies/:id', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch movie details', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch movie details', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -53,7 +54,7 @@ router.get('/movies', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch movies', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch movies', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const axios = require('axios');
|
||||
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;
|
||||
@@ -20,7 +21,7 @@ router.get('/queue', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch SABnzbd queue', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch SABnzbd queue', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,7 +38,7 @@ router.get('/history', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch SABnzbd history', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch SABnzbd history', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const axios = require('axios');
|
||||
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;
|
||||
@@ -16,7 +17,7 @@ router.get('/queue', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr queue', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr queue', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,7 +30,7 @@ router.get('/history', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr history', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr history', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,7 +42,7 @@ router.get('/series/:id', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch series details', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch series details', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -53,7 +54,7 @@ router.get('/series', async (req, res) => {
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch series', details: error.message });
|
||||
res.status(500).json({ error: 'Failed to fetch series', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
15
server/utils/sanitizeError.js
Normal file
15
server/utils/sanitizeError.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const API_KEY_PATTERN = /([?&]apikey=)[^&\s]*/gi;
|
||||
const TOKEN_PATTERN = /([?&]token=)[^&\s]*/gi;
|
||||
const HEADER_PATTERN = /x-(?:api-key|mediabrowser-token|emby-authorization):[^\s,]*/gi;
|
||||
|
||||
function sanitizeError(err) {
|
||||
let msg = err.message || String(err);
|
||||
// Redact API keys in URLs (SABnzbd passes apikey as query param)
|
||||
msg = msg.replace(API_KEY_PATTERN, '$1[REDACTED]');
|
||||
msg = msg.replace(TOKEN_PATTERN, '$1[REDACTED]');
|
||||
// Redact auth header values if they appear in the message
|
||||
msg = msg.replace(HEADER_PATTERN, (m) => m.split(':')[0] + ':[REDACTED]');
|
||||
return msg;
|
||||
}
|
||||
|
||||
module.exports = sanitizeError;
|
||||
Reference in New Issue
Block a user