diff --git a/includes/auth.js b/includes/auth.js index 8b7f3f4..20addc3 100644 --- a/includes/auth.js +++ b/includes/auth.js @@ -1,88 +1,296 @@ import unirest from 'unirest'; -import { randomBytes } from 'crypto'; +import { randomBytes, createHmac } from 'crypto'; +import { RateLimiterMemory } from 'rate-limiter-flexible'; +import sanitizeHtml from 'sanitize-html'; +import helmet from 'helmet'; +import csurf from 'csurf'; +import winston from 'winston'; +import validator from 'validator'; -const temporaryLinks = new Map(); +// Initialize logger +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + transports: [ + new winston.transports.File({ filename: 'security.log' }) + ] +}); -setInterval(() => { - const now = Date.now(); - for (const [linkId, linkData] of temporaryLinks.entries()) { - if (linkData.expiresAt < now) temporaryLinks.delete(linkId); - } -}, parseInt(process.env.TEMP_LINKS_CLEANUP_INTERVAL_MS, 10)); +// Environment variable validation +const requiredEnvVars = [ + 'ADMIN_SECRET_KEY', + 'AUTH_ENDPOINT', + 'AUTH_PASSWORD', + 'AUTO_LOGIN_LINK_PREFIX', + 'AUTO_LOGIN_REDIRECT_URL', + 'LINK_ID_BYTES', + 'LINK_EXPIRY_SECONDS', + 'TEMP_LINKS_CLEANUP_INTERVAL_MS' +]; -export async function generateLoginLink(req, res) { - try { - const { secretKey, username } = req.body; - if (secretKey !== process.env.ADMIN_SECRET_KEY) return res.status(401).json({ error: 'Invalid secret key' }); - if (!username) return res.status(400).json({ error: 'Username is required' }); - - const tokenResponse = await unirest - .post(process.env.AUTH_ENDPOINT) - .headers({ 'Accept': 'application/json', 'Content-Type': 'application/json' }) - .send({ username, password: process.env.AUTH_PASSWORD }); - - if (!tokenResponse.body.token) return res.status(500).json({ error: 'Failed to generate API key' }); - - const apiKey = tokenResponse.body.token; - const linkId = randomBytes(parseInt(process.env.LINK_ID_BYTES, 10)).toString('hex'); - const loginLink = `${process.env.AUTO_LOGIN_LINK_PREFIX}${linkId}`; - - temporaryLinks.set(linkId, { - apiKey, - username, - expiresAt: Date.now() + parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000 - }); - - setTimeout(() => temporaryLinks.delete(linkId), parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000); - res.json({ loginLink }); - } catch (error) { - res.status(500).json({ error: 'Internal server error' }); +for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + logger.error(`Missing required environment variable: ${envVar}`); + process.exit(1); } } -export function handleAutoLogin(req, res) { - const { linkId } = req.params; - const linkData = temporaryLinks.get(linkId); +// Initialize security middleware +const csrfProtection = csurf({ cookie: { secure: true, httpOnly: true, sameSite: 'strict' } }); +const rateLimiter = new RateLimiterMemory({ + points: 10, + duration: 60 // 10 requests per minute +}); - if (!linkData || linkData.expiresAt < Date.now()) { - temporaryLinks.delete(linkId); - return res.send(` +const temporaryLinks = new Map(); + +// Secure cleanup interval +const cleanupInterval = Math.max(60000, parseInt(process.env.TEMP_LINKS_CLEANUP_INTERVAL_MS, 10)); +setInterval(() => { + const now = Date.now(); + for (const [linkId, linkData] of temporaryLinks.entries()) { + if (linkData.expiresAt < now) { + temporaryLinks.delete(linkId); + logger.info(`Cleaned up expired link: ${linkId}`); + } + } +}, cleanupInterval); + +// Input sanitization and validation +const sanitizeInput = (input) => { + if (typeof input !== 'string') { + logger.warn(`Invalid input type: expected string, got ${typeof input}`); + return null; + } + // Allow alphanumeric characters and underscores + if (!/^[a-zA-Z0-9_]+$/.test(input)) { + logger.warn(`Invalid input format: ${input}`); + return null; + } + return sanitizeHtml(input); +}; + +// Generate secure token +const generateSecureToken = (bytes) => { + const buffer = randomBytes(Math.max(16, parseInt(bytes, 10))); + const hmac = createHmac('sha256', process.env.ADMIN_SECRET_KEY); + return hmac.update(buffer).digest('hex'); +}; + +// Check if IP is local (for development) +const isLocalIp = (ip) => { + return ( + ip === '127.0.0.1' || + ip === '::1' || + ip.startsWith('192.168.') || + ip.startsWith('10.') + ); +}; + +// Generate nonce for CSP +const generateNonce = () => { + return randomBytes(16).toString('base64'); +}; + +export async function generateLoginLink(req, res) { + // Apply security headers + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"] + } + }, + xFrameOptions: { action: 'deny' } + })(req, res, async () => { + try { + // Rate limiting + await rateLimiter.consume(req.ip); + + // CSRF protection + csrfProtection(req, res, async () => { + const { secretKey, username } = req.body; + + // Validate inputs + if (!sanitizeInput(secretKey) || secretKey !== process.env.ADMIN_SECRET_KEY) { + logger.warn(`Invalid secret key attempt from IP: ${req.ip}`); + return res.status(401).json({ error: 'Unauthorized' }); + } + + const sanitizedUsername = sanitizeInput(username); + if (!sanitizedUsername) { + logger.warn(`Invalid username attempt from IP: ${req.ip}, username: ${username}`); + return res.status(400).json({ error: 'Invalid username' }); + } + + // Secure API request + const tokenResponse = await unirest + .post(process.env.AUTH_ENDPOINT) + .headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Request-ID': generateSecureToken(16) + }) + .send({ username: sanitizedUsername, password: process.env.AUTH_PASSWORD }) + .timeout(5000); + + if (!tokenResponse.body.token) { + logger.error(`Failed to generate API key for username: ${sanitizedUsername}`); + return res.status(500).json({ error: 'Authentication service error' }); + } + + const apiKey = tokenResponse.body.token; + const linkId = generateSecureToken(process.env.LINK_ID_BYTES); + const loginLink = `${process.env.AUTO_LOGIN_LINK_PREFIX}${linkId}`; + + // Store link data with additional security metadata + temporaryLinks.set(linkId, { + apiKey, + username: sanitizedUsername, + expiresAt: Date.now() + Math.min(3600000, parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000), + ip: req.ip, + userAgent: req.get('User-Agent') || 'Unknown' + }); + + // Secure timeout + setTimeout(() => { + temporaryLinks.delete(linkId); + logger.info(`Expired link removed: ${linkId}`); + }, Math.min(3600000, parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000)); + + logger.info(`Generated login link for username: ${sanitizedUsername} from IP: ${req.ip}, userAgent: ${req.get('User-Agent') || 'Unknown'}`); + res.json({ loginLink }); + }); + } catch (error) { + logger.error(`Error generating login link: ${error.message}`); + res.status(500).json({ error: 'Server error' }); + } + }); +} + +export function handleAutoLogin(req, res) { + // Generate nonce for CSP + const nonce = generateNonce(); + + // Apply security headers with nonce + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'", `'nonce-${nonce}'`] + } + }, + xFrameOptions: { action: 'deny' } + })(req, res, () => { + const { linkId } = req.params; + const sanitizedLinkId = sanitizeInput(linkId); + const linkData = temporaryLinks.get(sanitizedLinkId); + + if (!linkData || linkData.expiresAt < Date.now()) { + temporaryLinks.delete(sanitizedLinkId); + logger.warn(`Expired or invalid login attempt for link: ${sanitizedLinkId} from IP: ${req.ip}`); + + return res.send(` + +
+ + + + + +Logging in...
- - - `); + }); } \ No newline at end of file diff --git a/package.json b/package.json index 566f724..9d54b58 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "axios": "^1.10.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "csurf": "^1.11.0", "dockerode": "^4.0.2", "dotenv": "^16.5.0", "envalid": "^8.0.0", "express": "^4.21.2", "helmet": "^8.1.0", "node-fetch": "^2.7.0", + "rate-limiter-flexible": "^7.1.1", "sanitize-html": "^2.17.0", "ssh2-sftp-client": "^12.0.0", "unirest": "^0.6.0", diff --git a/security.log b/security.log new file mode 100644 index 0000000..cbbda9f --- /dev/null +++ b/security.log @@ -0,0 +1,17 @@ +{"level":"warn","message":"Invalid username attempt from IP: 127.0.0.1","timestamp":"2025-06-16T18:10:18.600Z"} +{"level":"info","message":"Generated login link for username: mc_342128351638585344 from IP: 127.0.0.1","timestamp":"2025-06-16T18:11:50.622Z"} +{"level":"warn","message":"Suspicious login attempt for link: 70006d7efa618b692d3cd7a4e0de3e7ad1f97b973d1c3e73cc2d1744e8fbebc9 from IP: 192.168.0.12","timestamp":"2025-06-16T18:11:52.271Z"} +{"level":"info","message":"Generated login link for username: mc_342128351638585344 from IP: 127.0.0.1","timestamp":"2025-06-16T18:11:57.737Z"} +{"level":"warn","message":"Suspicious login attempt for link: f89530e9feb3eb61e46a524eebc2e19072629d690987553835c065c55a937ada from IP: 192.168.0.12","timestamp":"2025-06-16T18:11:59.391Z"} +{"level":"info","message":"Generated login link for username: mc_342128351638585344 from IP: 127.0.0.1","timestamp":"2025-06-16T18:13:40.783Z"} +{"level":"warn","message":"Suspicious login attempt for link: 1949a9b1794399ac52627f0004b3189420324834239f0aedb8488ee90fd25827 from IP: 192.168.0.12, expected IP: 127.0.0.1, isLocal: true, userAgentMatch: false","timestamp":"2025-06-16T18:13:42.236Z"} +{"level":"warn","message":"Expired or invalid login attempt for link: 1949a9b1794399ac52627f0004b3189420324834239f0aedb8488ee90fd25827 from IP: 192.168.0.12","timestamp":"2025-06-16T18:13:45.560Z"} +{"level":"info","message":"Generated login link for username: mc_342128351638585344 from IP: 127.0.0.1, userAgent: Unknown","timestamp":"2025-06-16T18:15:20.365Z"} +{"level":"info","message":"Non-critical user-agent mismatch for link: 169a25f0d2290e382ab179fe57ea127f395be42bc5adfa1d56dddbfebd5b9386 from IP: 192.168.0.12, expectedUserAgent: Unknown, actualUserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36","timestamp":"2025-06-16T18:15:22.233Z"} +{"level":"info","message":"Successful auto-login for username: mc_342128351638585344 from IP: 192.168.0.12, userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36","timestamp":"2025-06-16T18:15:22.234Z"} +{"level":"info","message":"Generated login link for username: mc_342128351638585344 from IP: 127.0.0.1, userAgent: Unknown","timestamp":"2025-06-16T18:17:54.566Z"} +{"level":"info","message":"Non-critical user-agent mismatch for link: b093bc205ebf1d5993b3a5efa25003ca78b6f313d9e6d12600e53e3d4cbf412e from IP: 192.168.0.12, expectedUserAgent: Unknown, actualUserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36","timestamp":"2025-06-16T18:17:55.765Z"} +{"level":"info","message":"Successful auto-login for username: mc_342128351638585344 from IP: 192.168.0.12, userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36","timestamp":"2025-06-16T18:17:55.765Z"} +{"level":"info","message":"Generated login link for username: mc_342128351638585344 from IP: 127.0.0.1, userAgent: Unknown","timestamp":"2025-06-16T18:18:17.715Z"} +{"level":"info","message":"Non-critical user-agent mismatch for link: 5f2cdcd8bb77caebc860c411ddb714a53513084d7d9fd7cbce3faf8c74faac46 from IP: 192.168.0.12, expectedUserAgent: Unknown, actualUserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36","timestamp":"2025-06-16T18:18:19.312Z"} +{"level":"info","message":"Successful auto-login for username: mc_342128351638585344 from IP: 192.168.0.12, userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36","timestamp":"2025-06-16T18:18:19.313Z"}