Security vulnerabilities

  • Status Closed
  • Assigned To
    cbay
  • Private
Attached to Project: Security vulnerabilities
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.

Closed by  cbay
02.07.2026 15:08
Reason for closing:  Fixed
Admin
cbay commented on 02.07.2026 14:05

Hello,

the platform default umask is `022`

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.

Admin
cbay commented on 02.07.2026 14:40

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:

  
  ssh <host> umask        -> 0007   (non-interactive, non-login)
  bash -c  'umask'        -> 0007
  sh   -c  'umask'        -> 0007
  bash -lc 'umask'        -> 0007   (login)
  bash -ilc 'umask'       -> 0007   (interactive)
  
  Previously, all of these returned 0022.
  

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.

Admin
cbay commented on 02.07.2026 15:08

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

Loading...

Available keyboard shortcuts

Tasklist

Task Details

Task Editing