<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/owasp-top-10-2025-a01-part-3-access-control-breaks-at-the-final-request-inwvfwlod" -->

---
title: OWASP Top 10 2025 A01, Part 3: Access Control Breaks at...
description: Part 3 of an OWASP Top 10 2025 A01 series focusing on a critical gap: even when pages and workflows are protected, the final state-changing HTTP request must...
canonical: https://daily.dev/posts/owasp-top-10-2025-a01-part-3-access-control-breaks-at-the-final-request-inwvfwlod
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: OWASP Top 10 2025 A01, Part 3: Access Control Breaks at the Final Request | daily.dev
og:description: Part 3 of an OWASP Top 10 2025 A01 series focusing on a critical gap: even when pages and workflows are protected, the final state-changing HTTP request must...
og:url: https://daily.dev/posts/owasp-top-10-2025-a01-part-3-access-control-breaks-at-the-final-request-inwvfwlod
og:image: https://api.daily.dev/og/posts/InwVFwlod.png
og:image:alt: OWASP Top 10 2025 A01, Part 3: Access Control Breaks at the Final Request
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# OWASP Top 10 2025 A01, Part 3: Access Control Breaks at the Final Request

**[Application Security Blog](https://daily.dev/sources/applicationsecurityblog)** · [@przemyslaw91](https://daily.dev/przemyslaw91) · 8 min read · 0 upvotes · 0 comments

## Summary

Part 3 of an OWASP Top 10 2025 A01 series focusing on a critical gap: even when pages and workflows are protected, the final state-changing HTTP request must have its own authorization check. Using an Express.js example, the post demonstrates how attackers can bypass UI-level protections by sending the final request directly. It covers horizontal and vertical privilege escalation, incomplete role checks, multi-step workflow trust issues, stale permissions, and provides a practical testing checklist for reviewing sensitive endpoints — including replaying requests with lower-privileged accounts, changing target IDs, and verifying server-side state after failure.

## Content

**The page may be protected and the workflow may look correct, but the final state-changing request still needs its own authorization decision.**

I have not published the next part of this series for a while because I was working on an interesting internal project.

The goal is to add a small, practical security layer to our projects, including SAST, secret detection, and project-specific rules without making the developer workflow heavy. I hope we can gradually introduce it across more of our projects in our department. 😄

In Part 1, I looked at the signals that appear before an exploit. In Part 2, I focused on common implementation mistakes.

This time, I want to look at another place where Broken Access Control becomes dangerous: the final request.

As a frontend developer, I used to think mainly about whether the correct page and controls were protected. A01 made me look beyond the visible workflow and focus on the request that performs the real action.

The important question is:

```
What happens when the final request is sent directly?
```

## The application should not trust the path taken by the user

Imagine a user-management flow:

```
GET  /settings/users
GET  /settings/users/123/edit
POST /settings/users/123/change-role
```

The first page may require an administrator role.

The edit page may also be protected.

But if the final `POST` request does not check the same permission, an attacker does not need either page.

They only need the request.

They can capture it, replay it, modify it, or build it manually.

This is where the intended workflow and the real security boundary separate.

The application expects the user to follow the workflow, but the backend only sees the request it finally receives.

## A simple Express example

![458d953b-5e9f-4e0b-a184-8783acc398d3.png](https://media.daily.dev/image/upload/s--25t17qZz--/f_auto/v1784545982/ugc/content_9ed1fce1-6bb5-4c54-8d4b-aa1e47ce2e1b?_a=BAMAMicg0)

Consider this route:

```
router.post(
  "/users/:userId/change-role",
  requireAuth,
  async (req, res) => {
    const updatedUser = await users.changeRole(
      req.params.userId,
      req.body.role
    );

    return res.json(updatedUser);
  }
);
```

The route checks that the caller is authenticated.

It does not check whether the caller is allowed to change roles.

A normal user may never see the administration page, but that does not matter if they can still send:

```
POST /users/user_456/change-role
Content-Type: application/json

{
  "role": "admin"
}
```

The problem is not the missing page restriction.

The problem is that the final action accepts a sensitive state change without a complete authorization decision.

## Which final actions would I check first?

During a real review, I would start with actions that change permissions, ownership, approval state, or tenant scope. One successful request may unlock access beyond the current screen.

I would also prioritise deletion and sensitive exports. Testing only `GET` requests gives an incomplete picture: a user may be unable to view a resource and still be able to change, approve, export, or delete it through another handler.

## Privilege escalation is not always an obvious admin switch

![89fe64b3-e671-402b-96c8-cbfdec7b6fdb.png](https://media.daily.dev/image/upload/s--K5csFKE6--/f_auto/v1784545725/ugc/content_7e33700e-f97f-4334-9a15-7aa5ce8e8f9c?_a=BAMAMicg0)

The easiest example is changing:

```
{
  "role": "user"
}
```

to:

```
{
  "role": "admin"
}
```

Real applications can be less obvious.

A request may contain:

```
{
  "teamId": "team_a",
  "ownerId": "user_123",
  "canApprove": true,
  "status": "approved"
}
```

Changing one value may not create a global administrator, but it can still move a resource into the attacker’s team, assign ownership, enable approval, or skip a required review state.

Privilege escalation can mean gaining a higher role, but also gaining one action, object, or scope that should remain unavailable.

## Role, ownership, scope, and state can all matter

![30b18326-9b7f-41eb-a728-3197811fbc9c.png](https://media.daily.dev/image/upload/s--xu_OHqMS--/f_auto/v1784545671/ugc/content_32f25e19-75b1-4ff5-8182-4dfe7d965a2d?_a=BAMAMicg0)

A route may check the user’s role and still make the wrong decision.

Consider:

```
role: manager
scope: team A
resource: request from team B
action: approve
state: waiting for finance review
```

The user is a manager.

That alone does not answer whether they can approve this request.

The application may also need to know:

- whether the resource belongs to their team,
- whether they created the request,
- whether the current state allows approval,
- whether another role must review it first,
- whether the action is allowed inside this tenant.

A broad role check can miss these conditions.

```
if (req.user.role === "manager") {
  approveRequest();
}
```

The code looks like authorization, but the decision may still be incomplete.

## Previous workflow steps are not proof

A multi-step flow may collect or validate information earlier:

```
Step 1: select the account
Step 2: choose the action
Step 3: review the change
Step 4: confirm
```

The final request should not assume that the user completed those steps correctly.

A client can skip a step, replay a request, change a value after validation, submit it from another account, or replace a hidden or disabled field.

For example, a confirmation page may display:

```
Change role from Editor to Manager
```

But the final request may be modified to send:

```
{
  "newRole": "admin"
}
```

The value shown on the previous screen is not the value the backend should automatically trust.

The final request needs to be evaluated using the current user, target resource, requested action, trusted scope, and current application state.

## How I would test the final request

For this part of A01, I would not stop after confirming that the correct user can complete the flow.

I would capture the final request and ask what else it accepts.

A practical test could look like this:

1. Complete the action with an allowed user.
2. Capture the final request in the browser, Postman, or Burp.
3. Repeat it using a lower-privileged account.
4. Change the target user or resource.
5. Change the action, role, owner, tenant, or status.
6. Send the request without visiting the earlier pages.
7. Repeat an old request after the resource state has changed.

Then check the result carefully.

```
Was access denied?
Was protected data returned?
Did the database state change?
Did part of the action happen before the error?
Did the response reveal whether the target exists?
```

A visible `403` means little if the role was already changed or the export was already generated.

The outcome matters more than the message.

## Horizontal and vertical tests should both be included

Two simple accounts can reveal different problems.

### Horizontal test

```
User A owns report 123.
User B sends the update or delete request for report 123.
```

Both users may have the same role, but only one owns the resource.

### Vertical test

```
A normal user captures an administrator action.
The normal user sends the same final request.
```

The target may belong to the same company, but the caller does not have the required function-level permission.

Testing both directions helps separate ownership, scope, role, and action-level problems.

## Do not forget stale permissions and state changes

Authorization is not always static. A user may lose a role or team membership while keeping an active session, and a resource may move to another workflow state.

A previously valid request may no longer be valid.

For example:

```
The user opened an approval page while they were a manager.
Their manager role was removed.
They submit the old approval request.
```

The application should decide using current state, not only information stored when the page was opened. The same applies after ownership or tenant changes.

## What I would look for during review

When reviewing a sensitive workflow, I would ask:

- Which request performs the real action?
- Does that request check more than authentication?
- Can it be sent without visiting the earlier pages?
- Can another user replay it?
- Can the target ID, role, owner, scope, or state be changed?
- Is authorization based on current server-side data?
- Does the same rule apply to every way of reaching the action?
- Does failure leave both the response and application state safe?

These questions are more useful than asking only whether the page is protected.

## My takeaway

The UI shows the intended path, but the final request reveals the real boundary.

A protected dashboard, edit page, confirmation screen, or role-based navigation can make the application look secure while the state-changing endpoint still accepts the wrong user, action, resource, or scope.

That is why I would test the final request directly. The earlier workflow still matters, but the backend cannot use it as proof of permission.

In Part 4, I will focus on remediation: deny-by-default, trusted identity context, action-specific permission checks, safe failure, logging, and negative authorization tests.

## Extended version

I cover the wider implementation and workflow problems in the expanded Medium article:

[OWASP Top 10 2025 A01: Where Broken Access Control Fails in Real Applications](https://medium.com/@cprzemek91/owasp-top-10-2025-a01-where-broken-access-control-fails-in-real-applications-f3c28b500f21)

## References and further reading

- [OWASP Top 10 2025 — A01 Broken Access Control](https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/)
- [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)
- [OWASP WSTG — Testing for Privilege Escalation](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/03-Testing_for_Privilege_Escalation)

---

Tags: [#security](https://daily.dev/tags/security), [#authorization](https://daily.dev/tags/authorization)

[View this post on daily.dev](https://daily.dev/posts/owasp-top-10-2025-a01-part-3-access-control-breaks-at-the-final-request-inwvfwlod)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"DiscussionForumPosting","mainEntityOfPage":"https://daily.dev/posts/owasp-top-10-2025-a01-part-3-access-control-breaks-at-the-final-request-inwvfwlod","headline":"OWASP Top 10 2025 A01, Part 3: Access Control Breaks at the Final Request","text":"Part 3 of an OWASP Top 10 2025 A01 series focusing on a critical gap: even when pages and workflows are protected, the final state-changing HTTP request must have its own authorization check. Using an Express.js example, the post demonstrates how attackers can bypass UI-level protections by sending the final request directly. It covers horizontal and vertical privilege escalation, incomplete role checks, multi-step workflow trust issues, stale permissions, and provides a practical testing checklist for reviewing sensitive endpoints — including replaying requests with lower-privileged accounts, changing target IDs, and verifying server-side state after failure.","url":"https://daily.dev/posts/owasp-top-10-2025-a01-part-3-access-control-breaks-at-the-final-request-inwvfwlod","datePublished":"2026-07-20T11:21:20.288Z","dateModified":"2026-07-20T11:21:45.029Z","author":{"@type":"Person","name":"Przemyslaw","url":"https://daily.dev/przemyslaw91","image":"https://media.daily.dev/image/upload/s--tcy6XTmy--/f_auto/v1778578633/avatars/avatar_zIjg9D2aXRpPyx4SnlnEv?_a=BAMAMiWQ0","interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"EndorseAction"},"userInteractionCount":520}},"image":"https://media.daily.dev/image/upload/s--uSJh_lep--/f_auto/v1784546482/posts/InwVFwlod?_a=BAMAMicg0","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"isPartOf":{"@type":"WebPage","url":"https://daily.dev/squads/applicationsecurityblog","name":"Application Security Blog"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Application Security Blog","item":"https://daily.dev/squads/applicationsecurityblog"},{"@type":"ListItem","position":3,"name":"OWASP Top 10 2025 A01, Part 3: Access Control Breaks at the Final Request"}]}
```

