Fix 5 critical security vulnerabilities

1. WebSocket exec auth bypass (exec.js): Require valid JWT or API key
   before accepting WebSocket upgrade. Reject unauthenticated requests
   with 401 before the upgrade completes.

2. Shell injection in router auto-login (session-handlers.js): Validate
   baseUrl against safe hostname pattern before embedding in wget shell
   command. Reject with null session if invalid.

3. Path traversal in credentials routes (services.js): Add explicit
   serviceId validation (alphanumeric + dash/underscore/dot, max 100
   chars) to all three credential endpoints. Removed redundant
   try/catch wrapper.

4. execSync injection in CA CSR generation (ca.js): Add sanitize step
   replacing any non-alphanumeric domain chars with underscore before
   interpolation into shell subj argument. Redundant with existing
   validation but provides defense-in-depth.

5. Auth bypass when TOTP disabled (middleware.js): Split the logic
   cleanly — disabled TOTP means no auth (initial setup state), enabled
   TOTP means all auth methods checked (session/JWT/API key). Removed
   the sessionDuration:never conflating shortcut.
This commit is contained in:
Hermes
2026-05-27 18:05:35 -07:00
parent 445da9f5fc
commit 17edb3bc90
6 changed files with 92 additions and 28 deletions
+21 -8
View File
@@ -196,8 +196,14 @@ module.exports = function({
// ===== SERVICE CREDENTIAL ENDPOINTS =====
// Store credentials for a service
router.post('/services/:serviceId/credentials', asyncHandler(async (req, res) => {
router.post('/:serviceId/credentials', asyncHandler(async (req, res) => {
const { serviceId } = req.params;
// Validate serviceId to prevent path traversal in credential keys
if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
return ctx.errorResponse(res, 400, 'Invalid service ID');
}
const { apiKey, username, password } = req.body;
if (apiKey) {
@@ -214,8 +220,14 @@ module.exports = function({
}, 'store-service-creds'));
// Delete credentials for a service
router.delete('/services/:serviceId/credentials', asyncHandler(async (req, res) => {
router.delete('/:serviceId/credentials', asyncHandler(async (req, res) => {
const { serviceId } = req.params;
// Validate serviceId to prevent path traversal in credential keys
if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
return ctx.errorResponse(res, 400, 'Invalid service ID');
}
await credentialManager.delete(`service.${serviceId}.apikey`);
await credentialManager.delete(`service.${serviceId}.username`);
await credentialManager.delete(`service.${serviceId}.password`);
@@ -223,9 +235,13 @@ module.exports = function({
}, 'delete-service-creds'));
// Check credential status for a service (what's stored)
router.get('/services/:serviceId/credentials', asyncHandler(async (req, res) => {
try {
const { serviceId } = req.params;
router.get('/:serviceId/credentials', asyncHandler(async (req, res) => {
const { serviceId } = req.params;
// Validate serviceId to prevent path traversal in credential keys
if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
return ctx.errorResponse(res, 400, 'Invalid service ID');
}
const arrKey = await credentialManager.retrieve(`arr.${serviceId}.apikey`).catch(() => null);
const svcKey = await credentialManager.retrieve(`service.${serviceId}.apikey`).catch(() => null);
const username = await credentialManager.retrieve(`service.${serviceId}.username`).catch(() => null);
@@ -234,9 +250,6 @@ module.exports = function({
hasBasicAuth: !!username,
username: username || null
});
} catch (error) {
success(res, { hasApiKey: false, hasBasicAuth: false });
}
}, 'service-creds'));
// ===== SEEDHOST CREDENTIAL ENDPOINTS =====