1
0

Merge pull request #1094 from bigcapitalhq/fix/tenant-bypass-organization-id-header

fix(server): prevent cross-tenant access via organization-id header
This commit is contained in:
Ahmed Bouhuolia
2026-05-15 21:19:36 +02:00
committed by GitHub
3 changed files with 26 additions and 5 deletions
@@ -18,7 +18,7 @@ import { createBullBoardAuthMiddleware } from '@/middleware/bull-board-auth.midd
import { BullModule } from '@nestjs/bullmq';
import { ScheduleModule } from '@nestjs/schedule';
import { PassportModule } from '@nestjs/passport';
import { ClsModule, ClsService } from 'nestjs-cls';
import { ClsModule } from 'nestjs-cls';
import { AppController } from './App.controller';
import { AppService } from './App.service';
import { ItemsModule } from '../Items/Items.module';
@@ -169,9 +169,6 @@ import { AppThrottleModule } from './AppThrottle.module';
global: true,
middleware: {
mount: true,
setup: (cls: ClsService, req: Request, res: Response) => {
cls.set('organizationId', req.headers['organization-id']);
},
generateId: true,
saveReq: true,
},
@@ -2,6 +2,7 @@ import { ClsService } from 'nestjs-cls';
import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { ModelObject } from 'objection';
import { JwtPayload } from '../Auth.interfaces';
import { InvalidEmailPasswordException } from '../exceptions/InvalidEmailPassword.exception';
@@ -12,6 +13,10 @@ export class AuthSigninService {
constructor(
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
@Inject(TenantModel.name)
private readonly tenantModel: typeof TenantModel,
private readonly jwtService: JwtService,
private readonly clsService: ClsService,
) { }
@@ -49,6 +54,7 @@ export class AuthSigninService {
*/
async verifyPayload(payload: JwtPayload): Promise<any> {
let user: SystemUser;
let tenant: TenantModel | undefined;
try {
user = await this.systemUserModel
@@ -56,8 +62,14 @@ export class AuthSigninService {
.findOne({ email: payload.sub })
.throwIfNotFound();
tenant = await this.tenantModel
.query()
.findById(user.tenantId)
.throwIfNotFound();
this.clsService.set('tenantId', user.tenantId);
this.clsService.set('userId', user.id);
this.clsService.set('organizationId', tenant.organizationId);
} catch (error) {
throw new UserNotFoundException(String(payload.sub));
}
@@ -7,6 +7,7 @@ import {
SetMetadata,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ClsService } from 'nestjs-cls';
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
import { getAuthApiKey } from '../Auth/Auth.utils';
@@ -16,7 +17,10 @@ export const TenantAgnosticRoute = () => SetMetadata(IS_TENANT_AGNOSTIC, true);
@Injectable()
export class TenancyGlobalGuard implements CanActivate {
constructor(private reflector: Reflector) {}
constructor(
private readonly reflector: Reflector,
private readonly clsService: ClsService,
) {}
/**
* Validates the organization ID in the request headers.
@@ -43,6 +47,14 @@ export class TenancyGlobalGuard implements CanActivate {
if (!organizationId) {
throw new UnauthorizedException('Organization ID is required.');
}
const authenticatedOrganizationId =
this.clsService.get('organizationId');
if (
authenticatedOrganizationId &&
authenticatedOrganizationId !== organizationId
) {
throw new UnauthorizedException('Organization mismatch.');
}
return true;
}
}