From 18ca30ee11cde5abff7982a2a9635a18d0cb3cee Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:20:57 +0000 Subject: [PATCH] feat(security): remove hardcoded admin email backdoor and enforce DB role checks - Removed hardcoded 'gustavo.caetano@gmail.com' from requireAdmin middleware. - Implemented robust RBAC checks using `user_roles` table. - Implemented legacy check for `is_admin` column in `users` table. - Preserved `ADMIN_EMAILS` environment variable fallback. - Added comprehensive unit tests in `backend/src/tests/auth-middleware.test.ts`. This fixes a critical privilege escalation vulnerability where a specific email address was granted admin access without database validation. Co-authored-by: criptogus <128640021+criptogus@users.noreply.github.com> --- backend/package-lock.json | 4 +- backend/src/middleware/auth.ts | 50 +++++++++--- backend/src/tests/auth-middleware.test.ts | 97 +++++++++++++++++++++++ 3 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 backend/src/tests/auth-middleware.test.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 3e35a9129..0d86eba77 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "liquid-ai-backend", + "name": "liquid-platform-backend", "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "liquid-ai-backend", + "name": "liquid-platform-backend", "version": "4.0.0", "license": "MIT", "dependencies": { diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 386ac3344..d9fed2d9f 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -116,26 +116,58 @@ export async function authenticateToken(req: AuthRequest, res: Response, next: N * Deve ser usado APÓS authenticateToken */ export async function requireAdmin(req: AuthRequest, res: Response, next: NextFunction) { - if (!req.user) { + if (!req.user || !req.userId) { return res.status(401).json({ error: 'Usuário não autenticado' }); } - // Lista de emails de admin (gustavo.caetano@gmail.com + variável de ambiente) - const defaultAdminEmails = ['gustavo.caetano@gmail.com']; - const envAdminEmails = (process.env.ADMIN_EMAILS || '').split(',').map(e => e.trim().toLowerCase()).filter(e => e); - const adminEmails = [...defaultAdminEmails, ...envAdminEmails].map(e => e.toLowerCase()); + // 1. Check Environment Variable (Config-based Admin) + const envAdminEmails = (process.env.ADMIN_EMAILS || '') + .split(',') + .map(e => e.trim().toLowerCase()) + .filter(e => e); const userEmail = req.user.email?.toLowerCase(); - if (!userEmail || !adminEmails.includes(userEmail)) { + if (userEmail && envAdminEmails.includes(userEmail)) { + console.log(`[AUTH] ✅ Admin access granted (ENV) for ${userEmail}`); + return next(); + } + + try { + // 2. Check Database Roles (RBAC) + // Check if user has 'admin' role in user_roles table + const hasAdminRole = await queryOne( + `SELECT 1 FROM user_roles WHERE user_id = $1 AND role = 'admin'`, + [req.userId] + ); + + if (hasAdminRole) { + console.log(`[AUTH] ✅ Admin access granted (ROLE) for ${userEmail}`); + return next(); + } + + // 3. Check User Flag (Legacy/Alternative) + // Check if user has is_admin flag in users table + const isUserAdmin = await queryOne( + `SELECT 1 FROM users WHERE id = $1 AND is_admin = true`, + [req.userId] + ); + + if (isUserAdmin) { + console.log(`[AUTH] ✅ Admin access granted (FLAG) for ${userEmail}`); + return next(); + } + + // If none of the above, deny access return res.status(403).json({ error: 'Acesso negado. Apenas administradores podem executar esta ação.', code: 'ADMIN_REQUIRED' }); - } - console.log(`[AUTH] ✅ Admin access granted for ${userEmail}`); - next(); + } catch (error) { + console.error('[AUTH] Error checking admin status:', error); + return res.status(500).json({ error: 'Erro ao verificar permissões' }); + } } // Alias para compatibilidade com Password Vault diff --git a/backend/src/tests/auth-middleware.test.ts b/backend/src/tests/auth-middleware.test.ts new file mode 100644 index 000000000..7754828c6 --- /dev/null +++ b/backend/src/tests/auth-middleware.test.ts @@ -0,0 +1,97 @@ +import { requireAdmin, AuthRequest } from '../middleware/auth'; +import { queryOne } from '../services/database'; +import { Response, NextFunction } from 'express'; + +// Mock database +jest.mock('../services/database', () => ({ + queryOne: jest.fn(), + query: jest.fn(), +})); + +describe('requireAdmin Middleware Security Fix', () => { + let mockReq: Partial; + let mockRes: Partial; + let mockNext: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockReq = { + user: { + id: 'user-123', + email: 'test@example.com', + }, + userId: 'user-123' + }; + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + mockNext = jest.fn(); + // Reset env vars + process.env.ADMIN_EMAILS = ''; + }); + + it('should deny access if user is not authenticated', async () => { + mockReq.user = undefined; + await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction); + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should deny access for regular user', async () => { + (queryOne as jest.Mock).mockResolvedValue(null); // Not found in DB checks + + await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should GRANT access if user email is in ADMIN_EMAILS env var', async () => { + process.env.ADMIN_EMAILS = 'test@example.com'; + + await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction); + + expect(mockNext).toHaveBeenCalled(); + expect(mockRes.status).not.toHaveBeenCalled(); + }); + + it('should DENY access for hardcoded "gustavo.caetano@gmail.com" if not in DB or Env', async () => { + mockReq.user!.email = 'gustavo.caetano@gmail.com'; + (queryOne as jest.Mock).mockResolvedValue(null); // Not in DB + + await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction); + + // If the vulnerability is present, this expectation will FAIL (it would call next()) + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should GRANT access if user has admin role in user_roles', async () => { + // Mock finding admin role + (queryOne as jest.Mock).mockResolvedValueOnce({ 1: 1 }); // Found row + + await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction); + + expect(queryOne).toHaveBeenCalledWith( + expect.stringContaining('user_roles'), + expect.arrayContaining(['user-123']) + ); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should GRANT access if user has is_admin=true in users table', async () => { + // Mock NOT finding in user_roles first + (queryOne as jest.Mock).mockResolvedValueOnce(null); + // Mock finding is_admin in users + (queryOne as jest.Mock).mockResolvedValueOnce({ is_admin: true }); + + await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction); + + expect(queryOne).toHaveBeenCalledWith( + expect.stringContaining('users'), + expect.arrayContaining(['user-123']) + ); + expect(mockNext).toHaveBeenCalled(); + }); +});