Skip to content

API Exploitation

Overview

With the full endpoint map established and Swagger UI accessible after switching the backend to development mode, this section focuses on exploiting authorization weaknesses in the API. Two vulnerabilities are demonstrated: an IDOR on the todo endpoint that allows cross-user data access, and a privilege escalation on the user update endpoint that allows any authenticated user to promote themselves to admin.

Both are authorization failures, not authentication failures. The backend correctly identifies who is making the request — the token is validated, the user is known. What it fails to do is check whether that user is allowed to perform the requested operation on the requested resource. The distinction matters because it means the attack surface exists independently of any bypass methodology. An attacker does not need to reverse engineer the APK or intercept traffic to exploit these vulnerabilities — a legitimate account and a proxy is sufficient.


Tools

Tool Purpose
Postman API request management, collection organization, and persistent history
Burp Suite Traffic interception and request analysis
curl Quick endpoint testing

1. Endpoint Map

From the Swagger UI exposed after switching the backend to development mode:

Method Endpoint Description Auth Required
POST /auth/register Register new user No
POST /auth/login Login No
GET /auth/me Get current user Yes
GET /users Get all users Yes
GET /users/{id} Get user by ID Yes
PUT /users/{id} Update user Yes
DELETE /users/{id} Delete user Yes
GET /todos Get todos Yes
POST /todos Create todo Yes
GET /todos/{id} Get todo by ID Yes
PUT /todos/{id} Update todo Yes
DELETE /todos/{id} Delete todo Yes

Screenshot 1

/system/mode and /health do not appear in the spec — confirming these are undocumented endpoints outside the standard API surface, discovered only through fuzzing in the previous section.

Attempting to test directly via the Swagger UI returns a "Failed to fetch" error. The spec hardcodes localhost:3000 as the server URL rather than the public Railway domain, so all requests fail at the network level. All testing will be done through Postman with the correct base URL configured.

Screenshot 2


2. Postman Setup

For testing across an entire endpoint surface, Postman is more practical than raw curl or Burp Repeater — persistent request history, collection organization, and variable management that survives across sessions. When a valid OpenAPI spec is available, requests can be imported directly from Swagger-generated curl commands rather than built manually.

Log in to Postman and create a new workspace named Todo Api:

Screenshot 3

Screenshot 4

Create a new collection named API:

Screenshot 5

Import requests by copying the curl commands from the Swagger UI:

curl -X 'POST' \
  'http://localhost:3000/api/v1/auth/register' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "username": "test",
  "password": "test"
}'

Screenshot 6

Screenshot 7

Replace the hardcoded localhost:3000 with a collection variable so all requests share the same configurable base URL:

http://localhost:3000  →  {{base_url}}/api/v1/auth/register

Set the base_url variable under the collection's Variables tab:

Variable Value
base_url https://api-production-27f1.up.railway.app

Screenshot 8

Screenshot 9

Send the register request to verify the setup is working:

{
    "success": false,
    "message": "Username already exists",
    "error_code": "USER_EXISTS"
}

Screenshot 10

Import all remaining endpoints and organize by feature. Send the login request to obtain a JWT:

Screenshot 11

Screenshot 12

Configure the token as a Bearer token under the collection's Authorization tab — setting it at the collection level applies it to every request automatically:

Screenshot 13

Verify the token is active and the current identity is correct:

{
    "success": true,
    "data": {
        "id": 24,
        "username": "test",
        "role": "user"
    }
}

The collection is configured and ready.


3. IDOR — GET /todos/{id}

Setup

Cross-user data access requires two independent test accounts — userA and userB, each operating with their own token:

Screenshot 15

Screenshot 17

Authenticated as userA, create a todo item. The response returns the assigned resource ID and confirms ownership via the is_owner field:

{
    "success": true,
    "data": {
        "id": 112,
        "title": "writing",
        "description": "poc",
        "completed": true,
        "is_owner": true
    }
}

Screenshot 16

Exploit

Switch to userB's token and request the same resource using the known ID:

{
    "success": true,
    "data": {
        "id": 112,
        "title": "writing",
        "description": "poc",
        "completed": true,
        "is_owner": false
    }
}

Screenshot 18

The request succeeds. userB has full read access to a resource they don't own.

The is_owner: false field is the most telling detail in this response. The backend computed ownership correctly — it knows userB does not own resource 112 — but that information never feeds into an access control decision. Ownership is calculated as a display convenience (so the UI can conditionally show edit buttons), not as an authorization gate. The check that should read "if not owner, return 403" is simply not there.

