July 5, 2026
Your Admin Panel Isn’t Protected. It’s Just Hidden.
Hiding a button isn’t the same as blocking the request behind it.

By Vibesecurity
3 min read
Hiding a button isn't the same as blocking the request behind it.
I've lost count of how many "admin panels" I've walked into by literally just asking for them.
Not hacking. Not exploiting a zero-day. Just typing /admin, or pulling a route name out of a JS bundle and hitting it directly with a plain GET request. No token forgery, no session tricks. Just the front door — except nobody locked it, they just took the doorknob off and hoped that would stop people.
It never does. And this exact mistake shows up so often, across companies with real security budgets, that I stopped treating it as a rare bug. It's a default state teams have to actively fight their way out of.
The mistake is always the same shape
A frontend engineer builds an admin dashboard and gates it the way frontend engineers are trained to: conditionally rendering a component. if (user.role === 'admin') { showAdminPanel() }. Ships clean, reads well in the PR.
The problem: this check only decides what gets drawn on screen. It decides nothing about what the server is willing to do. The API behind that panel — /api/admin/users, /api/admin/refunds — was built assuming the only callers are people who clicked through the hidden UI. That assumption is the vulnerability. Open dev tools, read the bundled JS, call the endpoint directly with your own regular session, and the role check is gone — because it was in React. Never in the backend.
Why "hidden" feels safe when it isn't
Hiding the button feels like control. QA logs in as a normal user, the admin link is gone, ticket closed. That test only verifies the UI. It never asks the one question every attacker asks first: what happens if I skip the UI and talk to the API myself?
And "nobody will guess the route" doesn't hold up:
- It's in your JS bundle in plaintext — client-side code has to reference it somehow.
- It's in your exposed Swagger/OpenAPI doc, if you have one.
- It's in a decompiled mobile APK.
- It's on any wordlist scanner running common ops terms:
internal,staff,backoffice,console.
Obscurity buys minutes against a human. It buys nothing against automated discovery.
What I actually check, in order
- Log in at the lowest privilege tier available.
- Grep every JS bundle for route-shaped strings —
/admin,/internal,/manage,/staff,/ops. - Check for exposed schemas —
/swagger.json, GraphQL introspection left on. It's a menu of every backend function, sorted for you. - Replay each discovered route directly, as the low-privilege session — no UI involved. This step is the whole test.
- Read the payload, not just the status code. A
200 OKwith an empty array looks identical to a blocked request until you check whether it should've returned 40,000 records. - Check writes, not just reads. An exposed
DELETE /api/admin/users/:idis a very different severity than an exposed read.
No exotic tooling required. Burp, curl, and remembering to ask the question QA never asked.
The fix: move the check, don't hide it better
- The button's role check is a UX nicety. The one that matters lives in middleware, on the server, on every request.
- Every admin-scoped endpoint checks the caller's current role — not a client-side assumption about it.
- Obscure paths, custom headers, hidden docs — none of that is an authorization control. Treat it as attack surface, not protection.
The uncomfortable part
This bug isn't hard to fix — it's often a one-line middleware addition. It survives because it's invisible during normal development. The demo works. The button really is hidden. Nobody ever asks what happens when someone skips the part of the app they built.
Go check your admin routes today. Not through the UI — directly. Ten minutes is the difference between a duplicate report and a critical one.