- Status Closed
-
Assigned To
cbay - Private
Opened by S_Haque - 02.07.2026
Last edited by cbay - 02.07.2026
FS#363 - Cross-tenant file disclosure via world-readable shared `/tmp` on alwaysdata SSH/web hosts
Vulnerability Name: Cross-tenant file disclosure via world-readable shared `/tmp` on alwaysdata SSH/web hosts
Severity: High
CVSS 4.0 vector: `CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N`
CVSS 4.0 score: ~8.2 (High)
Target: `ssh-<account>.alwaysdata.net` (physical SSH host `ssh1`) and the web-application hosts (`http21`) — kernel `6.18.30-alwaysdata`
Type: CWE-668 (Exposure of Resource to Wrong Sphere) / CWE-732 (Incorrect Permission Assignment) / CWE-200
Description
On alwaysdata's shared hosting infrastructure every customer account on a given physical host shares a single, non-polyinstantiated `/tmp` directory (mode `drwxrwxrwt` / `1777`), while the platform default umask is `022` — so any file a customer writes into `/tmp` is created world-readable (`644`).
Account isolation on these hosts is enforced only by cgroups + per-account Unix UIDs, and SSH is explicitly not chrooted (per alwaysdata's own documentation). As a result, any customer — on any plan, including the free plan — can read any other customer's world-readable files in `/tmp` on the same physical host. This is a cross-tenant confidentiality boundary violation: it lets a low-privileged tenant passively harvest other tenants' source code, configuration, and temporary artifacts.
The condition is confirmed on both the SSH tier (`ssh1`) and the web-application tier (`http21`), indicating it is fleet-wide. (Note: PHP `session.save_path` is per-account, so live sessions are not exposed — which bounds this at High rather than Critical.)
Exposed Endpoints / Affected Components
| Host / component | Path | Mode | Issue |
| — | — | — | — |
| SSH host `ssh1` | `/tmp` | `1777` (shared, not polyinstantiated) | other tenants' world-readable files readable |
| Web host `http21` | `/tmp` | `1777` (shared) | same exposure on the web tier |
| Platform default | umask | `022` | new `/tmp` files created world-readable (`644`) |
Steps to Reproduce
Requires two accounts you control (`A` and `B`, different customers) that land on the same physical host. In this report `A` = `steve-william` (uid 530469), `B` = `test-domain` (uid 530478), both on `ssh1`.
1. Create two free alwaysdata accounts with different emails; enable SSH on each (Remote access → SSH).
2. SSH into account A: `ssh A@ssh-A.alwaysdata.net`.
3. As A, write a file into the shared `/tmp` (created world-readable due to default umask 022):
`echo "SECRET_OF_A" > /tmp/canary_A.txt`
4. In a second terminal, SSH into account B (a *different* customer): `ssh B@ssh-B.alwaysdata.net`.
5. As B, confirm you are a different UID on the same host: `id; hostname`.
6. As B, read account A's file: `cat /tmp/canary_A.txt` → A's content is returned. 7. Enumerate the real cross-tenant exposure (metadata only): `find /tmp -maxdepth 1 -type f ! -user "$(id -un)" -readable`.
Observed live: as account B, 65 files across 12 other live customer accounts were readable, including source archives (`*.tgz`), a config script (`inject_config.py`), an Omeka DB env file (`omeka_db_env_*`), financial PDFs, and cryptocurrency wallet backups. (No third-party file *content* was read — only names/owners/permissions were enumerated, per program rules.)
Proof of Concept (PoC)
Bash PoC Script, Python PoC Script, and other attachments are attached.
Impact
- Cross-tenant confidentiality breach affecting all customers sharing the same physical host.
- On the affected host (`ssh1`), approximately 882 customer home directories are co-located, allowing tenants to access world-readable temporary files belonging to other customers.
- Sensitive information that may be exposed includes source code, configuration files, temporary application data, and other confidential files.
- If a world-readable `.env` or configuration file contains database credentials, an attacker could use those credentials to access the victim's remotely reachable database (e.g., `mysql-<account>.alwaysdata.net:3306` or `postgresql-<account>.alwaysdata.net:5432`).
- This could lead to unauthorized access to another customer's database and the data stored within it.
- No other customers' files, credentials, or databases were accessed during testing. The impact assessment is based solely on the demonstrated file exposure and the resulting attack path.
Remediation
- Polyinstantiate `/tmp` (and `/var/tmp`) per account — e.g. `pam_namespace` with per-account instances, or a per-account-namespace private `tmpfs` — so each tenant sees an isolated `/tmp`.
- And/or set the platform default umask to `077`.
- Optionally enable `fs.protected_regular=2` and per-account `/tmp` reaping.
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
poc_F-01_canary.py
Hello,
That's not true, it's 0007.
Kind regards,
Cyril
Hello Cyril,
Could you reopen this — the closure premise ("default umask is 0007") doesn't match what the platform actually does. I re-measured on two accounts I own, in both the interactive login shell and a non-interactive bash -c 'umask':
$ umask
0022
$ bash -c 'umask'
0022
Files in /tmp therefore land world-readable (0644). Repro with two of my own accounts, A (steve-william, uid 530469, gid 486358) and B (test-domain, uid 530478, gid 486367) — different UIDs and groups, both on ssh1:
# as A
$ echo "SECRET_OF_A" > /tmp/canary.txt
$ stat -c '%U:%G %a' /tmp/canary.txt
steve-william:steve-william 644
# as B (a different customer)
$ cat /tmp/canary.txt
SECRET_OF_A
A and B are in different groups, so a 0660 file (what umask 0007 produces) wouldn't be readable across them — i.e. 0007 would fix this. The point is 0007 isn't what's applied: the shell/runtime umask is 0022. This isn't limited to my test files — as B, 69 files owned by other customers are world-readable in /tmp right now (e.g. nerudaarchives_codex 644 /tmp/omeka_db_env_2388891, storioscope_ssh 644 /tmp/storioscope-pre-security-final-20260701.tgz).
The exposure is a platform-side one: alwaysdata's default umask (0022) + a shared, non-polyinstantiated /tmp. The standard mitigation on multi-tenant shared hosts is a per-account /tmp (pam_namespace).
You can confirm in seconds on any account: umask (observe 0022), then find /tmp -maxdepth 1 -type f ! -user "$(id -un)" -readable | head.
Happy to provide a screen recording. Kind regards.
I can confirm that the umask was not set in the non-interactive case (and only in that case). I've made a change, can you confirm you no longer get a 0022 umask?
Hi,
Thanks for the quick turnaround. I've re-tested on ssh1 with two accounts I own
(steve-william, uid 530469, and test-domain, uid 530478 — a separate customer in a
different Unix group). Confirming your fix:
1. The umask is now 0007 in every shell context, including the non-interactive path
that was previously unset:
2. A /tmp file created via the default umask (no explicit chmod) now lands at
660 (rw-rw—-) instead of 644 (rw-r–r–).
3. The original cross-tenant repro no longer works: account test-domain (different
group) attempting to read a default-umask file created by steve-william now gets
"Permission denied". The group bit doesn't help because the two accounts are in
different groups.
So the non-interactive umask issue is resolved — thank you.
One residual I want to flag in good faith, in case you're treating this as a full
close of F-01 rather than just the umask sub-issue: because /tmp is still a single
shared, non-polyinstantiated directory (1777), files written with an *explicit* mode
(chmod 644, open(…, 0o644), tar/unzip preserving archived perms, install -m 644,
etc.) still land world-readable and remain cross-tenant readable. Right now, from
test-domain, 68 other-tenant files in /tmp are still readable, including some that
look sensitive (e.g. a DB env file and source archives), and at least one tenant is
actively producing fresh 644/664 files — so this isn't only legacy data. umask only
sets the default; it isn't a ceiling.
If the goal is to close the class fully, polyinstantiating /tmp per account
(pam_namespace / per-account tmpdir) removes the shared-directory exposure entirely
and doesn't depend on every process honoring the umask.
Happy to re-test again if you make further changes. I did not access or copy any of
the third-party files referenced above — enumeration was metadata-only.
Having a per-account /tmp is a different issue. Anyway, a program that uses /tmp (instead of $TMPDIR, which is private) AND uses its own permissive umask would be clearly at fault anyway.
Can you open a support ticket to claim a small bounty for that non-interactive umask issue?
sure