Broken Access Control in Nestjs with Hmac Signatures
Broken Access Control in Nestjs with Hmac Signatures
Broken Access Control occurs when API endpoints do not properly enforce who can access them and what they are allowed to do. In NestJS applications that use Hmac Signatures for request authentication, misconfiguration or inconsistent validation can turn an integrity check into a bypass path, leading to unauthorized access and privilege escalation.
Hmac Signatures are commonly used to guarantee integrity and authenticity of requests. A client computes a signature from the request method, path, timestamp, and a secret, then sends it in a header (e.g., x-api-signature and x-api-timestamp). The server recomputes the signature and compares it before authorizing the request. When access control rules are not applied uniformly after signature validation, an authenticated (but not authorized) caller can reach endpoints they should not. Common patterns include:
- Validating the Hmac but skipping role- or scope-based authorization checks.
- Using the same verification middleware for all routes, including admin or sensitive endpoints, without additional checks.
- Relying on the timestamp window to prevent replay but failing to bind the signature to the intended HTTP method and path, enabling signature reuse across routes with different access requirements.
Consider a NestJS controller with public read endpoints and an admin-only write endpoint. If the Hmac verification middleware is applied globally but authorization is only checked in some handlers, a caller with a valid signature can invoke the admin endpoint and perform actions reserved for privileged roles. This is a classic Broken Access Control issue: authentication (Hmac) succeeds, but authorization fails.
Another realistic scenario involves route-level parameter constraints. An endpoint like /users/:userId/activate may verify the Hmac but not ensure the authenticated principal is allowed to activate the target user (e.g., users can only activate their own accounts unless they are admins). Without explicit ownership or role checks, the signature validation alone does not prevent IDOR-like access violations in the context of access control.
To illustrate, a vulnerable route might look like this: the Hmac is verified, but there is no check that the requesting user can only modify their own data unless they are an admin:
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Post(':userId/activate')
@UseInterceptors(HmacValidationInterceptor)
async activateUser(@Param('userId') userId: string, @Request() req) {
// BUG: Hmac validated, but no check that req.user can activate this userId
return this.usersService.activate(userId);
}
}
In this example, HmacValidationInterceptor ensures the request has not been tampered with, but it does not confirm that the requester is allowed to activate the specific user. This is a Broken Access Control flaw because the authorization decision is incomplete.
Hmac Signatures-Specific Remediation in Nestjs — concrete code fixes
Remediation centers on ensuring that Hmac verification is followed by explicit, context-aware authorization and that the signature scope tightly binds to the intended operation. Below are concrete, safe patterns to implement in NestJS.
1. Decouple verification and authorization: Use the Hmac interceptor only for integrity/authenticity, then enforce role- and ownership-based checks in the handler or via Guards.
2. Bind the signature to the full request context: Include the HTTP method and path in the signed payload and reject requests where any component changes. Do not allow signature reuse across different endpoints or methods.
3. Apply least privilege: Scope tokens or signatures to the minimal set of endpoints and data a principal is allowed to access. Enforce this with route guards.
Here is a secure example combining Hmac validation with explicit ownership checks. The Hmac interceptor verifies integrity, and a custom Guard ensures the user can only activate their own account unless they have admin privileges:
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService, private reflector: Reflector) {}
@Post(':userId/activate')
@UseInterceptors(HmacValidationInterceptor)
@UseGuards(HmacAuthGuard, OwnershipOrAdminGuard)
async activateUser(
@Param('userId') userId: string,
@Request() req
// Safe: Hmac integrity verified and ownership/admin check enforced
return this.usersService.activate(userId);
}
}
// Guard that checks ownership or admin role
@Injectable()
export class OwnershipOrAdminGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
const user = req.user; // set by HmacAuthGuard after verifying principal
const userId = req.params.userId;
if (user.role === 'admin') return true;
return user.id === userId;
}
}
// Guard that ensures a valid principal after Hmac validation
@Injectable()
export class HmacAuthGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
// The HmacValidationInterceptor should have attached a verified principal
if (!req.user) {
throw new UnauthorizedException('Invalid or missing principal');
}
return true;
}
}
Below is a minimal Hmac verification implementation that includes the method and path in the signed payload, preventing path-based signature reuse:
import { createHmac, timingSafeEqual } from 'crypto';
export class HmacSignatureUtils {
static verify(request: any, secret: string): boolean {
const receivedSignature = request.headers['x-api-signature'];
const timestamp = request.headers['x-api-timestamp'];
const method = request.method;
const url = request.url; // path without query params
if (!receivedSignature || !timestamp) return false;
if (Math.abs(Date.now() - parseInt(timestamp, 10)) > 300000) return false; // 5 min window
const payload = `${method}|${url}|${timestamp}`;
const expected = createHmac('sha256', secret).update(payload).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSignature));
}
}
In your interceptor, use this utility to validate the signature, then attach a verified principal to the request before passing to the next middleware or handler:
@Injectable()
export class HmacValidationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest();
if (!HmacSignatureUtils.verify(req, process.env.HMAC_SECRET)) {
throw new UnauthorizedException('Invalid signature');
}
// Derive or map to a principal; in practice this may involve a lookup
req.user = { id: 'user-123', role: 'user' }; // example attached principal
return next.handle();
}
}
By combining these patterns, you ensure that Hmac Signatures provide integrity while NestJS guards and handlers enforce proper access control, eliminating the conditions that lead to Broken Access Control.