Skip to content
Mrozowski Digital

NotesBuilding with AI3 min read

Hiding a Page Does Not Protect Your Data

Removing a button from the screen does not remove someone’s ability to make the request behind it.

Piotr Mrozowski — Founder, Mrozowski International

You ask AI to make the admin section accessible only to administrators (it will likely put it under the /admin route - one of the most frequently probed and attacked route of all, but that’s a different topic).

It hides the navigation link, removes a few buttons and redirects regular users to the dashboard.

You log in with a normal account. No admin section. Looks secure.

But what happens if someone requests the admin data directly, without going through your carefully arranged navigation?

If the server sends it back, you have hidden the entrance without actually restricting access.

We already added authentication, though

In the authentication article, we looked at why AI should integrate an established authentication system instead of inventing one.

That handles confirming who a user is.

Authorization answers a different question: what is this particular user allowed (and more importantly - not allowed) to do?

Being logged in should not automatically let someone:

  • read another customer’s invoices;
  • change another company’s settings;
  • delete records intended to be read-only;
  • use administrative functions.

An authentication provider can supply identity, roles and other useful information. Your application still needs to enforce the permissions that apply to its data and actions.

“User is logged in” is a fairly incomplete security policy for an application with more than one user.

The browser is not in charge

When you click a button, the browser often sends a request to the server. That request might fetch invoices, update a profile or delete a file.

The button is a convenient way to trigger it. It is not the only way.

Someone can send a request without clicking the button or even opening the page where it appears.

This is why hiding controls is useful for the interface but insufficient for security. Users should see the actions available to them, and the backend must independently enforce those restrictions.

Permission checks need to happen for every protected request, before data is returned or an action is performed. When permission has not been established, access should be denied. This is also the approach recommended by OWASP’s authorization guidance. (cheatsheetseries.owasp.org)

Your invoice, someone else’s invoice

Imagine you build a customer portal.

A customer opens an invoice at an address like:

/invoices/123

The application checks that they are logged in and loads invoice 123.

But does it check whether they are allowed to see that invoice?

In an insecure implementation, changing the number to 124 could return someone else’s invoice. The login system may be working perfectly. The missing check is access to the specific record.

Using long, random identifiers does not remove the need for that check. An identifier tells the application which record someone wants. It does not prove they are entitled to it.

The same applies to changes. Permission to view an invoice does not necessarily include permission to edit or delete it.

Filtering after the data arrives is too late

There is another version of this mistake that can look completely correct on screen.

The application fetches every customer’s invoices, then filters the list in the browser so the current customer sees only theirs.

The page shows the right records. Unfortunately, the other records have already been sent to their device.

They do not need to appear in a table to be exposed.

For a customer-facing request, the backend should return only the records and fields that customer is permitted to access.

Ask AI where the filtering happens. “Only your invoices are displayed” and “only your invoices are returned” are different claims.

Test with more than your own account

If you build and test everything using an administrator account, it is easy to miss these problems. Administrators are often allowed to do almost everything anyway.

Use separate test accounts with different permissions. Include two ordinary users who should not share access to each other’s records.

Check that permitted actions work, then check that forbidden ones fail even when requested directly.

A redirect to the dashboard proves that a redirect happened. It does not prove the data behind that page is protected.

A prompt you can reuse

Review authorization for the feature being implemented.

Identify which users can read, create, update and delete the relevant data.
Include record ownership, organization membership and role permissions where
applicable. Flag unclear access rules before implementing them.

Enforce these rules in the trusted backend or database access policies.
Do not rely on hidden buttons, client-side redirects or browser-side filtering.

Validate permission for every protected request and the specific records
involved. Deny access when permission has not been established.

Use the verified session to establish identity. Do not trust user IDs,
organization IDs or roles supplied by the browser as proof of permission.

Return only the records and fields the requesting user is allowed to access.

Using test accounts and test data, verify:
- A logged-out user cannot access protected data or actions.
- One ordinary user cannot access another user's private records.
- A user cannot access another organization's private data.
- Restricted actions fail when requested directly.
- Authorized users can still complete the intended flow.

Run the relevant checks and report what was actually verified, along with
anything that could not be tested.

Keep the hidden buttons and sensible redirects. They make the application easier to use.

Just make sure that when someone skips the interface entirely, the same restrictions still apply.