This is a common pattern in authorization bugs: the data needed to make the right decision exists in the backend, the developer even surfaced it in the response, but the enforcement step was never wired up. It is not a missing feature — it is an incomplete implementation of a feature that partially exists.

The practical consequence: resource IDs appear to be sequential integers starting from 1. Any authenticated user can enumerate the entire todo dataset across all users by iterating GET /todos/{id} from 1 upward. No special tooling required — a simple loop in curl or a Burp Intruder run with a numeric sequence covers the full dataset.

Scope of Impact

Testing write operations from userB against userA's resource:

{
    "success": false,
    "message": "Forbidden",
    "error_code": "FORBIDDEN"
}

Screenshot 19

Screenshot 20

Update and delete are correctly restricted. The IDOR is read-only — any authenticated user can enumerate and read any todo by ID, but cannot modify or delete resources they do not own.

The authorization model applied write-level ownership checks but omitted the equivalent check on reads. This is the kind of inconsistency that appears when authorization is implemented per-operation rather than defined as a policy applied uniformly across all operations on a resource.


4. Privilege Escalation — PUT /users/{id}

Setup

Confirm the identity and current role of both test accounts:

{ "id": 76, "username": "userA", "role": "user" }
{ "id": 77, "username": "userB", "role": "user" }

Authorization Boundary Check

Before targeting the update endpoint directly, map what a regular user can and cannot do across the user management surface. This is worth doing systematically — knowing exactly where the boundary sits makes it easier to identify which endpoint is the outlier.

userB attempting to access userA's profile by ID returns Forbidden:

{ "success": false, "message": "Forbidden", "error_code": "FORBIDDEN" }

Screenshot 23

Listing all users and attempting to delete a different user also return Forbidden:

Screenshot 24

Attempting to delete one's own account is also blocked:

Screenshot 25

Read, delete, and list are all restricted for regular users. The update endpoint has not been tested yet — and it is the one that matters.

Exploit

Using userB's token, send PUT /users/77 with a body attempting to change the username:

{
    "success": false,
    "message": "Validation error",
    "error_code": "VALIDATION_ERROR",
    "errors": {
        "username": "\"username\" is not allowed"
    }
}

Screenshot 26

Username is not updatable. The validation error is informative but expected — username being immutable is a common design choice. The interesting question is what fields are updatable. Rather than guessing, probe the endpoint with a field that is likely to exist but unlikely to be valid — an unrecognized role value:

{
    "success": false,
    "message": "Validation error",
    "error_code": "VALIDATION_ERROR",
    "errors": {
        "role": "\"role\" must be one of [user, admin]"
    }
}

Screenshot 27

The validation error does more than reject the input — it enumerates the valid options. This is a meaningful recon finding in its own right: the backend is confirming that role is a writable field on this endpoint and that admin is a valid value. A well-implemented API would return a generic validation error without revealing which values are accepted. This one hands the attacker the exact value they need.

Sending the request with "role": "admin":

{
    "success": true,
    "data": {
        "id": 77,
        "username": "userB",
        "role": "admin"
    }
}

Screenshot 28

A single request. No JWT forgery, no token manipulation, no secret required. The endpoint accepts a role field and updates it without checking whether the requesting user has any authority to change their own role.

Verification

/auth/me confirms the change is persisted at the identity level:

Screenshot 29

Logging in to the mobile app as userB confirms the escalation is reflected in the UI — admin-only features including the Users tab are now accessible:

Screenshot 30

Root Cause

The /users endpoints are intended to be restricted to admin users, however the PUT /users/{id} endpoint fails to enforce proper authorization checks. While other user management operations correctly return 403 Forbidden, the update endpoint allows a regular user to modify their own role field and escalate privileges to admin.


Findings Summary

Finding Endpoint Vulnerability Impact OWASP
Cross-user todo read GET /todos/{id} IDOR — ownership computed but not enforced on reads Any authenticated user can read any todo by ID; sequential IDs enable full dataset enumeration API1: BOLA
Role self-assignment PUT /users/{id} Missing field-level authorization on role update Any user can escalate to admin via single request; no secret or token forgery required API5: BFLA
Verbose validation error PUT /users/{id} Error response leaks accepted field values Reveals role is writable and admin is a valid value — eliminates the need to guess API8: Security Misconfiguration