Skip to content
Hugo Méndez
  • Security
  • NestJS
  • Best practices

The most common security holes in internal business apps (and how to close them)

"It is only used by employees" is not a security model. IDOR, client-side filters, leaky exports, mass assignment and other issues I find in nearly every internal-app audit.

Hugo MéndezAugust 4, 20262 min read

“It’s an internal app, only employees use it.” I hear that before almost every audit, and it almost always ends the same way: any logged-in user can see data they shouldn’t. In HR, procurement or audit systems that means salaries, supplier terms or whistleblower reports.

1. Changing the ID in the URL (IDOR)

/api/requests/6511a... returns the request without checking it’s yours. Put the user’s scope in the query, not in an if afterwards:

const doc = await this.model.findOne({ _id: id, ...this.scope.for(user) });
if (!doc) throw new NotFoundException(); // same response whether it doesn't exist or isn't yours

Return 404 in both cases; different responses reveal which IDs exist.

2. Filters that only exist in the frontend

The table shows “my site”, but the endpoint returns everything and the browser filters. Security filters live on the server.

3. List protected, detail not (or vice versa)

GET /items is scoped, but GET /items/:id, /items/:id/file or /items/export aren’t. Every route returning data uses the same scope rule, centralized in one service.

4. Exports leaking columns

The screen hides “salary”, the Excel export includes it. Remove sensitive columns server-side.

5. Mass assignment

“Update my profile” saves the whole client object and someone adds "role": "admin". Use whitelisted DTOs:

app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));

6. Guessable file URLs

Attachments served as static files without a session. Serve them through an endpoint that checks permissions and logs downloads.

7. Delegation without a ceiling

Managers can delegate more than they have. Nobody should grant a broader scope than their own.

8. No audit log

Record who did what, on which record, when and from where; for sensitive data, reads too.

My method: the failing test first

  1. Write a test that reproduces the attack (user A requests user B’s record).
  2. Confirm it fails (the hole exists).
  3. Fix it.
  4. The test passes and stays in the suite so the hole never comes back.

Have an internal system that never went through a review like this? Let’s audit it.

Back to blog

Related services

Got a process that has outgrown spreadsheets?

Tell me what you need. Within one business day I will reply with a clear idea of how to solve it and what it costs.