From e7d81ad5a3e19e8bd0b2b1bf8fdce1c0a058a9da Mon Sep 17 00:00:00 2001 From: MCHost Date: Thu, 3 Jul 2025 01:29:19 -0400 Subject: [PATCH] Add input validation and add advanced logging - Implemented input validation for binary paths, hostnames, and ports to enhance security - Added winston-based advanced logging with debug, info, warn, and error levels - Configured logging to output to both console and server.log file - Included request context and unique request IDs in logs - Fixed binary path validation to allow /home/go/status and added existence checks --- .gitignore | 2 + package.json | 5 +- status-site.js | 139 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 139 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 37d7e73..3d6d189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules .env +server.log +package-lock.json \ No newline at end of file diff --git a/package.json b/package.json index 102c00e..eed353e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "@tailwindcss/cli": "^4.1.11", "dotenv": "^16.4.5", "express": "^4.21.0", - "express-rate-limit": "^7.5.1" + "express-rate-limit": "^7.5.1", + "helmet": "^8.1.0", + "validator": "^13.15.15", + "winston": "^3.17.0" }, "devDependencies": { "nodemon": "^3.1.7", diff --git a/status-site.js b/status-site.js index a2093fd..d87e397 100644 --- a/status-site.js +++ b/status-site.js @@ -2,10 +2,26 @@ const express = require('express'); const { promisify } = require('util'); const { exec } = require('child_process'); const rateLimit = require('express-rate-limit'); +const path = require('path'); +const fs = require('fs'); +const winston = require('winston'); const app = express(); const port = 3066; require('dotenv').config(); +// Configure Winston logger +const logger = winston.createLogger({ + level: 'debug', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + transports: [ + new winston.transports.File({ filename: 'server.log' }), + new winston.transports.Console() + ] +}); + const execPromise = promisify(exec); // Custom key generator to support Cloudflare's CF-Connecting-IP header @@ -21,8 +37,12 @@ const limiter = rateLimit({ legacyHeaders: false, // Disable X-RateLimit headers keyGenerator: getClientIp, // Use CF-Connecting-IP or fallback to req.ip handler: (req, res) => { - // Calculate backoff time (exponential, starting at 2 seconds) const retryAfter = Math.pow(2, Math.min(req.rateLimit.current - req.rateLimit.limit, 5)); + logger.warn('Rate limit exceeded', { + ip: getClientIp(req), + url: req.url, + retryAfter + }); res.setHeader('Retry-After', retryAfter); res.status(429).json({ error: 'Too many requests, please try again later.', @@ -35,30 +55,118 @@ const limiter = rateLimit({ app.use('/java', limiter); app.use('/bedrock', limiter); -async function checkConnectionStatus(hostname, port) { +// Input validation functions +function validateBinaryPath(binaryPath) { + logger.debug('Validating binary path', { binaryPath }); + if (!binaryPath) { + logger.error('Binary path not configured'); + throw new Error('Binary path not configured'); + } + const resolvedPath = path.resolve(binaryPath); + const allowedPrefixes = [ + path.resolve('/home/go/status') + ]; + logger.debug('Checking binary path against allowed prefixes', { resolvedPath, allowedPrefixes }); + + // Check if path exists try { - const command = `${process.env.STATUS_CHECK_PATH} -host ${hostname} -port ${port}`; + fs.accessSync(resolvedPath, fs.constants.X_OK); + } catch (err) { + logger.error('Binary path does not exist or is not executable', { resolvedPath, error: err.message }); + throw new Error('Binary path does not exist or is not executable'); + } + + // Check if path starts with an allowed prefix + if (!allowedPrefixes.some(prefix => resolvedPath.startsWith(prefix))) { + logger.error('Invalid binary path location', { resolvedPath, allowedPrefixes }); + throw new Error('Invalid binary path'); + } + + // Check for shell metacharacters + if (/[|;&$><`]/.test(binaryPath)) { + logger.error('Invalid characters in binary path', { binaryPath }); + throw new Error('Invalid characters in binary path'); + } + logger.info('Binary path validated', { resolvedPath }); + return resolvedPath; +} + +function validateHostname(hostname) { + logger.debug('Validating hostname', { hostname }); + const hostnameRegex = /^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})$/; + if (!hostnameRegex.test(hostname)) { + logger.error('Invalid hostname format', { hostname }); + throw new Error('Invalid hostname format'); + } + if (/[|;&$><`]/.test(hostname)) { + logger.error('Invalid characters in hostname', { hostname }); + throw new Error('Invalid characters in hostname'); + } + logger.info('Hostname validated', { hostname }); + return hostname; +} + +function validatePort(port) { + logger.debug('Validating port', { port }); + const portNum = parseInt(port, 10); + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + logger.error('Invalid port number', { port }); + throw new Error('Port must be a number between 1 and 65535'); + } + if (/[^0-9]/.test(port.toString())) { + logger.error('Invalid characters in port', { port }); + throw new Error('Invalid characters in port'); + } + logger.info('Port validated', { port }); + return portNum.toString(); +} + +async function checkConnectionStatus(hostname, port) { + const requestId = Math.random().toString(36).substring(2); + logger.info('Starting Java server status check', { requestId, hostname, port }); + try { + const validatedBinary = validateBinaryPath(process.env.STATUS_CHECK_PATH); + const validatedHostname = validateHostname(hostname); + const validatedPort = validatePort(port); + + const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; + logger.debug('Executing command', { requestId, command }); + const { stdout, stderr } = await execPromise(command); if (stderr) { + logger.error('Command execution error', { requestId, error: stderr }); return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); + logger.info('Java server status check completed', { requestId, isOnline: true }); return { isOnline: true, data }; } catch (error) { + logger.error('Java server status check failed', { requestId, error: error.message }); return { isOnline: false, error: error.message }; } } async function checkGeyserStatus(hostname, port) { + const requestId = Math.random().toString(36).substring(2); + logger.info('Starting Bedrock server status check', { requestId, hostname, port }); try { - const command = `${process.env.GEYSER_STATUS_CHECK_PATH} -host ${hostname} -port ${port}`; + const validatedBinary = validateBinaryPath(process.env.GEYSER_STATUS_CHECK_PATH); + const validatedHostname = validateHostname(hostname); + const validatedPort = validatePort(port); + + const command = `${validatedBinary} -host ${validatedHostname} -port ${validatedPort}`; + logger.debug('Executing command', { requestId, command }); + const { stdout, stderr } = await execPromise(command); if (stderr) { + logger.error('Command execution error', { requestId, error: stderr }); return { isOnline: false, error: stderr }; } const data = JSON.parse(stdout); + logger.info('Bedrock server status check completed', { requestId, isOnline: true }); return { isOnline: true, data }; } catch (error) { + logger.error('Bedrock server status check failed', { requestId, error: error.message }); return { isOnline: false, error: error.message }; } } @@ -66,30 +174,49 @@ async function checkGeyserStatus(hostname, port) { app.use(express.static('public')); app.get('/', (req, res) => { + logger.info('Serving index page', { ip: getClientIp(req) }); res.sendFile(__dirname + '/public/index.html'); }); app.get('/java/:host/:port', async (req, res) => { const { host, port } = req.params; + logger.info('Received Java server check request', { + ip: getClientIp(req), + host, + port + }); try { const result = await checkConnectionStatus(host, port); res.json(result); } catch (error) { + logger.error('Java server check request failed', { + ip: getClientIp(req), + error: error.message + }); res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` }); } }); app.get('/bedrock/:host/:port', async (req, res) => { const { host, port } = req.params; + logger.info('Received Bedrock server check request', { + ip: getClientIp(req), + host, + port + }); try { const result = await checkGeyserStatus(host, port); - console.log(result); + logger.debug('Bedrock check result', { result }); res.json(result); } catch (error) { + logger.error('Bedrock server check request failed', { + ip: getClientIp(req), + error: error.message + }); res.status(500).json({ isOnline: false, error: `Server error: ${error.message}` }); } }); app.listen(port, () => { - console.log(`Server running at http://localhost:${port}`); + logger.info(`Server started on port ${port}`); }); \ No newline at end of file