From a2aff30b8cbf48c6ca287ef03b20375d1bd2b92f Mon Sep 17 00:00:00 2001 From: Jonas Pacheco Date: Wed, 1 Apr 2026 17:26:28 -0300 Subject: [PATCH] security: correcoes semana 1 - session secret, CORS, filtro tenant, protecao /api/tenants - Session secret com randomBytes(32), obrigatorio em producao - CORS whitelist configurado (origens permitidas) - getUserByUsername com validacao opcional de tenantId - /api/tenants restrito: admin ve todos, user ve so seu tenant - Metodo getTenant(id) adicionado a interface IStorage --- server/auth.ts | 10 ++++++---- server/communication/engine.ts | 21 +++++++++++++++++++-- server/routes.ts | 11 +++++++++++ server/storage.ts | 31 +++++++++++++++++++++++++++++-- 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/server/auth.ts b/server/auth.ts index 76f2d37..e79fe64 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -30,12 +30,14 @@ async function comparePasswords(supplied: string, stored: string) { return timingSafeEqual(hashedBuf, suppliedBuf); } -if (!process.env.SESSION_SECRET) { - console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production."); -} +// CORREÇÃO: Session secret seguro - obrigatório em produção +const SESSION_SECRET = process.env.SESSION_SECRET || + (process.env.NODE_ENV === 'production' + ? (() => { throw new Error('SESSION_SECRET obrigatório em produção'); })() + : randomBytes(32).toString('hex')); const sessionSettings: session.SessionOptions = { - secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`, + secret: SESSION_SECRET, resave: false, saveUninitialized: false, store: storage.sessionStore, diff --git a/server/communication/engine.ts b/server/communication/engine.ts index 8d9017b..fcbc17e 100644 --- a/server/communication/engine.ts +++ b/server/communication/engine.ts @@ -6,9 +6,26 @@ import ws from "ws"; neonConfig.webSocketConstructor = ws; const app = express(); -const PORT = 8006; +const PORT = process.env.PORT || 8006; -app.use(cors()); +// CORREÇÃO: CORS restrito - whitelist de origens permitidas +const allowedOrigins = [ + 'http://localhost:5000', + 'https://localhost:5000', + process.env.FRONTEND_URL, + process.env.DOMAIN ? `https://${process.env.DOMAIN}` : null, +].filter(Boolean); + +app.use(cors({ + origin: (origin, callback) => { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('CORS não permitido')); + } + }, + credentials: true +})); app.use(express.json()); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); diff --git a/server/routes.ts b/server/routes.ts index 367e6eb..7139198 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -182,11 +182,22 @@ export async function registerRoutes( // Arcádia Plus - SSO routes (proxy already registered at top) app.use("/api/plus/sso", plusSsoRoutes); + // CORREÇÃO: /api/tenants protegido - apenas admin vê todos app.get("/api/tenants", async (req: any, res) => { if (!req.isAuthenticated()) { return res.status(401).json({ error: "Authentication required" }); } try { + // Se não for admin, retornar apenas o tenant do usuário + if (req.user?.role !== 'admin') { + if (req.user?.tenantId) { + const tenant = await storage.getTenant(req.user.tenantId); + return res.json(tenant ? [tenant] : []); + } + return res.status(403).json({ error: "Access denied" }); + } + + // Admin vê todos const tenants = await storage.getTenants(); res.json(tenants); } catch (error) { diff --git a/server/storage.ts b/server/storage.ts index c589380..4a267f1 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -15,7 +15,7 @@ export interface IStorage { sessionStore: session.Store; getUser(id: string): Promise; - getUserByUsername(username: string): Promise; + getUserByUsername(username: string, tenantId?: number): Promise; createUser(user: InsertUser): Promise; getEnrichedUser(user: User): Promise; @@ -28,6 +28,7 @@ export interface IStorage { getUserApplications(userId: string): Promise; assignApplicationToUser(userId: string, applicationId: string): Promise; removeApplicationFromUser(userId: string, applicationId: string): Promise; + getTenant(id: number): Promise<{ id: number; name: string; slug: string; tenantType?: string; plan?: string; status?: string } | undefined>; getTenants(): Promise<{ id: number; name: string; slug: string }[]>; } @@ -46,8 +47,21 @@ export class DatabaseStorage implements IStorage { return user; } - async getUserByUsername(username: string): Promise { + // CORREÇÃO: getUserByUsername com validação de tenant + async getUserByUsername(username: string, tenantId?: number): Promise { const [user] = await db.select().from(users).where(eq(users.username, username)); + + // Se tenantId foi passado, validar que usuário pertence a esse tenant + if (tenantId && user) { + const [tenantUser] = await db.select() + .from(tenantUsers) + .where(and( + eq(tenantUsers.userId, user.id), + eq(tenantUsers.tenantId, tenantId) + )); + if (!tenantUser) return undefined; + } + return user; } @@ -146,6 +160,19 @@ export class DatabaseStorage implements IStorage { return enriched; } + // CORREÇÃO: getTenant para buscar tenant específico + async getTenant(id: number): Promise<{ id: number; name: string; slug: string; tenantType?: string; plan?: string; status?: string } | undefined> { + const [tenant] = await db.select({ + id: tenants.id, + name: tenants.name, + slug: tenants.slug, + tenantType: tenants.tenantType, + plan: tenants.plan, + status: tenants.status + }).from(tenants).where(eq(tenants.id, id)); + return tenant; + } + async getTenants(): Promise<{ id: number; name: string; slug: string }[]> { const result = await db.select({ id: tenants.id,