|
Task Description
Vulnerability Information:
Name of Vulnerability: `/transfer/<id>/confirm/` performs no object-level authorization, allowing any authenticated customer to read account transfer records created for other customers by walking the global sequential ID space.
The endpoint exposes the transferred account's name, product tier, and email address of the person receiving the transfer, while the sibling `/transfer/<id>/accept/` and `/transfer/<id>/cancel/` views correctly scope the same object.
Vulnerability Category: Access Control Issues, Exposure of Sensitive Members Information, per the qualifying list on your bug bounty page.
This is the same class as FS#423 (IDOR to registrant dossiers) and FS#203 (account data exposed through insufficient authorization), both of which you fixed.
CVSS 3.1: 6.5 Medium AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
I score this based on the demonstrated primitive only: read access, one record per request, and no write capability.
The issue exposes third-party personal data, including email addresses, and therefore falls within the "accessing customers' data" impact category.
This report is unrelated to my two reports concerning `/reseller/domain/`. It is a different application and a different authorization defect.
Description:
Account ownership transfers, referred to as "cession", are represented by objects with a single global sequential ID rather than per-customer numbering.
The three views that operate on a transfer are:
``` /transfer/<id>/accept/
Correctly scoped
404 for a transfer the authenticated customer is not party to
/transfer/<id>/cancel/
Correctly scoped
404 for a transfer the authenticated customer is not party to
/transfer/<id>/confirm/
NOT scoped
200 for every existing transfer ID
```
The missing authorization check on `/confirm/` is the entire defect.
The existing codebase provides the control that demonstrates this clearly. Using the same user, in the same session, against the same transfer ID, `/accept/` and `/cancel/` refuse access while `/confirm/` serves the object.
The confirmation page renders:
``` "The transfer of the account <ACCOUNT NAME> is currently being accepted by <EMAIL ADDRESS>." ```
The product tier is also present in the page heading, for example:
``` "Transfer of a Private cloud account" ```
Because the transfer ID space is a single global counter, an attacker does not need to guess individual identifiers. They can enumerate them sequentially.
I swept a contiguous window of 68 IDs and 64 returned HTTP 200. Additional spot checks at IDs 800, 1400, 2000, 3000, 4500, and 4900 also returned HTTP 200.
This demonstrates that the vulnerable endpoint covers historical transfer records rather than only a recent slice.
Transfer ID 2000 predates every account I own, which rules out any permission relationship between my own accounts as an explanation.
WHAT THIS REACHES THAT A CUSTOMER CANNOT ALREADY REACH
A tenant's own SSH shell cannot enumerate another customer's ownership-transfer history.
This is control-plane state. The record couples a business or project account with the email address of the individual receiving ownership of it, identifying which party in a private commercial handover ultimately received the asset.
Nothing in a customer's own environment exposes this information.
Vulnerable Instances:
``` GET https://admin.alwaysdata.com/transfer/<id>/confirm/
POST https://admin.alwaysdata.com/transfer/<id>/confirm/ ```
The POST is also read-only in the observed behavior and does not mutate the transfer.
`<id>` can be any existing transfer ID.
The endpoint is reachable by any authenticated customer.
No permission grant, reseller role, or paid plan is required.
Steps to Reproduce:
Total cost: EUR 0.
I used a customer login with no relationship to the affected transfers.
All times are UTC, 2026-09-23.
1. Log in as an ordinary customer.
I used the login listed in the private annex. It owns exactly one account:
``` zzr1sticky0921 ```
Its "Granted permissions" list is empty.
2. Request:
GET /transfer/2000/confirm/
The endpoint returns HTTP 200 with a 149,638-byte response.
The body identifies a third-party account and the email address of the person accepting its transfer.
I read three transfer records in full. Their identifying values are listed in the private annex rather than in this report because tasks on this tracker are published and the values belong to your customers.
3. THE CONTROL THAT PROVES THE CHECK IS MISSING
Using the same session, the same IDs, and the same minute, two of the three sibling views refuse the exact object that `/confirm/` serves.
See the Proof of Concept for the response matrix.
4. FURTHER CONTROLS
a) A 200 response genuinely means that the record exists, making the endpoint an existence oracle:
``` /transfer/99999999/confirm/
404
/transfer/-1/confirm/
404
/transfer/0/confirm/
404
```
b) Authentication is required, so this is not an unauthenticated leak:
``` Anonymous GET /transfer/2000/confirm/ ```
returns:
``` 302 to /login/?next=/transfer/2000/confirm/ ```
c) I reproduced the issue from a second, unrelated customer login, confirming that it is not an artifact of a single account.
d) No other transfer sub-route I tested was open.
I swept 24 sub-routes including:
``` accept cancel decline refuse reject delete detail edit update resend renew validate complete finish abort status retry approve deny transfer the bare ID route ```
Every tested route returned 404 at exactly 179 bytes, identical to the response for a nonexistent ID.
Only `/confirm/` was readable cross-customer.
I include this control to make the blast radius precise: this is a disclosure issue, not a demonstrated takeover path.
5. RE-VERIFIED
I re-verified the issue at 05:45:11Z UTC from a newly established session, logged in from scratch specifically for this check.
No part of the evidence therefore depends on a session used for another test.
Proof of Concept:
All output below is verbatim. Customer-identifying values are replaced with placeholders and provided in the private annex.
Sibling view matrix, same session, same IDs, same minute:
``` ID /confirm/ /accept/ /cancel/ 2000 200 (149,638 B) 404 (179 B) 404 (179 B) 4500 200 (149,636 B) 404 (179 B) 404 (179 B) 5030 200 (149,632 B) 404 (179 B) 404 (179 B) ```
Nonexistent and negative ID controls on the SAME view:
``` /transfer/99999999/confirm/ 404 /transfer/-1/confirm/ 404 /transfer/0/confirm/ 404 ```
Unauthenticated control:
``` anonymous GET /transfer/2000/confirm/
302 to /login/?next=/transfer/2000/confirm/
```
Shape of the disclosed response body, with the two sensitive values replaced:
``` Home > Transfers > Transfer of a Private cloud account
"The transfer of the account <ACCOUNT-X> is currently being accepted by <EMAIL-X>. This only concerns the ownership of the account. If you also want the contents to be migrated to another server, please …" ```
Extent of the ID space:
``` IDs 4995 to 5062
64 of 68 returned HTTP 200
```
Spot checks returning HTTP 200:
``` 800 1400 2000 3000 4500 4900 ```
Transfer ID 2000 predates every account I own.
Impact:
Any person with a free Alwaysdata account can enumerate the global history of account ownership transfers on the platform.
For each transfer, the endpoint exposes:
``` Account name Product tier Email address of the receiving party ```
Concretely:
Commercial and personal relationship disclosure.
The endpoint links a named hosting account, and therefore potentially a business or project, to the personal email address of the individual receiving control of it.
For customers who are individuals or sole traders, this directly exposes personal information to any authenticated customer.
Commercially sensitive transfer events.
An account transfer can represent a business handover, a freelancer transferring client infrastructure, or another private ownership change.
The existence, timing, account identity, and receiving party of the transfer are disclosed.
Phishing and social-engineering targeting.
An attacker can obtain the exact account name and product tier associated with a genuine transfer and address the recipient using the email address exposed by the endpoint.
This provides a substantially more specific pretext than generic phishing.
Platform-wide transfer enumeration.
The existence oracle also provides information about historical transfer activity across the platform.
Recommendation:
1. Apply to `/transfer/<id>/confirm/` the same queryset scoping already used by `/transfer/<id>/accept/` and `/transfer/<id>/cancel/`.
Those two views provide the correct reference implementation.
The missing object-level authorization check appears to be the direct defect.
2. Confirm that the scoping predicate covers both parties to the transfer.
Specifically, verify that access is restricted to the transfer sender or the named recipient, and that this predicate is applied consistently to both GET and POST handling.
`/confirm/` currently responds to GET requests without the required object-level scope.
3. As defense in depth, apply the same principle used for the reseller views.
The transfer views should not default to a global queryset when the customer-specific filter is omitted.
A shared base view or authorization mechanism that requires object scoping would prevent another sibling view from accidentally exposing the same global dataset.
4. Optional and secondary: consider whether the confirmation page needs to display the recipient's full email address before acceptance.
A masked form may reduce unnecessary personal-data exposure even after the authorization issue is fixed.
DISCLOSURE AND CLEANUP:
I read three transfer records in full, plus one earlier record during discovery.
For the remaining enumeration, I recorded only HTTP status codes and response sizes.
Nothing was created, modified, accepted, cancelled, or deleted.
`/confirm/` is read-only based on the observed behavior.
I have not retained the personal data beyond the private annex submitted alongside this report, and I will destroy that annex upon your confirmation.
Customer-identifying values have deliberately been omitted from this published task because they belong to your customers.
Test window:
``` 2026-09-23 04:40Z to 05:45Z UTC ```
|