HIGH auth bypassnestjsdynamodb

Auth Bypass in Nestjs with Dynamodb

Auth Bypass in Nestjs with Dynamodb — how this specific combination creates or exposes the vulnerability

Auth bypass in a NestJS application backed by DynamoDB typically occurs when authorization logic is incomplete or misaligned with how DynamoDB returns data. Because DynamoDB is a NoSQL store, it does not enforce referential integrity or server-side session checks; it only returns items based on the keys and conditions you supply. If NestJS routes rely solely on the presence of a user record in DynamoDB (e.g., fetching by user ID from a token claim) without verifying ownership or role-based access, an attacker can manipulate identifiers to access another user’s data.

A common pattern is to decode a JWT in an interceptor or guard to extract a sub (user ID) and then query DynamoDB for that user’s record. If the route parameter (e.g., :userId) is used to construct a DynamoDB Key but not cross-checked against the authenticated subject, an attacker can change the userId in the request to access another user’s item. This is a Broken Level of Access (BOLA) / IDOR issue specific to the DynamoDB interaction: the API trusts the client-supplied identifier instead of deriving it from authenticated identity and ensuring it matches the DynamoDB key.

In NestJS, this can be exacerbated when using the AWS SDK directly without a data access layer that enforces ownership. For example, a controller might call docClient.get({ TableName, Key: { userId: req.params.userId } }) where req.params.userId is user-controlled. If the token payload contains sub but the code does not assert that req.params.userId === sub, the request will be authorized incorrectly. DynamoDB will return the item if the key exists, and NestJS may mistakenly treat that as proper authorization because the record exists.

Another vector involves role or scope checks stored in DynamoDB. If authorization decisions are made by fetching a user’s record from DynamoDB and checking a role or permissions attribute, but the fetch uses an ID controlled by the client, an attacker can enumerate valid user IDs and attempt privilege escalation by supplying IDs of users with higher privileges. Because DynamoDB does not prevent this, the application must enforce that the authenticated identity maps to the correct item and that sensitive attributes are not used for access decisions without server-side checks.

Middleware and guards in NestJS should treat DynamoDB as an authoritative data source but never as the sole enforcer of authorization. Use the authenticated subject from the request (e.g., from JWT) to construct DynamoDB keys and to filter queries, and always compare that subject against any user-provided identifiers. Combine this with rate limiting and input validation to reduce enumeration risks. The scanning tool checks for these mismatches by correlating authentication state with DynamoDB key construction and by testing whether changing identifiers leads to unauthorized data access.

Dynamodb-Specific Remediation in Nestjs — concrete code fixes

Remediation centers on ensuring the authenticated identity is the source of truth for DynamoDB keys and queries, and never allowing client-supplied identifiers to directly dictate which item is accessed.

First, enforce that the subject derived from authentication (e.g., JWT sub) is used as the partition key for DynamoDB operations. Do not trust route parameters for user identity. Below is a secure example using the AWS SDK for JavaScript v3 within a NestJS service:

import { DynamoDBClient, GetCommand } from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";

@Injectable()
export class UserService {
  constructor(
    private readonly ddb: DynamoDBClient,
    @Inject('AWS_DDB_TABLE') private readonly tableName: string,
  ) {}

  async getUserProfile(userId: string): Promise {
    // userId should come from the authenticated subject, not route params
    const command = new GetCommand({
      TableName: this.tableName,
      Key: { userId: { S: userId } },
    });
    const response = await this.ddb.send(command);
    if (!response.Item) {
      throw new NotFoundException('Profile not found');
    }
    return unmarshall(response.Item);
  }
}

In your controller, derive userId from the request context (e.g., req.user.sub) and pass it to the service. Do not use req.params.userId unless you have already validated that it matches the authenticated subject:

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':userId')
  getUser(@Param('userId') userId: string, @Req() req: Request) {
    const subjectId = req.user?.sub;
    if (subjectId !== userId) {
      throw new ForbiddenException('Cannot access other user data');
    }
    return this.userService.getUserProfile(userId);
  }
}

For queries that might need to scan or filter, use a composite key design where the partition key includes a tenant or user prefix, and always filter on the server side. For example, use a PK like USER# and a sort key like PROFILE, then query with a condition that binds the authenticated user to the partition key:

import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";

@Injectable()
export class ProfileService {
  constructor(
    private readonly ddb: DynamoDBClient,
    @Inject('AWS_DDB_TABLE') private readonly tableName: string,
  ) {}

  async listUserPosts(userId: string): Promise {
    const command = new QueryCommand({
      TableName: this.tableName,
      KeyConditionExpression: 'PK = :pk',
      ExpressionAttributeValues: {
        ':pk': { S: `USER#${userId}` },
      },
    });
    const response = await this.ddb.send(command);
    return response.Items?.map(unmarshall) ?? [];
  }
}

Additionally, apply input validation to ensure IDs conform to expected formats (e.g., UUID or numeric) to prevent injection or enumeration via invalid keys. Combine this with the scanner’s checks for BOLA and property authorization to confirm that every DynamoDB key construction is verified against the authenticated identity. The Pro plan’s continuous monitoring can help detect regressions where client-supplied identifiers might again influence DynamoDB key selection.

Related CWEs: authentication

CWE IDNameSeverity
CWE-287Improper Authentication CRITICAL
CWE-306Missing Authentication for Critical Function CRITICAL
CWE-307Brute Force HIGH
CWE-308Single-Factor Authentication MEDIUM
CWE-309Use of Password System for Primary Authentication MEDIUM
CWE-347Improper Verification of Cryptographic Signature HIGH
CWE-384Session Fixation HIGH
CWE-521Weak Password Requirements MEDIUM
CWE-613Insufficient Session Expiration MEDIUM
CWE-640Weak Password Recovery HIGH

Frequently Asked Questions

Can DynamoDB’s conditional writes prevent auth bypass in NestJS?
Conditional writes in DynamoDB (e.g., using ConditionExpression) are useful for data consistency but do not prevent authorization issues. They do not replace server-side checks that ensure the authenticated subject matches the item’s key. Use conditions for integrity, but enforce ownership in your application logic.
How does the scanner detect Auth Bypass with DynamoDB and NestJS?
The scanner checks whether DynamoDB key construction uses authenticated identity as the source of truth and verifies that client-controlled identifiers are not directly used to fetch items. It tests whether altering identifiers leads to unauthorized data access by probing endpoints with modified keys while authenticated as different subjects.