Disabled but Shipped: Reading a Delete Action From a Disabled Form
The challenge
This admin dashboard greys out the 'Delete user' button so it cannot be clicked. But the form behind it still ships in the HTML, action and all. Open View source, find the address the delete action posts to, and submit it.
What you'll learn
- Understand that disabling a button is a client-side display state, not a server check
- Read a form action URL from a page's View source
- Find the target endpoint behind a control the UI greyed out
- Recognise that hidden or disabled controls still ship their target to the client
- Explain why authorization must be enforced server-side on every request
Skills tested
Prerequisites
- Knowing how to open View source in a browser
- Basic idea that forms post to a URL (their action)
How it works
Web UIs often dim or disable a control the current user is not allowed to use - a greyed-out Delete user button, for instance. This feels like a safety measure, but disabling a button only prevents a click inside that browser. The form the button belongs to, including the URL it would post to (its action), is still part of the HTML that was sent to the page.
That means the target endpoint is readable in View source, and worse, it can be called directly. In this challenge the source shows the delete action points at /admin/users/delete, with a comment admitting the button is only "disabled client-side until role=owner." An attacker does not need to enable the button; they can send a request straight to /admin/users/delete themselves. If the server trusts that the UI prevented the action, the deletion goes through.
This is a classic broken-access-control pattern: the frontend is decorative. Hidden buttons, disabled inputs, and "you lack permission" messages are hints to the user, not enforcement. The only thing that actually stops the action is the server re-checking, on the request itself, that the caller is allowed to perform it.
Common mistakes
- Trusting the disabled state. Assuming a greyed-out button means the action cannot happen, when only the click was blocked.
- Looking only at the button. Reading the button label and missing the form action and comment that name the real endpoint.
- Submitting the wrong route. Typing the reset or list link instead of the delete action URL.
- Assuming hidden fields are inert. Overlooking the hidden field and comment that carry
/admin/users/delete.
How to defend against it
Never treat a disabled or hidden control as an access check. Enforce authorization on the server for every state-changing request, regardless of what the UI showed.
- Re-verify the caller's role and permission on the server inside the delete endpoint itself, on every request.
- Do not rely on disabling, hiding, or greying out controls to prevent an action - those are display states only.
- Make state-changing routes require a proper request method and reject unauthorized callers, even if they know the URL.
- Avoid leaking the action target and access logic in comments and hidden fields shipped to the client.