DEV Community

Cover image for Four things I would check first with NestJS if a bank's API kept leaking sensitive data
Peace Melodi
Peace Melodi

Posted on

Four things I would check first with NestJS if a bank's API kept leaking sensitive data

A bank's API rarely leaks sensitive data on purpose. Nobody writes a line of code that says send the customer's full account number to anyone who asks. It happens in smaller, quieter ways, a response object that includes more fields than the frontend actually needed, an authorization check that confirms someone is logged in but never checks whether they should see this specific record, a log line that was only meant for debugging but ended up storing a full card number in plain text.

If I were brought in to look at a bank or fintech API that had a data exposure problem, these are the four places I would check first, in the order I would check them.

First, what the response actually sends back, not what the code assumes it sends

The most common leak I see is not a security bug in the traditional sense, it is an entity object being returned directly from the database without shaping what actually leaves the server.

@Entity()
export class Customer {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  fullName: string;

  @Column()
  nationalIdNumber: string;

  @Column()
  passwordHash: string;
}

@Controller('customers')
export class CustomersController {
  @Get(':id')
  async getCustomer(@Param('id') id: string) {
    return this.customerService.findById(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Whatever fields exist on that entity get sent to the client, including a national id number and a password hash that the frontend never needed and should never have been able to see. The fix is controlling the shape of the response explicitly, rather than trusting that nobody will notice the extra fields.

export class CustomerResponseDto {
  id: string;
  fullName: string;

  @Exclude()
  nationalIdNumber: string;

  @Exclude()
  passwordHash: string;
}

@Controller('customers')
@UseInterceptors(ClassSerializerInterceptor)
export class CustomersController {
  @Get(':id')
  async getCustomer(@Param('id') id: string): Promise<CustomerResponseDto> {
    const customer = await this.customerService.findById(id);
    return plainToInstance(CustomerResponseDto, customer);
  }
}
Enter fullscreen mode Exit fullscreen mode

With the serializer interceptor in place, excluded fields never make it into the response, no matter what the underlying entity contains, and no matter what gets added to that entity later by someone who forgets this rule exists.

Second, whether the requester should see this specific record, not just whether they are logged in

Being authenticated answers one question, who is this person. It does not answer a second, completely separate question, should this specific person see this specific record. A shockingly common bug in financial APIs is a route that checks for a valid token, then happily returns any account by id, regardless of whether it belongs to the person asking.

@Injectable()
export class AccountOwnershipGuard implements CanActivate {
  constructor(private readonly accountsService: AccountsService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const requestedAccountId = request.params.id;
    const requestingUserId = request.user.id;

    const account = await this.accountsService.findById(requestedAccountId);

    if (!account || account.ownerId !== requestingUserId) {
      throw new ForbiddenException('You do not have access to this account');
    }

    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

This guard sits alongside authentication, not instead of it, and closes the gap between someone being a valid user and someone being the right user for this particular piece of data.

Third, what actually ends up in the logs

Logging is where I have seen sensitive data leak in ways nobody intended and almost nobody notices, since logs are rarely reviewed with the same scrutiny as an API response. A request or error logger that dumps the full request body during debugging can quietly store card numbers, national id numbers, or full addresses in plain text log files for months.

@Injectable()
export class RedactingLoggerInterceptor implements NestInterceptor {
  private readonly sensitiveFields = ['cardNumber', 'nationalIdNumber', 'password'];

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const redactedBody = { ...request.body };

    for (const field of this.sensitiveFields) {
      if (redactedBody[field]) {
        redactedBody[field] = '[redacted]';
      }
    }

    Logger.log(`Incoming request: ${JSON.stringify(redactedBody)}`);

    return next.handle();
  }
}
Enter fullscreen mode Exit fullscreen mode

Redacting known sensitive fields before anything gets written to a log means a debugging session six months from now never accidentally becomes the reason a customer's data ended up somewhere it should not be.

Fourth, whether the API can be scraped one record at a time

The last thing I would check is not a single leaked field, it is whether the API allows someone to quietly pull large amounts of data by simply guessing or iterating through ids, one request at a time, faster than any human would ever do it manually.

@Controller('accounts')
@UseGuards(ThrottlerGuard)
@Throttle({ default: { limit: 20, ttl: 60000 } })
export class AccountsController {
  @Get(':id')
  @UseGuards(AccountOwnershipGuard)
  async getAccount(@Param('id') id: string) {
    return this.accountsService.findById(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Combined with the ownership guard from earlier, this limits how many requests a single client can make in a given window, which makes large scale scraping meaningfully slower and easier to notice, even in the rare case where an authorization check elsewhere has a gap.

The bigger picture

None of these four checks are exotic. Controlling exactly what a response contains, confirming ownership and not just identity, keeping sensitive fields out of logs, and limiting how fast an API can be queried, these are all things NestJS makes genuinely straightforward through interceptors, guards, and built in throttling. The hard part is rarely the tooling, it is remembering to apply these checks consistently across every endpoint that touches sensitive data, not just the ones that feel obviously risky.

If your team is building or maintaining an API that handles financial or personal data and wants a second set of eyes checking for exactly these kinds of gaps, that is exactly the kind of review I would be glad to help with.

I am Peace Melodi, a backend software engineer. If you want your business to scale big, comfortably handling millions of users without breaking, with strong scalability and security in place, feel free to reach out.

LinkedIn: https://www.linkedin.com/in/melodi-peace-406494368
GitHub: https://github.com/PeaceMelodi

Top comments (0)