TA AzeezCode

Case studies

IDOR: missing server-side authorisation in an invoice workflow

An authenticated user could modify an invoice identifier and attempt to access another customer's resource. The remediation enforced ownership at the database query layer and was verified with positive and negative authorisation tests.

high severity IDOR Broken Access Control Authorisation PHP Retesting

Security issue

An authenticated invoice workflow accepted a resource identifier from the request.

The application interface only presented invoices belonging to the logged-in customer, but this did not guarantee that the server enforced the same restriction when an individual invoice was requested directly.

This created an Insecure Direct Object Reference (IDOR) / broken access-control risk.

Why this matters

Authentication answers:

Who is making this request?

Authorisation answers:

Is this user allowed to access this specific resource?

A user being successfully logged in does not mean they should be able to access every invoice in the system.

Hiding another customer's invoice identifier from the interface is also not an effective security control because HTTP requests can be modified independently of the application's frontend.

Testing approach

Testing was performed in an authorised environment using separate user contexts.

The expected workflow was first established:

  1. Authenticate as a normal customer.
  2. Request an invoice belonging to that customer.
  3. Confirm that the legitimate request succeeds.
  4. Modify the invoice identifier in the request.
  5. Attempt to request a resource belonging to a different customer.
  6. Compare the application's response and determine whether ownership is enforced server-side.

The important test was not whether the identifier was visible in the interface.

The important question was whether the server independently verified that the requested invoice belonged to the authenticated customer.

Vulnerable design

A vulnerable implementation can occur when the application retrieves an invoice using only the user-controlled identifier:

$invoiceId = (int) $_GET['id'];

$invoice = $invoiceRepository->findById($invoiceId);

The application knows which invoice was requested, but the lookup does not establish whether the authenticated customer owns that invoice.

If subsequent code returns the record without another authorisation check, changing the identifier may cross the account boundary.

Root cause

The root cause is missing object-level authorisation.

The application relies on possession of a valid session and knowledge of a resource identifier rather than enforcing the relationship between:

  • the authenticated customer
  • the requested invoice

This is a server-side security requirement and cannot safely be delegated to the user interface.

Impact

Successful exploitation could allow an authenticated user to access information belonging to another customer.

Depending on the affected endpoint and information stored in the invoice, exposed data could include:

  • customer information
  • invoice details
  • transaction information
  • business records
  • financial amounts

If update or delete endpoints contained the same authorisation weakness, the impact could extend beyond confidentiality to unauthorised modification of another customer's records.

Remediation

The secure approach is to include ownership in the server-side lookup.

$currentCustomerId = (int) $_SESSION['customer_id'];
$invoiceId = (int) $_GET['id'];

$invoice = $invoiceRepository->findByIdAndCustomerId(
    $invoiceId,
    $currentCustomerId
);

if (!$invoice) {
    http_response_code(404);
    exit('Invoice not found.');
}

The repository query should enforce both conditions:

SELECT *
FROM invoices
WHERE id = :invoice_id
  AND customer_id = :customer_id
LIMIT 1

This changes the security model.

Instead of:

Find invoice 123 and then assume the current user may access it.

the application asks:

Find invoice 123 only if it belongs to the currently authenticated customer.

Why the database-level scope matters

Enforcing ownership as part of the query reduces the chance that a developer retrieves an unrestricted record and forgets to perform a separate authorisation check later.

The application should still have a consistent authorisation strategy across controllers, services and repositories, but sensitive records should never be returned simply because the caller supplied a valid identifier.

Why changing the identifier format is not the fix

Replacing sequential IDs with UUIDs, ULIDs or other difficult-to-guess identifiers can make enumeration harder.

It does not solve broken access control.

If another user's identifier becomes known through logs, URLs, browser history, referrers, application behaviour or another disclosure, the server must still reject unauthorised access.

Authorisation remains the primary control.

Retesting

After remediation, both positive and negative tests should be performed.

Positive test

Authenticated Customer A requests an invoice owned by Customer A.

Expected result:

200 OK

The authorised invoice is returned normally.

Negative test

Authenticated Customer A requests an invoice belonging to Customer B.

Expected result:

404 Not Found

No Customer B invoice information is returned.

Returning 404 can also avoid confirming whether a particular resource exists outside the authenticated customer's accessible dataset.

Additional verification

The same ownership checks should be verified across every endpoint that operates on the resource, including:

  • view
  • edit
  • update
  • delete
  • download
  • print
  • export
  • API endpoints

Protecting only the main invoice page can leave secondary routes vulnerable.

Security lesson

The central lesson from this case study is simple:

Authentication does not equal authorisation.

Access control must be enforced server-side for every sensitive resource and action.

A secure application should assume that identifiers, hidden fields, URLs and HTTP requests can all be modified by the user.

The security decision therefore belongs on the server.

Remediation principle

For multi-user applications, database access patterns such as:

findByIdAndCustomerId($invoiceId, $currentCustomerId)

are safer than unrestricted lookups when ownership is part of the application's security boundary.

The same principle applies beyond invoices to:

  • orders
  • bookings
  • documents
  • messages
  • payment records
  • uploaded files
  • account resources
  • API objects

Object-level authorisation should be designed into the application rather than added only after an IDOR vulnerability is discovered.

Authorisation

This work was carried out under written authorisation or inside an authorised training environment. All evidence has been sanitised.

Continue reading

Related case studies

high severity SSRF

SSRF in a server-side document import feature

An authorised security lab examining how a user-controlled URL can cause a web server to make unintended requests to internal or restricted destinations, followed by layered remediation and retesting.

SSRF Web Security PHP Input Validation

9 min read

high severity XXE

XXE in a legacy XML import parser

A supplier feed parser resolved external entities, exposing server-side file contents. Remediated by disabling entity loading, switching to a safe parser configuration and validating against a schema.

XXE Secure parsing Legacy code Retested

2 min read