Your frontend is not a security boundary

Your frontend is not a security boundary

We spend quite a lot of time deciding what a user is allowed to see. Hide this button, disable that field, add a route guard. It looks convincing during a demo. Log in as an employee and the refund button disappears. Log in as a manager and it comes back. Security implemented, ticket closed.

My problem with this is not the button. It is that we can mistake finishing our assigned frontend work for finishing the security requirement. I do not think a senior frontend developer gets to stop at “the backend should handle that”. The browser is running on the user's computer. If sending the request directly succeeds, the feature we helped deliver is insecure, however correctly our component hides it.

I wrote about the broader approach in Security by design in Angular. Here I want to look at something smaller, because this is where a perfectly reasonable frontend can give us a false sense of confidence.

The user does not have to follow your interface

Imagine a business portal where finance employees can refund orders. We build a nice screen, check a role and hide the action from everyone else. I have no problem with that part. Showing people buttons they cannot use is frustrating. The mistake is assuming that the screen is the only way to perform the operation.

An API request contains values. An order ID, an amount, perhaps an organisation ID. Those values can be changed. A user can copy a request and send it again, or call the endpoint without visiting the page at all. None of this requires an especially sophisticated attacker. It requires knowing that a browser has a network tab.

A route guard helps people navigate through the application. It does not turn the API into a private function of your component. Angular's own documentation explicitly warns about relying on client-side guards for access control. Yet it is surprisingly easy to spend an entire discussion on guards without discussing the operation they are supposed to protect.

Finance can refund. Fine. Whose order?

Checking a role on the backend is a start. It is not necessarily the whole rule. In our example, an employee might be allowed to refund orders belonging to their organisation, below a certain amount, provided the order has not already been refunded. Suddenly, isFinance is doing rather a lot of work in our heads that it is not doing in the code.

This is where I would rather talk to the person responsible for the process than design another generic permissions service. Can someone approve their own refund? Does an employee who changes departments keep access to old orders? Can they read an order without being allowed to refund it? Those answers determine the implementation. A beautifully abstract role model cannot supply them.

The IDs in the request are not evidence

If the frontend sends an organisation ID, the backend knows which organisation the caller is asking about. It does not yet know whether the caller belongs to it. The API has to establish the identity and check access to the actual resource. Otherwise, changing one ID can turn a perfectly valid request into access to somebody else's data.

There is also a difference between a permission being true when the page loaded and being true when the action runs. An order may have changed. Access may have been revoked. Token claims can remain valid until the token expires, even after a role changes elsewhere. For sensitive operations, we need to decide how current that decision must be instead of assuming the open tab still reflects reality.

Two legitimate requests can create trouble too. If both check the remaining refundable amount before either updates it, a permission check alone will not prevent a double refund. The backend needs to enforce the business rule under concurrency. The fact that we disabled the button after clicking it will be of little comfort to the person reconciling the payments.

And then somebody adds an export

The main screen gets attention. We discuss its permissions, test its buttons and review the endpoint. Later, someone adds a CSV export. It uses a slightly different query because it needs all the rows. Does it still apply the same organisation restrictions?

The same question applies to search results, document downloads and bulk actions. A bulk request should not become a shortcut around checks on individual records. A search endpoint should not reveal another customer's information just because opening the corresponding detail page is forbidden. Protecting the most visible route is not the same as protecting the data.

That is a backend problem?

Enforcing the rule belongs on the backend. Establishing that the rule exists is a team responsibility. Those two statements can both be true. I do not expect an Angular developer to rewrite the API's authorisation layer, but I do expect them to ask what happens when the request bypasses the interface.

In a review, I would rather see that question answered than another discussion about where the permission directive belongs in the folder structure. We spend plenty of time crossing team boundaries when we want a shared component library. Apparently, the boundary becomes much more important when an awkward security question needs an owner.

My standard for calling this done is simple: somebody can show that an unauthorised request cannot perform the operation. If our team cannot demonstrate that, I would call the security work unfinished. A frontend ticket being complete does not change the state of the product.

Test the request you did not build a button for

A test that confirms the refund button is hidden proves that we hid a button. Useful, but limited. Send the API request as an employee without permission. Then try an employee with the right role but an order from another organisation. Use real, separate test records so the distinction actually exists.

I also want to know what happened after the response. A forbidden status is not reassuring if the refund was already created before the code returned it. Check that the balance, transaction and any queued side effects remain unchanged. This takes more thought than asserting a status code, which is precisely why it tells us more.

The frontend still has work to do when access is denied. Explain what happened, refresh stale information where appropriate and avoid throwing away unrelated form input. Users should not need to understand our permission model to recover from it.

I judge the quality of this work by whether someone can perform an operation they should not be allowed to perform. That sounds obvious, yet our reviews and tests can spend far more time on the component than on that question. A beautifully structured Angular application is perfectly capable of being the frontend of an insecure product.

We cannot enforce trust in the browser, but we can stop using the division between frontend and backend as an excuse to avoid checking it. I would happily accept a less impressive permission directive and spend that time finding out who can actually move the money. That is the part the customer is relying on us to get right.

Comments

There are no comments yet, leave yours below.

Leave a comment

Do you have an addition, question or experience related to this article? Share it below.

Comments are briefly reviewed before they appear.

Read more about:

Security by design in Angular

Security by design in Angular

A route guard and a green pipeline do not stop one customer reading another customer's invoice. Using Angular and a .NET API, I turn a security requirement into server-side ownership checks and tests. Security by design starts with boundaries and evidence, before choosing a scanner.

Continue reading

Starting fresh with Angular

A new Angular project is an opportunity to make fewer premature decisions. I work through a practical starting point: one useful vertical slice, clear feature boundaries, deliberate state management and tests that protect real behaviour. The goal is a codebase that can grow with the product, without building an imaginary enterprise platform on day one.

Continue reading

The bullshit of slow SAST scans

The bullshit of slow SAST scans

A security scan that keeps developers waiting can undermine the workflow it is supposed to protect. I look at feedback time, useful pipeline gates and the cost of noisy results. Faster checks should help teams act on real findings, without quietly removing the security coverage they need.

Continue reading