How do PHP's disable_functions and open_basedir directives harden a server, and why is neither a real security boundary on its own?
answer
- disable_functions: INI_SYSTEM, php.ini only
- since 8.0 disabled means undefined
- open_basedir can only be tightened
- directory name, not a prefix
- child processes and sockets escape it
basics
~20 sdisable_functions removes named internal functions and open_basedir limits which paths PHP's file functions may open. Both are useful defence in depth, but the manual says disable_functions can be circumvented, and open_basedir does not bind child processes.
solid answer
~40 s`disable_functions` takes a comma-separated list of **internal** functions, such as `exec,shell_exec,system,proc_open`. It is `INI_SYSTEM`, set in php.ini only, and since PHP 8.0 a disabled function is treated as **undefined**: calling it throws `Error` ("Call to undefined function"), `function_exists()` returns `false`, and user code may even define a function with that name. `open_basedir` restricts paths opened by PHP's file functions and `include` to listed directory trees, after resolving symlinks. It is `INI_ALL`, but at runtime it can only be **tightened**, and since 8.3 `ini_set()` rejects paths containing `..`. Neither is a sandbox: the manual warns that `disable_functions` can be circumvented and is not sufficient for shared hosting, and `open_basedir` does not bind programs PHP starts or Unix domain sockets. Real isolation comes from separate OS users, containers and file permissions.
code
ini · 3 lines; php.ini for the photo-contest site's PHP-FPM pool
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
open_basedir = /var/www/contest:/tmpgo deeper
Know that disable_functions can turn off dangerous functions such as exec and that open_basedir limits which directories PHP can open files in.
Explain their modes, that disabled functions are undefined since PHP 8.0, and that open_basedir can only be tightened at runtime.
Name what each fails to cover, child processes, sockets and extensions, and pair them with OS users, permissions and separate FPM pools.
Decide the isolation model for multi-tenant PHP, treating php.ini hardening as a second layer behind OS and container boundaries.
## What each directive does Two php.ini directives are the classic PHP hardening knobs. They reduce what compromised or careless code can do, but they work inside the PHP process, which limits how far they reach. | | `disable_functions` | `open_basedir` | |---|---|---| | Purpose | remove named internal functions | confine file access to directory trees | | Default | empty | not set (all paths allowed) | | Mode | `INI_SYSTEM`, php.ini only | `INI_ALL`, but runtime changes may only tighten it | | Applies to | internal functions only | PHP's own file operations, `include` and extensions that honour it | ## disable_functions The directive takes a comma-separated list, for example: ```ini disable_functions = exec,passthru,shell_exec,system,proc_open,popen ``` Key behaviours: - **Internal functions only.** User-defined functions are unaffected. - **Since PHP 8.0, disabled means undefined.** The manual's migration notes say disabled functions are treated exactly like non-existent ones: calling one throws `Error: Call to undefined function exec()`, `function_exists('exec')` returns `false`, and redefining a disabled function in user code is possible. Before 8.0 the function stayed defined and calling it only produced a warning. - **System-level only.** A tenant's `.user.ini`, `.htaccess` or `ini_set()` cannot change it. - **Classes are no longer covered.** The companion `disable_classes` directive was removed in PHP 8.5. The manual adds an explicit caveat: the directive **can be circumvented** and should not be considered a sufficient security measure for shared hosting. Code that can run arbitrary PHP has other paths to the same capability, through other extensions or engine bugs. ## open_basedir `open_basedir` lists directory trees PHP may open files in, separated by `:` on Unix and `;` on Windows. When a script calls `fopen()`, `file_get_contents()`, `include` and similar functions, PHP resolves the path, including **symlinks**, and refuses access outside the list with a warning. Points interviewers probe: 1. **A directory name, not a prefix.** The manual states the restriction is a directory name, so `/var/www/contest` does not also allow `/var/www/contest-backup`. 2. **Tighten only at runtime.** A script may narrow it with `ini_set('open_basedir', '/var/www/contest/uploads')` when the configured value is `/var/www/contest`, but may not widen or remove it. Since PHP 8.3, `ini_set()` also rejects values containing `..`. 3. **It disables the realpath cache**, because every path must be checked, which costs some filesystem performance. 4. **`.` means the working directory**, which the manual calls dangerous, since `chdir()` can change it. ## Why neither is a boundary Both live inside PHP, so anything outside PHP's own checks escapes them: - **Child processes.** If a program is started through any shell or process function left enabled, that program runs as the OS user and ignores `open_basedir`. - **Unix domain sockets.** The manual notes `open_basedir` does not restrict connecting to a Unix socket path with `stream_socket_client()` or `fsockopen()`. - **Extensions and bugs.** Any extension that forgets to check, or an engine bug, bypasses the rule, which is why the manual will not call `disable_functions` sufficient. ## What real isolation looks like - run each site or tenant as its **own OS user**, with its own PHP-FPM pool; - use **file permissions** so a site cannot read another's files at all; - put untrusted workloads in **containers or VMs**; - then add `disable_functions` and `open_basedir` as **defence in depth**, a second layer that stops simple exploits and mistakes. ## Side effects to plan for - **Libraries that probe for functions.** Some packages check `function_exists('proc_open')` and fall back or fail loudly; with disabled functions they take the fallback path, which should be tested. - **Temporary files and sessions.** `open_basedir` must include the directories PHP writes to, such as the upload and session paths, or uploads and sessions break with restriction warnings. - **Performance.** Every checked path costs a resolution step, and the realpath cache is off. ## Testing the settings Verify from the SAPI that serves the site: `function_exists('exec')` should return `false`, and an attempt to read `/etc/passwd` should fail with an `open_basedir restriction in effect` warning. Checking from the CLI proves nothing if the CLI loads a different configuration.
- What happens in PHP 8 when code calls a function listed in disable_functions?The function does not exist for that process, so the call throws Error with 'Call to undefined function'. function_exists() returns false, and user code may define its own function with the same name. Before PHP 8.0 the call only emitted a warning.
- Can a script widen open_basedir with ini_set()?No. At runtime the new value must be at least as restrictive as the current one, so ini_set() can only narrow the list; widening or clearing it returns false. Since PHP 8.3, values containing .. are also rejected.
- Why doesn't open_basedir stop a shell command from reading /etc/passwd?open_basedir is enforced by PHP's own file functions. A program started through an enabled shell or process function runs as the OS user, outside PHP, so it reads whatever that user's permissions allow.
saying these in an interview costs you the question
- disable_functions can be set per site with ini_set() or .user.ini.
- Calling a disabled function in PHP 8 only raises a warning.
- open_basedir=/var/www/app also allows /var/www/app-old.
- open_basedir stops shell commands from reading outside the allowed paths.
- With disable_functions and open_basedir set, tenants on one user are isolated.