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
This commit is contained in:
Jonas Pacheco 2026-04-01 17:26:28 -03:00
parent 4fb3f87dc3
commit a2aff30b8c
4 changed files with 65 additions and 8 deletions

View File

@ -30,12 +30,14 @@ async function comparePasswords(supplied: string, stored: string) {
return timingSafeEqual(hashedBuf, suppliedBuf); return timingSafeEqual(hashedBuf, suppliedBuf);
} }
if (!process.env.SESSION_SECRET) { // CORREÇÃO: Session secret seguro - obrigatório em produção
console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production."); 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 = { const sessionSettings: session.SessionOptions = {
secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`, secret: SESSION_SECRET,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
store: storage.sessionStore, store: storage.sessionStore,

View File

@ -6,9 +6,26 @@ import ws from "ws";
neonConfig.webSocketConstructor = ws; neonConfig.webSocketConstructor = ws;
const app = express(); 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()); app.use(express.json());
const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const pool = new Pool({ connectionString: process.env.DATABASE_URL });

View File

@ -182,11 +182,22 @@ export async function registerRoutes(
// Arcádia Plus - SSO routes (proxy already registered at top) // Arcádia Plus - SSO routes (proxy already registered at top)
app.use("/api/plus/sso", plusSsoRoutes); app.use("/api/plus/sso", plusSsoRoutes);
// CORREÇÃO: /api/tenants protegido - apenas admin vê todos
app.get("/api/tenants", async (req: any, res) => { app.get("/api/tenants", async (req: any, res) => {
if (!req.isAuthenticated()) { if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
} }
try { 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(); const tenants = await storage.getTenants();
res.json(tenants); res.json(tenants);
} catch (error) { } catch (error) {

View File

@ -15,7 +15,7 @@ export interface IStorage {
sessionStore: session.Store; sessionStore: session.Store;
getUser(id: string): Promise<User | undefined>; getUser(id: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>; getUserByUsername(username: string, tenantId?: number): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>; createUser(user: InsertUser): Promise<User>;
getEnrichedUser(user: User): Promise<any>; getEnrichedUser(user: User): Promise<any>;
@ -28,6 +28,7 @@ export interface IStorage {
getUserApplications(userId: string): Promise<Application[]>; getUserApplications(userId: string): Promise<Application[]>;
assignApplicationToUser(userId: string, applicationId: string): Promise<void>; assignApplicationToUser(userId: string, applicationId: string): Promise<void>;
removeApplicationFromUser(userId: string, applicationId: string): Promise<void>; removeApplicationFromUser(userId: string, applicationId: string): Promise<void>;
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 }[]>; getTenants(): Promise<{ id: number; name: string; slug: string }[]>;
} }
@ -46,8 +47,21 @@ export class DatabaseStorage implements IStorage {
return user; return user;
} }
async getUserByUsername(username: string): Promise<User | undefined> { // CORREÇÃO: getUserByUsername com validação de tenant
async getUserByUsername(username: string, tenantId?: number): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.username, username)); 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; return user;
} }
@ -146,6 +160,19 @@ export class DatabaseStorage implements IStorage {
return enriched; 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 }[]> { async getTenants(): Promise<{ id: number; name: string; slug: string }[]> {
const result = await db.select({ const result = await db.select({
id: tenants.id, id: tenants.id,