From 09f2f3be369b437a9543c3ebd7cb889e9eec0d87 Mon Sep 17 00:00:00 2001 From: Carter Myers <206+cmyers@users.noreply.github.mieweb.com> Date: Wed, 17 Sep 2025 15:13:42 -0700 Subject: [PATCH 1/2] Enhance security and config in server.js Replaced vulnerable 'exec' with 'spawn' for user authentication to improve security. Session secret is now loaded from environment variables, and the app is configured to trust proxy and use secure cookies. Middleware setup and server initialization were refactored for better structure. --- create-a-container/server.js | 59 ++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/create-a-container/server.js b/create-a-container/server.js index 5b97cc62..5f4d298e 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -1,28 +1,37 @@ +require('dotenv').config(); + const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); -const { spawn, exec } = require('child_process'); +const { spawn, exec } = require('child_process'); // spawn is now used const path = require('path'); const crypto = require('crypto'); -const fs = require('fs'); // Added fs module +const fs = require('fs'); const app = express(); +app.use(express.json()); // <-- ADDED: Middleware to parse JSON bodies +app.set('trust proxy', 1); // A simple in-memory object to store job status and output const jobs = {}; // --- Middleware Setup --- -app.use(bodyParser.urlencoded({ extended: true })); -app.use(express.json()); -app.use(express.static('public')); +if (!process.env.SESSION_SECRET) { + throw new Error("SESSION_SECRET is not set in environment!"); +} + app.use(session({ - secret: 'A7d#9Lm!qW2z%Xf8@Rj3&bK6^Yp$0Nc', - resave: false, - saveUninitialized: true, - cookie: { secure: false } // Set to true if using HTTPS + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: true, + cookie: { secure: true } // true if behind HTTPS })); // --- Route Handlers --- +app.use(express.static('public')); + +const PORT = 3000; +app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); // Serves the main container creation form, protected by login app.get('/form.html', (req, res) => { @@ -33,16 +42,31 @@ app.get('/form.html', (req, res) => { }); // Handles user login +// <-- CHANGED: Replaced vulnerable 'exec' with secure 'spawn' app.post('/login', (req, res) => { const { username, password } = req.body; - exec(`node /root/bin/js/runner.js authenticateUser ${username} ${password}`, (err, stdout) => { - if (err) { - console.error("Login script execution error:", err); + // Use spawn for security + const runner = spawn('node', ['/root/bin/js/runner.js', 'authenticateUser', username, password]); + + let stdoutData = ''; + let stderrData = ''; + + runner.stdout.on('data', (data) => { + stdoutData += data.toString(); + }); + + runner.stderr.on('data', (data) => { + stderrData += data.toString(); + }); + + runner.on('close', (code) => { + if (code !== 0) { + console.error("Login script execution error:", stderrData); return res.status(500).json({ error: "Server error during authentication." }); } - if (stdout.trim() === 'true') { + if (stdoutData.trim() === 'true') { req.session.user = username; req.session.proxmoxUsername = username; req.session.proxmoxPassword = password; @@ -53,7 +77,8 @@ app.post('/login', (req, res) => { }); }); -// ✨ UPDATED: API endpoint to get user's containers + +// API endpoint to get user's containers app.get('/api/my-containers', (req, res) => { if (!req.session.user) { return res.status(401).json({ error: "Unauthorized" }); @@ -155,8 +180,4 @@ app.get('/api/stream/:jobId', (req, res) => { clearInterval(intervalId); res.end(); }); -}); - -// --- Server Initialization --- -const PORT = 3000; -app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); \ No newline at end of file +}); \ No newline at end of file From 8d37b1813b44f9992d7d9645c73706317a9ab5e4 Mon Sep 17 00:00:00 2001 From: Carter Myers <206+cmyers@users.noreply.github.mieweb.com> Date: Wed, 17 Sep 2025 15:25:17 -0700 Subject: [PATCH 2/2] add limiter --- create-a-container/server.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/create-a-container/server.js b/create-a-container/server.js index 5f4d298e..ad37bf78 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -3,16 +3,17 @@ require('dotenv').config(); const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); -const { spawn, exec } = require('child_process'); // spawn is now used +const { spawn, exec } = require('child_process'); const path = require('path'); const crypto = require('crypto'); const fs = require('fs'); +const rateLimit = require('express-rate-limit'); // <-- ADDED const app = express(); -app.use(express.json()); // <-- ADDED: Middleware to parse JSON bodies +app.use(express.json()); app.set('trust proxy', 1); -// A simple in-memory object to store job status and output + const jobs = {}; // --- Middleware Setup --- @@ -24,12 +25,20 @@ app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true, - cookie: { secure: true } // true if behind HTTPS + cookie: { secure: true } })); -// --- Route Handlers --- app.use(express.static('public')); +// --- Rate Limiter for Login --- +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 requests per windowMs + message: { error: "Too many login attempts. Please try again later." } +}); + +// --- Route Handlers --- + const PORT = 3000; app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); @@ -41,12 +50,10 @@ app.get('/form.html', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'form.html')); }); -// Handles user login -// <-- CHANGED: Replaced vulnerable 'exec' with secure 'spawn' -app.post('/login', (req, res) => { +// Handles user login with rate limiting +app.post('/login', loginLimiter, (req, res) => { const { username, password } = req.body; - // Use spawn for security const runner = spawn('node', ['/root/bin/js/runner.js', 'authenticateUser', username, password]); let stdoutData = ''; @@ -77,16 +84,12 @@ app.post('/login', (req, res) => { }); }); - // API endpoint to get user's containers app.get('/api/my-containers', (req, res) => { if (!req.session.user) { return res.status(401).json({ error: "Unauthorized" }); } - // The username in port_map.json doesn't have the @pve suffix const username = req.session.user.split('@')[0]; - - // Command to read the remote JSON file const command = "ssh root@10.15.20.69 'cat /etc/nginx/port_map.json'"; exec(command, (err, stdout, stderr) => { @@ -97,7 +100,6 @@ app.get('/api/my-containers', (req, res) => { try { const portMap = JSON.parse(stdout); const userContainers = Object.entries(portMap) - // This check now ensures 'details' exists and has a 'user' property before comparing .filter(([_, details]) => details && details.user === username) .map(([name, details]) => ({ name, ...details }));