diff --git a/auth/src/index.js b/auth/src/index.js index 4c44a63..d7268a0 100644 --- a/auth/src/index.js +++ b/auth/src/index.js @@ -12,15 +12,76 @@ const version = process.env.VERSION; const vBuild = process.env.VERSION_BUILD; const vState = process.env.VERSION_STATE; -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); +// ─── SICUREZZA GLOBALE ────────────────────────────────────────────── + +// Trust proxy (necessario dietro Nginx/Traefik per avere il vero IP del client) +app.set('trust proxy', 1); + +// Rate Limiter in-memory (protezione DoS base, senza dipendenze esterne) +const rateLimitStore = new Map(); +const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minuto +const RATE_LIMIT_MAX = 100; // max richieste per finestra +const RATE_LIMIT_AUTH_MAX = 10; // max tentativi login/register per finestra + +// Pulizia periodica dello store +setInterval(() => { + const now = Date.now(); + for (const [key, entry] of rateLimitStore) { + if (now - entry.start > RATE_LIMIT_WINDOW_MS) { + rateLimitStore.delete(key); + } + } +}, RATE_LIMIT_WINDOW_MS); + +function createRateLimiter(maxRequests) { + return (req, res, next) => { + const key = `${req.ip}:${maxRequests}`; + const now = Date.now(); + const entry = rateLimitStore.get(key); + + if (!entry || now - entry.start > RATE_LIMIT_WINDOW_MS) { + rateLimitStore.set(key, { count: 1, start: now }); + return next(); + } + + entry.count++; + if (entry.count > maxRequests) { + res.set('Retry-After', Math.ceil((RATE_LIMIT_WINDOW_MS - (now - entry.start)) / 1000)); + return res.status(429).json({ error: 'Troppe richieste, riprova più tardi' }); + } + + next(); + }; +} + +const globalRateLimit = createRateLimiter(RATE_LIMIT_MAX); +const authRateLimit = createRateLimiter(RATE_LIMIT_AUTH_MAX); + +// Security headers (equivalente leggero di Helmet, senza dipendenze) +app.use((req, res, next) => { + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '0'); // Disabilitato a favore di CSP + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + // Rimuovi header che rivelano info sul server + res.removeHeader('X-Powered-By'); + next(); +}); + +// Rate limit globale +app.use(globalRateLimit); + +// Limiti dimensione body per prevenire payload eccessivi +app.use(express.json({ limit: '16kb' })); +app.use(express.urlencoded({ extended: true, limit: '16kb' })); app.use(parser()); -// Static files +// ─── STATIC FILES ─────────────────────────────────────────────────── const staticFolder = path.join(__dirname, 'static'); app.use('/static', express.static(staticFolder)); -// Nunjucks templates +// ─── NUNJUCKS TEMPLATES ───────────────────────────────────────────── const templatesFolder = path.join(__dirname, 'templates'); nunjucks.configure(templatesFolder, { autoescape: true, @@ -31,19 +92,24 @@ nunjucks.configure(templatesFolder, { app.set('views', templatesFolder); app.set('view engine', 'html'); -// Routes +// ─── ROUTES ───────────────────────────────────────────────────────── +// Views (pagine HTML) app.use('/', require('./routes/views/auth')); app.use('/sessions', require('./routes/views/sessions')); app.use('/user', require('./routes/views/user')); +// API - Auth (con rate limit più stretto su login/register) const authRoutes = require('./routes/auth'); -const usersRoutes = require('./routes/users'); -const sessionsRoutes = require('./routes/sessions'); +app.use('/api/auth/login', authRateLimit); +app.use('/api/auth/register', authRateLimit); app.use('/api/auth', authRoutes); -app.use('/api/users', usersRoutes); -app.use('/api/sessions', sessionsRoutes); +// API - Risorse +app.use('/api/users', require('./routes/users')); +app.use('/api/sessions', require('./routes/sessions')); + +// ─── HEALTH CHECK ─────────────────────────────────────────────────── app.get('/health', async (req, res) => { const dbConnected = await database.checkPostgres(); const redisHelper = require('./storage/redis'); @@ -60,7 +126,18 @@ app.get('/health', async (req, res) => { }); }); -// Startup +// ─── 404 HANDLER ──────────────────────────────────────────────────── +app.use((req, res) => { + res.status(404).json({ error: 'Risorsa non trovata' }); +}); + +// ─── ERROR HANDLER GLOBALE ────────────────────────────────────────── +app.use((err, req, res, _next) => { + console.error('[AUTH] Errore non gestito:', err); + res.status(500).json({ error: 'Errore interno del server' }); +}); + +// ─── STARTUP ──────────────────────────────────────────────────────── async function start() { await database.initDb(); app.listen(PORT, '0.0.0.0', () => { diff --git a/auth/src/middlewares/internal.security.js b/auth/src/middlewares/internal.security.js index 2a14b11..5228521 100644 --- a/auth/src/middlewares/internal.security.js +++ b/auth/src/middlewares/internal.security.js @@ -1,12 +1,42 @@ +const crypto = require('crypto'); + const API_KEY = process.env.INTERNAL_API_KEY; +/** + * Middleware di autenticazione per servizi interni (container-to-container). + * Verifica l'header 'x-internal-api-key' contro INTERNAL_API_KEY nell'env. + * + * SICUREZZA: + * - Se INTERNAL_API_KEY non è configurata, TUTTE le richieste vengono rifiutate + * - Usa timingSafeEqual per prevenire attacchi timing side-channel + */ const internalAuth = (req, res, next) => { - const internalToken = req.headers['x-internal-api-key']; - if (internalToken === API_KEY) { - req.user = { id: 'system', role: 'internal_service' }; - return next(); + // Se la chiave non è configurata nel server, blocca tutto + if (!API_KEY) { + console.error('[SECURITY] INTERNAL_API_KEY absent! All internal requests blocked.'); + return res.status(503).json({ error: 'Service not configured correctly' }); } - return res.status(403).json({ error: 'Accesso negato: Richiesta interna non autorizzata' }); + + const internalToken = req.headers['x-internal-api-key']; + + if (!internalToken || typeof internalToken !== 'string') { + return res.status(403).json({ error: 'unauthorized' }); + } + + // Confronto timing-safe per prevenire timing attacks + try { + const tokenBuffer = Buffer.from(internalToken, 'utf8'); + const keyBuffer = Buffer.from(API_KEY, 'utf8'); + + if (tokenBuffer.length !== keyBuffer.length || !crypto.timingSafeEqual(tokenBuffer, keyBuffer)) { + return res.status(403).json({ error: 'Accesso negato' }); + } + } catch { + return res.status(403).json({ error: 'Accesso negato' }); + } + + req.user = { id: 'system', role: 'internal_service' }; + return next(); }; module.exports = internalAuth; diff --git a/auth/src/middlewares/user.security.js b/auth/src/middlewares/user.security.js index 4484eb5..8a6c501 100644 --- a/auth/src/middlewares/user.security.js +++ b/auth/src/middlewares/user.security.js @@ -1,10 +1,21 @@ const jwt = require('../tools/jwt'); +/** + * Middleware di autenticazione per utenti finali. + * Verifica il JWT dal cookie 'auth_token' o dall'header 'Authorization: Bearer '. + * + * Se valido, inietta req.user con { user_id, username, session_id }. + */ const userAuth = (req, res, next) => { const token = (req.cookies && req.cookies.auth_token) || jwt.getToken(req.headers['authorization']); - if (!token) { - return res.status(401).json({ error: 'Accesso negato: Token utente mancante' }); + if (!token || typeof token !== 'string') { + return res.status(401).json({ error: 'Accesso negato: token mancante' }); + } + + // Limite ragionevole sulla lunghezza del token per evitare abusi + if (token.length > 2048) { + return res.status(400).json({ error: 'Token non valido' }); } const verified = jwt.verifyToken(token); diff --git a/auth/src/routes/auth.js b/auth/src/routes/auth.js index ea70bfa..786055d 100644 --- a/auth/src/routes/auth.js +++ b/auth/src/routes/auth.js @@ -5,6 +5,10 @@ const jwt = require('../tools/jwt'); const CONSOLE_URL = process.env.CONSOLE_URL || 'http://localhost:3004'; const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || undefined; +// Validazione input +const USERNAME_REGEX = /^[a-zA-Z0-9_.\-]{3,50}$/; +const PASSWORD_MIN_LENGTH = 8; +const PASSWORD_MAX_LENGTH = 128; router.post('/register', async (req, res) => { const { username, password } = req.body; @@ -13,21 +17,59 @@ router.post('/register', async (req, res) => { return res.status(400).json({ error: 'Username e password richiesti' }); } + if (typeof username !== 'string' || typeof password !== 'string') { + return res.status(400).json({ error: 'Formato dati non valido' }); + } + + if (!USERNAME_REGEX.test(username)) { + return res.status(400).json({ + error: 'Username non valido. 3-50 caratteri alfanumerici, underscore, punto o trattino.' + }); + } + + if (password.length < PASSWORD_MIN_LENGTH || password.length > PASSWORD_MAX_LENGTH) { + return res.status(400).json({ + error: `Password deve essere tra ${PASSWORD_MIN_LENGTH} e ${PASSWORD_MAX_LENGTH} caratteri` + }); + } + try { await auth.register(username, password); res.status(201).end(); } catch (err) { console.error('[AUTH] Register failed:', err.message); const status = err.message === 'User already exists' ? 409 : 500; - res.status(status).json({ error: err.message }); + res.status(status).json({ error: err.message === 'User already exists' ? err.message : 'Errore interno' }); } }); - - router.post('/login', async (req, res) => { const { username, password, redirect } = req.body; + // Validazione base + if (!username || !password || typeof username !== 'string' || typeof password !== 'string') { + return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); + } + + // Limiti di lunghezza per prevenire abuse + if (username.length > 50 || password.length > PASSWORD_MAX_LENGTH) { + return res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); + } + + // Validazione redirect URL per prevenire open redirect attacks + if (redirect && typeof redirect === 'string') { + try { + const redirectUrl = new URL(redirect); + const consoleUrl = new URL(CONSOLE_URL); + // Permetti redirect solo allo stesso dominio del CONSOLE_URL + if (redirectUrl.hostname !== consoleUrl.hostname) { + return res.render('loginpage', { error: 'Redirect non autorizzato', redirect: '' }); + } + } catch { + // URL relativo o non valido — ignora il redirect + } + } + try { const user = await auth.login(username, password); const session = await auth.newSession(user.id, req.headers['user-agent'], req.ip); @@ -37,7 +79,7 @@ router.post('/login', async (req, res) => { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000 // 7 giorni + maxAge: 7 * 24 * 60 * 60 * 1000 }; if (COOKIE_DOMAIN) { @@ -46,11 +88,11 @@ router.post('/login', async (req, res) => { res.cookie('auth_token', token, cookieOptions); - // Redirect alla pagina da cui l'utente e' arrivato, o alla console const destination = redirect || CONSOLE_URL; res.redirect(destination); } catch (err) { - console.error('[AUTH] Login failed:', err.message, err.stack); + console.error('[AUTH] Login failed:', err.message); + // Mai rivelare se è l'utente o la password ad essere sbagliati res.render('loginpage', { error: 'Credenziali non valide', redirect: redirect || '' }); } }); diff --git a/auth/src/routes/sessions.js b/auth/src/routes/sessions.js index 5a422c4..8bc787d 100644 --- a/auth/src/routes/sessions.js +++ b/auth/src/routes/sessions.js @@ -6,14 +6,49 @@ const userAuth = require('../middlewares/user.security'); router.use(userAuth); +// Mostra SOLO le sessioni dell'utente autenticato (non di tutti!) router.get('/', async (req, res) => { try { const result = await query( - 'SELECT id, user_id, session_code FROM sessions' + `SELECT id, ip_address, browser, os, device_type, + location_country, location_city, created_at, last_active, is_revoked + FROM sessions + WHERE user_id = $1 + ORDER BY last_active DESC`, + [req.user.user_id] ); res.json(result.rows); } catch (err) { - res.status(500).json({ error: 'Errore interno del server ' + err }); + console.error('[SESSIONS] Errore recupero sessioni:', err); + res.status(500).json({ error: 'Errore interno del server' }); + } +}); + +// Revoca una sessione specifica dell'utente +router.delete('/:sessionId', async (req, res) => { + const { sessionId } = req.params; + + // Validazione UUID + const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!UUID_REGEX.test(sessionId)) { + return res.status(400).json({ error: 'ID sessione non valido' }); + } + + try { + // Verifica che la sessione appartenga all'utente autenticato + const result = await query( + 'UPDATE sessions SET is_revoked = TRUE WHERE id = $1 AND user_id = $2 AND is_revoked = FALSE', + [sessionId, req.user.user_id] + ); + + if (result.rowCount === 0) { + return res.status(404).json({ error: 'Sessione non trovata o già revocata' }); + } + + res.json({ success: true, message: 'Sessione revocata' }); + } catch (err) { + console.error('[SESSIONS] Errore revoca sessione:', err); + res.status(500).json({ error: 'Errore interno del server' }); } }); diff --git a/auth/src/routes/users.js b/auth/src/routes/users.js index ce6ee2a..48cac6e 100644 --- a/auth/src/routes/users.js +++ b/auth/src/routes/users.js @@ -5,6 +5,12 @@ const { query } = require('../storage/database'); const userAuth = require('../middlewares/user.security'); const internalAuth = require('../middlewares/internal.security'); +// ─── VALIDAZIONE INPUT ────────────────────────────────────────────── +const USERNAME_REGEX = /^[a-zA-Z0-9_.\-]{3,50}$/; +const TELEGRAM_ID_REGEX = /^[0-9]{5,15}$/; + +// ─── ROTTE INTERNAL (prima del router.use userAuth) ───────────────── + router.get('/', internalAuth, async (req, res) => { try { const result = await query( @@ -12,9 +18,10 @@ router.get('/', internalAuth, async (req, res) => { ); res.json(result.rows); } catch (err) { - res.status(500).json({ error: 'Errore interno del server ' + err }); + console.error('[USERS] Errore lista utenti:', err); + res.status(500).json({ error: 'Errore interno del server' }); } -}) +}); router.get('/tonotify', internalAuth, async (req, res) => { try { @@ -23,9 +30,12 @@ router.get('/tonotify', internalAuth, async (req, res) => { ); res.json(result.rows); } catch (err) { - res.status(500).json({ error: 'Errore interno del server ' + err }); + console.error('[USERS] Errore lista notifiche:', err); + res.status(500).json({ error: 'Errore interno del server' }); } -}) +}); + +// ─── ROTTE USER (tutte le rotte sotto usano userAuth) ─────────────── router.use(userAuth); @@ -47,23 +57,33 @@ router.get('/me', async (req, res) => { } }); -// 2. Modificare l'username dell'utente router.put('/me/username', async (req, res) => { - // Si aspetta il nuovo parametro via query (?newUsername=Mario) o body se preferibile const newUsername = req.query.newUsername || req.body?.newUsername; - if (!newUsername) { + if (!newUsername || typeof newUsername !== 'string') { return res.status(400).json({ error: 'Nuovo username richiesto' }); } + // Validazione formato username + if (!USERNAME_REGEX.test(newUsername)) { + return res.status(400).json({ + error: 'Username non valido. Deve contenere 3-50 caratteri alfanumerici, underscore, punto o trattino.' + }); + } + try { - await query( - 'UPDATE users SET username = $1 WHERE id = $2', - [newUsername, req.user.user_id] // Nessuna informazione identitaria prelevata dal body + const result = await query( + 'UPDATE users SET username = $1 WHERE id = $2 RETURNING username', + [newUsername, req.user.user_id] ); - res.json({ success: true, message: 'Username aggiornato con successo' }); + + if (result.rowCount === 0) { + return res.status(404).json({ error: 'Utente non trovato' }); + } + + res.json({ success: true, username: result.rows[0].username }); } catch (err) { - if (err.code === '23505') { // UNIQUE constraint violation PostgreSQL + if (err.code === '23505') { return res.status(409).json({ error: 'Questo username è già in uso' }); } console.error('[USERS] Errore aggiornamento username:', err); @@ -71,22 +91,31 @@ router.put('/me/username', async (req, res) => { } }); -// 3. Modificare altri parametri (es. telegram_id) router.put('/me/telegram', async (req, res) => { const telegramId = req.query.telegramId || req.body?.telegramId; - if (!telegramId) { + if (!telegramId || typeof telegramId !== 'string') { return res.status(400).json({ error: 'Telegram ID richiesto' }); } + // Validazione formato Telegram ID (solo numeri, 5-15 cifre) + if (!TELEGRAM_ID_REGEX.test(telegramId)) { + return res.status(400).json({ + error: 'Telegram ID non valido. Deve contenere solo numeri (5-15 cifre).' + }); + } + try { await query( 'UPDATE users SET telegram_id = $1 WHERE id = $2', [telegramId, req.user.user_id] ); - res.json({ success: true, message: 'Telegram ID aggiornato con successo' }); + res.json({ success: true, message: 'Telegram ID aggiornato' }); } catch (err) { - console.error('[USERS] Errore aggiornamento parametro:', err); + if (err.code === '23505') { + return res.status(409).json({ error: 'Questo Telegram ID è già associato a un altro account' }); + } + console.error('[USERS] Errore aggiornamento telegram:', err); res.status(500).json({ error: 'Errore interno del server' }); } });