Skip to content

LearnWeb and API

Prove a web finding's business impact

Brief

A web finding's impact is proven with the smallest act that shows it: one record, one cross-tenant read between tenants we created, one transaction worth nothing. The impact paragraph then says what was shown and what was assumed, in that order, so nobody has to argue about it later.

Stage
Impact
Reviewed
On this page
  1. Check first
  2. How do I prove data exposure with one record?
  3. How do I count affected records without reading them?
  4. How do I demonstrate cross-tenant access safely?
  5. How do I prove a money movement flaw without moving money?
  6. How do I write an impact paragraph that is true and not inflated?
  7. The defender sees
  8. Done when
  9. Not covered
  10. Tools
  11. Related research
  12. Reviewed
Situation
A confirmed flaw from the escalation stage, such as an object ID that another account can read, and test accounts in at least two tenants we created on the client's test or production instance. We test only the applications, tenants and accounts named in the scope letter, and any step that moves money needs its own written agreement first.
Goal
An impact paragraph backed by one record, one cross-tenant request and, where money is involved, one agreed test transaction.
Outputs
Representative examples on placeholder hosts, trimmed to the lines that matter. Not captured from a public fixture yet.

Check first

  • The flaw is confirmed and written up as a finding with its steps, from the IDOR and access control checks or another escalation step.
  • Two tenants, organizations or workspaces that we created for the test, each with its own test accounts. A tenant that belongs to a real customer is never one of the two.
  • The scope letter or a signed addendum says whether any step may create a payment, transfer, refund or order, the largest amount allowed, and who reverses it.
  • A unique string we can plant in our own test objects, so every record we read back is visibly ours.
  • An agreed point at which we stop and call the client, usually the first sight of data that is not ours.

Steps

How do I prove data exposure with one record?

GET /api/invoices/10432 HTTP/1.1
Host: app.example.com
Authorization: Bearer <token for tester-b, redacted>

Expected output

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":10432,"org":"<tenant A, redacted>","total":"<redacted>","memo":"atarus-canary-7f3e"}   # <- our own planted record, read from the other tenant's account

One record proves the flaw. The record is one we planted, so the response carries our canary string and no real customer data. If the only way to show the flaw is a real record, we take one, redact it in the report, and stop.

Negative result. A 403 or 404 for the other tenant's object, with the content checked as well as the status, means the check holds for this route. Single page apps answer 200 with their own shell, so read the body.

Fix. Load every object through a query scoped to the caller's tenant, taken from the session and never from the request, and return 404 when no row matches.

How do I count affected records without reading them?

curl -s -D - -o /dev/null \
  -H "Authorization: Bearer $TOKEN_B" \
  "https://app.example.com/api/invoices?page=1&per_page=1" | grep -i '^x-total-count'

Expected output

x-total-count: 284117   # <- the scale, from one header, with one record in the body

The scale of a flaw is often what turns a medium into a high, and the API usually states it for us: a total count header, a total field in the first page, or the highest ID in a list we were allowed to see. We request a page size of one so the body holds at most one record, and we never page through the rest.

Negative result. No count header and no total field means the scale stays an assumption. Write it as one in the impact paragraph rather than enumerating to find out.

How do I demonstrate cross-tenant access safely?

PATCH /api/orgs/org_test_a/settings HTTP/1.1
Host: app.example.com
Authorization: Bearer <token for tester-b in org_test_b, redacted>
Content-Type: application/json

{"display_name":"atarus-canary-7f3e"}

Expected output

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":"org_test_a","display_name":"atarus-canary-7f3e"}   # <- tenant B changed tenant A, and both are ours

A write across tenants is proven only between two tenants we created. The change is a harmless field set to our canary, and we set it back in the next request. We do not try the same request against any other tenant ID to show that it works there too: the code path is the same, and the report says so as an assumption.

Negative result. A 403 or 404, and tenant A's settings unchanged when read back from tenant A's own account, is the clean result.

Fix. Authorize writes against the tenant in the session, and add a test that sends another tenant's ID and expects the request to fail.

How do I prove a money movement flaw without moving money?

POST /api/transfers HTTP/1.1
Host: app.example.com
Authorization: Bearer <token for tester-a, redacted>
Content-Type: application/json

{"from":"acct_test_a1","to":"acct_test_b1","amount":"0.00","currency":"USD","approval_id":null}

Expected output

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{"id":"tr_test_5521","status":"released","amount":"0.00","approved_by":null}   # <- released with no approval

A zero-value transaction proves the missing approval without moving money, but it still writes a record to the ledger, so it needs the same written agreement as any other test transaction. Where the system rejects zero, the addendum names the smallest amount, the two test accounts and the person who reverses it, and the report records the reversal reference beside the transaction ID. Nothing moves between accounts we did not create.

Negative result. A 400 or 403 citing the missing approval, and no ledger entry for the attempt, is the clean result.

Fix. The approval check runs on the server for every amount, zero included, and the ledger rejects a release that has no approval record.

How do I write an impact paragraph that is true and not inflated?

Inflated: An attacker could steal all customer financial data, causing massive losses.
True:     Any user in any tenant can read any invoice by ID. We read one invoice that we
          planted in a second test tenant. The API reported 284,117 invoices in total;
          we assume all are reachable the same way but read no others.

The true version says who can do what, shows the scale, and marks the one assumption. A reader can disagree with the assumption without disputing the finding. The VPN cookie case shows the opposite failure, a medium rating that left out what the flaw reached. The Snowflake token thefts show why cross-tenant reach belongs in the first sentence: one integration's access became many customers' loss.

The defender sees

The single read and the count request show in access logs as a token from one tenant requesting another tenant's object, which an application that logs the tenant on both sides can alert on. The settings change shows in tenant A's audit log under a user from tenant B. The test transaction shows in the ledger and in payment monitoring at the time the addendum named. If none of this is visible, the finding needs a second recommendation: log the caller's tenant and the object's tenant on every request.

Done when

The finding holds one redacted record, a count of affected objects taken without reading them, the cross-tenant request between two test tenants, any test transaction with its reversal reference, and an impact paragraph that separates shown from assumed.

Not covered

Finding the flaw, which the authorization page covers, and load or denial of service impact; the outputs shown are representative examples on placeholder hosts, not captured runs.