Skip to content
mediumCVSS 5.3CWE-22A01:2021 – Broken Access Control

open_basedir and disable_functions

Two hardening settings that are worth having and are not fixes. Knowing exactly what each one confines tells you what is still reachable when you meet them.

What they are

open_basedir confines PHP's filesystem access to a list of directories. disable_functions removes named functions from the language.

Both are worth enabling. Neither fixes an inclusion bug, and PHP's own documentation is explicit that they are not a security boundary — they are a way to make a compromise less useful, which is a real thing to want and a different thing from prevention.

What matters for testing is knowing precisely what each one covers, because the gaps are specific rather than general.

What open_basedir confines

TechniqueConfined?Why
Traversal to /etc/passwdYesPath resolution is checked against the base list
Traversal to /proc/self/environYesSame
Log poisoningYes/var/log is not in the app tree
Session poisoningYesThe session directory is not in the app tree
The temp-file raceYes, if /tmp is excludedFrequently /tmp is left in the list
php://filter source readPartlyThe resource= target is confined — to the app tree, which holds the config file
php://filter iconv chainNoNothing is read from the filesystem; the content is synthesised
data:// inline payloadNoNo filesystem is touched
php://inputNoThe request body is not a file
Reading the app's own sourceNoThat is inside the base directory by definition
zip:// or phar:// on an uploadNo, usuallyThe upload directory is normally inside the app tree
Classic RFINoA remote fetch resolves no local path

What that leaves

Read the table the other way round and the picture is clear: open_basedir is effective against everything outside the application and ineffective against everything inside it or synthesised from nothing.

That is a meaningful reduction. It removes /etc/passwd, /proc, logs, sessions, SSH keys and mounted secrets from reach — which on a container is a large fraction of what a read primitive is worth.

What it does not remove:

  • The application's own source and configuration, which is usually the highest-value read anyway.
  • Filter chains, which is to say full code execution. This is the important one: a host with open_basedir set is not protected against the single most powerful technique on this site.
  • data:// and php://input, on a configuration where allow_url_include is On.

So when you meet an open_basedir restriction in effect warning, do not conclude the finding is limited. Conclude that your traversal targets need to move inside the app tree, and that the escalation path is a filter chain rather than log poisoning.

Notes on how it behaves

php.iniphp.ini
; Directories are separated by : on Unix, ; on Windows.
open_basedir = /var/www/html:/tmp

; A trailing slash means "this directory and below".
; NO trailing slash means the value is treated as a PREFIX — so
;     open_basedir = /var/www/html
; also permits /var/www/htmlbackup and /var/www/html-old.
; This is a documented behaviour and a recurring configuration bug.
; Always write the trailing slash:
open_basedir = /var/www/html/:/tmp/

; It is checked at the point of resolution, so symlinks out of the tree are
; caught — realpath() runs first.

; It does NOT apply to:
;   - streams that touch no filesystem (data://, php://input, php://temp)
;   - anything a spawned process does. exec('cat /etc/passwd') is unaffected;
;     open_basedir is a PHP-level check, not a kernel one. Which is exactly
;     why disable_functions exists alongside it.

disable_functions

disable_functions removes functions from the language at startup. The usual list targets command execution:

disable_functions = exec,passthru,shell_exec,system,proc_open,popen,
  pcntl_exec,pcntl_fork,dl,putenv,mail,imap_open,
  posix_kill,posix_setuid,error_log

The important thing to understand for testing: this constrains what your payload can do after execution, not whether execution happens. A filter chain still runs your code. It just runs it in a PHP where system() does not exist.

The list also has to be complete, and completeness is hard. Historically bypasses have come from mail() and imap_open() reaching a shell through their fifth argument or through putenv('LD_PRELOAD=…'), from proc_open being forgotten while popen is blocked, and from FFI, which can call libc directly and makes the entire list irrelevant if it is enabled.

And it stops nothing that is not a function call: file reads, file writes, network connections via fsockopen or curl, and include itself are all unaffected unless separately disabled — at which point the application stops working.

What to do when you meet them

With open_basedir set: stop aiming outside the tree. Read the application's source and config through php://filter — still available, still the highest-value target. If you need execution, go straight to a filter chain, which is unaffected.

With disable_functions set: you may already have code execution and not realise it. Test with something that needs no disabled function — echo, file_put_contents, phpinfo(), or an outbound request via file_get_contents — before concluding the payload failed. A blank response after a filter chain often means the chain worked and system() was missing.

In a report: rate the finding on what you achieved, and note both settings as mitigating factors rather than as the fix. Recommending that they be enabled is fine as defence in depth; recommending them instead of fixing the sink is not, and a report that does so tends to result in exactly that outcome.