- Status Closed
-
Assigned To
xlefloch - Private
Opened by Bajinder - 23.09.2026
Last edited by xlefloch - 23.09.2026
FS#494 - Any Customer Reads Every Account Transfer Record
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
```
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
Hello,
Thank you for your report. The bug has been fixed, do you confirm?
Regards,
Hello,
Yes, confirmed fixed.
I retested on 2026-09-23 between 11:07Z and 11:12Z UTC, and the issue no longer reproduces. Below is exactly what I checked, including a positive control, so the confirmation is based on more than the endpoint simply returning 404.
1. THE ORIGINAL IDS ARE CLOSED
The four transfer IDs quoted in my report, none of which I am party to, now return 404 on all three views from a customer login with no relationship to any of them:
```
/transfer/2000/
/transfer/4500/
/transfer/5030/
/transfer/5059/
```
I reproduced the result from a second, unrelated customer login:
```
/transfer/2000/confirm/
```
POST also returns 404, as does GET. This matters because the old view responded to both methods.
I also performed a wider sweep of 13 IDs across the range covered by the original report:
```
800
1400
2000
3000
4500
4900
5000
5010
5020
5030
5040
5050
5060
```
Result:
```
0 HTTP 200 responses
```
2. THE CONTROLS STILL BEHAVE CORRECTLY
A nonexistent ID:
```
/transfer/99999999/
```
returns 404, the same as a real foreign transfer.
Therefore, the previous existence oracle is no longer present.
Anonymous access still correctly returns:
```
302 to /login/
```
3. POSITIVE CONTROL
This is the part that makes the retest a real confirmation.
A 404 everywhere could otherwise mean that the route had simply been removed. To rule that out, I created one genuine transfer between two accounts I own and verified that the feature still works.
Transfer:
```
5066
Sender: zzr1sticky0921
Recipient: vk7research@alwaysdata.net ```
Results:
```
Sender:
Recipient:
Recipient:
Both parties:
```
This confirms that the `/confirm/` view has been scoped rather than removed, while the transfer functionality itself remains intact.
4. CORRECTION TO MY EARLIER RECOMMENDATION
I previously suggested that the authorization predicate should be:
```
requesting user is the sender OR the named recipient
```
What has been implemented is sender-only.
On reflection, I consider the sender-only behavior consistent with the intended separation of the two actions rather than a gap:
```
Sender → /confirm/
Recipient → /accept/
```
The recipient can still see the pending transfer in `/transfer/` and can accept it through `/accept/`.
Therefore, the recipient does not lose the relevant functionality, while `/confirm/` exposes less information.
My earlier recommendation was therefore broader than necessary.
CLEANUP
I created exactly one object for this retest:
```
Transfer 5066
```
It has been cancelled.
It is no longer present in either party's `/transfer/` listing, and account `zzr1sticky0921` remains with its original owner.
Nothing else was created, modified, or deleted.
No third-party data was read at any point.
Kind regards,
Bajinder
Thanks, you can now claim your bounty by opening a support ticket.