From 03245c95498b441b03fef1b1f3b0d60755aaebfa Mon Sep 17 00:00:00 2001 From: Tom Van Looy Date: Mon, 20 Nov 2023 15:49:44 +0100 Subject: [PATCH] Add support for openbsd.pledge_promises, openbsd.pledge_execpromises and openbsd.unveil ini directives --- README.md | 289 ++++++++++++++++++++++------- package.xml | 63 ++++++- php_pledge.h | 2 +- pledge.c | 163 ++++++++++++++-- tests/pledge_dns_lookup.phpt | 11 +- tests/unveil_increase_attempt.phpt | 2 +- 6 files changed, 441 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 960b7cf..a52f0b6 100644 --- a/README.md +++ b/README.md @@ -2,68 +2,180 @@ This is a PHP extension that adds support for OpenBSD's pledge and unveil system calls. -## OpenBSD Ports +## Requirements + +This extension works with >= PHP 7.2 and needs at least OpenBSD 6.4. + +## The theory + +The [pledge(2) system call](http://man.openbsd.org/OpenBSD-current/man2/pledge.2) allows a program to restrict the types +of operations the program can do after that point. Unlike other similar systems, pledge is specifically designed for +programs that need to use a wide variety of operations on initialisation, but a fewer number after initialisation (when +user input will be accepted). -This package is present in OpenBSD ports. To install it, make sure you [have the ports tree](https://www.openbsd.org/faq/ports/ports.html#PortsFetch) -on your machine. Next, install the port: +The pledge system call is supported on OpenBSD >= 5.9 +The first call to [unveil(2) system call](http://man.openbsd.org/OpenBSD-current/man2/unveil.2) restricts the filesystem +view. Subsequent calls can open it more. To prevent further unveiling, call unveil with no parameters or drop the unveil +pledge if the program is pledged. + +The unveil system call is supported on OpenBSD >= 6.4 + +## OpenBSD installation + +This package is present in OpenBSD packages. To install it, run: + +```sh +pkg_add pecl82-pledge-2.1.2 ``` + +This package can also be installed from ports. To install it, make sure you +[have the ports tree](https://www.openbsd.org/faq/ports/ports.html#PortsFetch) on your machine. Next, install the port: + +```sh cd /usr/ports/www/pecl-pledge/ -env FLAVOR=php80 make FETCH_PACKAGES= install +env FLAVOR=php82 make FETCH_PACKAGES= install ``` Using the extension: -``` -echo 'extension=pledge' > /etc/php-8.0/pledge.ini -php-8.0 -m | grep pledge +```sh +echo 'extension=pledge' > /etc/php-8.2/pledge.ini +php-8.2 -m | grep pledge ``` -## Requirements +You can then use the `pledge()` and `unveil()` functions, or set a pledge and unveil default with ini values: -This extension works with >= 7.2 and needs at least OpenBSD 6.4. Note that with PHP >= 7.4 this extension is more or less useless but still works. -You can use the Foreign Function Interface (FFI) extension that is in core to call libc functions: +- openbsd.pledge_promises +- openbsd.pledge_execpromises +- openbsd.unveil -```bash -$ cat test_ffi.php -unveil(__DIR__, 'r'); -scandir('/etc'); +You can set promises and unveils in your PHP-FPM config. Eg: -$ php -dextension=ffi test_ffi.php -int(77) +``` +php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil inet +php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc,null:null +``` -Warning: scandir(/etc): failed to open dir: No such file or directory in /home/tvl/test_ffi.php on line 9 +An simplified httpd example config: -Warning: scandir(): (errno 2): No such file or directory in /home/tvl/test_ffi.php on line 9 +``` +chroot "/var/www" + +server "example.com" { + listen on * port 80 + root "/htdocs/public" + directory index "index.php" + + # Assets not served by PHP + location match "\.(css|gif|jpg|png|js)$" { + pass + } + + location match "/specific-path-1" { + request rewrite "/index.php/%1" + fastcgi socket "/run/php-fpm-specific-path-1.sock" + } + + location match "/specific-path-2" { + request rewrite "/index.php/%1" + fastcgi socket "/run/php-fpm-specific-path-2.sock" + } + + # The default PHP handler + location match "^/(.+)$" { + request rewrite "/index.php/%1" + fastcgi socket "/run/php-fpm.sock" + } +} ``` -## The theory +With a simplified PHP-FPM config: -The [pledge(2) system call](http://man.openbsd.org/OpenBSD-current/man2/pledge.2) allows a program to restrict the types -of operations the program can do after that point. Unlike other similar systems, pledge is specifically designed for -programs that need to use a wide variety of operations on initialisation, but a fewer number after initialisation (when -user input will be accepted). +``` +[global] +include=/etc/php-fpm.d/*.conf + +[specific-path-1] +user = www +group = www +listen.owner = www +listen.group = www +listen.mode = 0660 + +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +chroot = /var/www +pm.max_requests = 1 + +listen = /var/www/run/php-fpm-specific-path-1.sock +php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil +php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc + +[specific-path-2] +user = www +group = www +listen.owner = www +listen.group = www +listen.mode = 0660 + +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +chroot = /var/www +pm.max_requests = 1 + +listen = /var/www/run/php-fpm-specific-path-2.sock +php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil +php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc + +[www] +user = www +group = www +listen.owner = www +listen.group = www +listen.mode = 0660 + +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +chroot = /var/www +pm.max_requests = 1 + +listen = /var/www/run/php-fpm.sock +php_admin_value[openbsd.pledge_promises] = stdio rpath wpath cpath fattr flock unveil inet +php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc +``` -The pledge system call is supported on OpenBSD >= 5.9 +Don't forget to call `unveil(null, null);` in your PHP userland to disallow future unveil calls, or specify null:null as +the last argument eg: -The first call to [unveil(2) system call](http://man.openbsd.org/OpenBSD-current/man2/unveil.2) restricts the filesystem -view. Subsequent calls can open it more. To prevent further unveiling, call unveil with no parameters or drop the unveil -pledge if the program is pledged. - -The unveil system call is supported on OpenBSD >= 6.4 +``` +php_admin_value[openbsd.unveil] = /:r,/tmp:rwc,/htdocs/var/log:rwc,/htdocs/var/cache:rwc,null:null +``` -## Build +## Build from source -Install a release with PECL (see [security/pledge](https://pecl.php.net/package/pledge)) or build the latest from source: +Install a release from PECL [security/pledge](https://pecl.php.net/package/pledge) or build the latest from source: -``` +```sh git clone https://github.com/tvlooy/php-pledge cd php-pledge phpize @@ -75,19 +187,17 @@ doas make install Add the following to your configuration to enable the extension: ``` -extension=pledge.so +extension=pledge ``` Run the tests with: -``` +```sh NO_INTERACTION=1 make test ``` ## Pledge usage -Note: You will need to include the ```stdio``` promise every time because PHP will not work well without it. - All promises are documented in [the OpenBSD pledge(2) manual page](http://man.openbsd.org/OpenBSD-current/man2/pledge.2). When pledging your program keep an eye on ```/var/log/messages``` to see your violations. @@ -106,6 +216,25 @@ pledge('stdio inet unix dns'); If the PHP ```pledge()``` call fails, it will throw a ```\PledgeException```. +The ```error``` pledge will make the call fail but will not kill the program. An example: + +```sh +$ php \ + -dopenbsd.pledge_promises='stdio dns' \ + -r 'echo gethostbyname("openbsd.org");' +199.185.178.80 + +$ php \ + -dopenbsd.pledge_promises='stdio error' \ + -r 'echo gethostbyname("openbsd.org");' +openbsd.org + +$ php \ + -dopenbsd.pledge_promises='stdio' \ + -r 'echo gethostbyname("openbsd.org");' +Abort trap (core dumped) +``` + To set promises for an execve child: ```php @@ -132,52 +261,76 @@ unveil(); # No longer works unveil('/var', 'r'); - ``` If the PHP ```unveil()``` call fails, it will throw a ```\UnveilException```. +You can also restrict a process to /var/empty with unveil from the CLI: + +```sh +$ php \ + -dopenbsd.unveil='/var/empty:r,null:null' \ + -dopenbsd.pledge_promises='stdio dns' \ + -r 'echo gethostbyname("openbsd.org");' +199.185.178.80 +``` + ## Usage tips -Be careful what you pledge! If you run PHP with mod_php, you will be pledging an Apache child processes! If you pledge -php_fpm you will be pledging it for the lifetime of the process, not just the request! +See [ctors/pledge-symfony-routing](https://packagist.org/packages/ctors/pledge-symfony-routing) for Symfony framework +support. + +Be careful where you pledge / unveil! If you run PHP with mod_php, you will be pledging an Apache child processes! If +you pledge / unveil php_fpm you will be pledging it for the lifetime of the process, not just the request! + +Set `pm.max_requests = 1` in the PHP-FPM config to always start with a new process. For your web SAPI, make sure you limit the amount of loaded extensions. If you only need ```phar```, ```pcntl```, ... in your CLI, then don't load them in your web SAPI. -Unveil can for example be applied to FPM processes in addition to or as an alternative to chroot. +Unveil can for example be applied to FPM processes in addition to or as an alternative to chroot. Note that PHP-FPM runs +chroot-ed by default on OpenBSD, but you can't pledge chroot, so you can't start FPM with a openbsd.pledge_promises. + +Ways to configure: -Pledging CLI processes is the most convenient usecase. Examples: + - set `openbsd.pledge_promises` and/or `openbsd.unveil` in `php.ini` + - or set `php_admin_value[openbsd.pledge_promises]` and/or `php_admin_value[openbsd.unveil]` in the PHP-FPM config + - and/or apply `pledge()` and `unveil()` in your userland code -If you are running the php interactive shell with ```php -a``` you need these promises: +If you want to reuse pledged / unveiled processes, you can work around failing calls by checking if the process is +already running with a restricted view. Eg: ```php -pledge('stdio rpath wpath cpath tty ioctl'); +if (is_dir('/etc')) { + unveil(__DIR__, 'r'); +} +pledge('stdio rpath flock inet'); ``` -If you want to pledge Drupal8 or Symfony2 running on FPM, you need at least: +## A word about FFI -```php -pledge('stdio rpath wpath cpath inet dns flock fattr'); -``` +Note that with PHP >= 7.4 you could also use the Foreign Function Interface (FFI) extension that is in core to call libc +functions. But the FFI extension is not built by default in the OpenBSD ports/packages. And, it does not come with ini +settings like this pledge extension, so you cannot hook pledge() and unveil() into the PHP engine startup. -Preventing filesystem or network access with pledge seems impossible. +```sh +$ cat test_ffi.php +unveil(__DIR__, 'r'); +scandir('/etc'); + +$ php -dextension=ffi test_ffi.php +int(77) -You can then further limit filesystem access with unveil(). If you are in web SAPI, remember that you are not limiting -filesystem access for one request but for all subsequent requests. Avoid having to add the ```unveil``` promise by checking -if the process is already running with a restricted view. Eg: +Warning: scandir(/etc): failed to open dir: No such file or directory in /home/tvl/test_ffi.php on line 9 -```php -if (is_dir('/etc')) { - unveil(__DIR__, 'r'); -} -pledge('stdio rpath flock inet'); +Warning: scandir(): (errno 2): No such file or directory in /home/tvl/test_ffi.php on line 9 ``` ### Limiting network calls @@ -189,3 +342,9 @@ block out proto {tcp udp} user your_fpm_user pass out proto tcp to $mysql_db port 3306 user your_fpm_user pass out proto tcp to $some_rest_api port 443 user your_fpm_user ``` + +### Package + +Run `pecl package` to package this extension. + +On OpenBSD, get a base64 sha256 checksum with `sha256 -b *.tgz` and the file size with `ls -l *.tgz`. diff --git a/package.xml b/package.xml index 832729f..fc63529 100644 --- a/package.xml +++ b/package.xml @@ -10,10 +10,10 @@ tom@ctors.net yes - 2022-10-07 + 2023-11-22 - 2.0.3 - 2.0.3 + 2.1.2 + 2.1.2 stable @@ -21,7 +21,7 @@ ISC License -- handle EFAULT errors + - rename ini settings @@ -58,6 +58,51 @@ pledge + + 2023-11-21 + + 2.1.1 + 2.1.1 + + + stable + stable + + ISC License + + - set the ini values on runtime init, so we use set them with php_admin_value in fpm config + + + + 2023-11-20 + + 2.1.0 + 2.1.0 + + + stable + stable + + ISC License + + - add support for openbsd.promises, openbsd.execpromises and openbsd.unveil ini directives + + + + 2022-10-07 + + 2.0.3 + 2.0.3 + + + stable + stable + + ISC License + + - handle EFAULT errors + + 2018-10-13 @@ -70,7 +115,7 @@ ISC License -- correct reflection information + - correct reflection information @@ -85,7 +130,7 @@ ISC License -- add unveil test + - add unveil test @@ -100,8 +145,8 @@ ISC License -- add execpromises support to pledge() -- add unveil() + - add execpromises support to pledge() + - add unveil() @@ -116,7 +161,7 @@ ISC License -- add pledge() + - add pledge() diff --git a/php_pledge.h b/php_pledge.h index 91d4c90..45e709c 100644 --- a/php_pledge.h +++ b/php_pledge.h @@ -1,7 +1,7 @@ /* php_pledge.h */ #define PHP_PLEDGE_EXTNAME "pledge" -#define PHP_PLEDGE_VERSION "2.0.3" +#define PHP_PLEDGE_VERSION "2.1.2" extern zend_module_entry pledge_module_entry; #define phpext_pledge_ptr &check_pledge_entry diff --git a/pledge.c b/pledge.c index 57829b7..7cea2b4 100644 --- a/pledge.c +++ b/pledge.c @@ -6,6 +6,7 @@ /* Include PHP API */ #include "php.h" +#include "ext/standard/info.h" #include "ext/spl/spl_exceptions.h" #include "Zend/zend_exceptions.h" @@ -34,18 +35,159 @@ zend_function_entry pledge_functions[] = { PHP_FE_END }; +/* define the ini settings we want to add */ +PHP_INI_BEGIN() + PHP_INI_ENTRY("openbsd.pledge_promises", NULL, PHP_INI_SYSTEM, NULL) + PHP_INI_ENTRY("openbsd.pledge_execpromises", NULL, PHP_INI_SYSTEM, NULL) + PHP_INI_ENTRY("openbsd.unveil", "", PHP_INI_SYSTEM, NULL) +PHP_INI_END() + +/* create exception classes */ +void init_exceptions() { + zend_class_entry ce_pe; + INIT_CLASS_ENTRY(ce_pe, "PledgeException", NULL); + pledge_exception_ce = zend_register_internal_class_ex(&ce_pe, spl_ce_RuntimeException); + + zend_class_entry ce_ue; + INIT_CLASS_ENTRY(ce_ue, "UnveilException", NULL); + unveil_exception_ce = zend_register_internal_class_ex(&ce_ue, spl_ce_RuntimeException); +} + +/* use the unveil ini value to initialize the runtime */ +int init_unveil_ini() { + char *unveil_ini; + + unveil_ini = INI_STR("openbsd.unveil"); + + if (unveil_ini != NULL) { + char *unveil_directive = strtok(unveil_ini, ","); + char *path; + char *permissions; + + while (unveil_directive != NULL) { + path = malloc(strlen(unveil_directive) + 1); + permissions = malloc(strlen(unveil_directive) + 1); + + if (sscanf(unveil_directive, "%[^:]:%s", path, permissions) != 2) { + zend_error(E_ERROR, "Error parsing unveil directive: \"%s\"\n", unveil_directive); + + return 1; + } + + /* disallow future unveil calls, also stop parsing the rest of the config */ + if (strcmp(path, "null") == 0 && strcmp(permissions, "null") == 0) { + free(path); + free(permissions); + + if (unveil(NULL, NULL) != 0) { + zend_error(E_ERROR, "Call to unveil(NULL, NULL) to disallow future unveil calls failed"); + + return 1; + } + + return 0; + } + + if (unveil(path, permissions) != 0) { + switch (errno) { + case E2BIG: + zend_error(E_ERROR, "The addition of path would exceed the per-process limit for unveiled paths"); + break; + case EFAULT: + zend_error(E_ERROR, "path or permissions points outside the process's allocated address space"); + break; + case ENOENT: + zend_error(E_ERROR, "A directory in path did not exist"); + break; + case EINVAL: + zend_error(E_ERROR, "An invalid value of permissions was used"); + break; + case EPERM: + zend_error(E_ERROR, "An attempt to increase permissions was made, or the path was not accessible, or unveil() was called after locking"); + break; + default: + zend_error(E_ERROR, "Unveil error (%d)", errno); + } + + free(path); + free(permissions); + + return 1; + } + + free(path); + free(permissions); + + unveil_directive = strtok(NULL, ","); + } + } + + return 0; +} + +/* use the pledge ini values to initialize the runtime */ +int init_pledge_ini() { + char *pledge_promises_ini; + char *pledge_execpromises_ini; + + pledge_promises_ini = INI_STR("openbsd.pledge_promises"); + pledge_execpromises_ini = INI_STR("openbsd.pledge_execpromises"); + + if (pledge(pledge_promises_ini, pledge_execpromises_ini) != 0) { + switch (errno) { + case EFAULT: + zend_error(E_ERROR, "promises or execpromises points outside the process's allocated address space"); + break; + case EINVAL: + zend_error(E_ERROR, "promises is malformed or contains invalid keywords"); + break; + case EPERM: + zend_error(E_ERROR, "This process is attempting to increase permissions"); + break; + default: + zend_error(E_ERROR, "Pledge error (%d)", errno); + } + + return 1; + } + + return 0; +} + +/* module init */ PHP_MINIT_FUNCTION(pledge) { - zend_class_entry ce; - INIT_CLASS_ENTRY(ce, "PledgeException", NULL); - pledge_exception_ce = zend_register_internal_class_ex(&ce, spl_ce_RuntimeException); + REGISTER_INI_ENTRIES(); + + init_exceptions(); return SUCCESS; } -PHP_MINIT_FUNCTION(unveil) { - zend_class_entry ce; - INIT_CLASS_ENTRY(ce, "UnveilException", NULL); - unveil_exception_ce = zend_register_internal_class_ex(&ce, spl_ce_RuntimeException); +/* runtime init */ +PHP_RINIT_FUNCTION(pledge) { + if (init_unveil_ini() != 0) { + return FAILURE; + } + if (init_pledge_ini() != 0) { + return FAILURE; + } + + return SUCCESS; +} + +/* module info */ +PHP_MINFO_FUNCTION(pledge) { + php_info_print_table_start(); + php_info_print_table_row(2, "OpenBSD pledge/unveil support", "enabled"); + php_info_print_table_row(2, "Version", PHP_PLEDGE_VERSION); + php_info_print_table_end(); + + DISPLAY_INI_ENTRIES(); +} + +/* module shutdown */ +PHP_MSHUTDOWN_FUNCTION(pledge) { + UNREGISTER_INI_ENTRIES(); return SUCCESS; } @@ -56,14 +198,14 @@ zend_module_entry pledge_module_entry = { pledge_functions, /* Function entries */ PHP_MINIT(pledge), /* Module init */ NULL, /* Module shutdown */ - NULL, /* Request init */ + PHP_RINIT(pledge), /* Request init */ NULL, /* Request shutdown */ - NULL, /* Module information */ + PHP_MINFO(pledge), /* Module information */ PHP_PLEDGE_VERSION, STANDARD_MODULE_PROPERTIES }; -/* Install module */ +/* install module */ ZEND_GET_MODULE(pledge) /* function pledge(?string $promises = null, ?string $execpromises = null): bool */ @@ -179,4 +321,3 @@ PHP_FUNCTION(unveil) { RETURN_FALSE; } - diff --git a/tests/pledge_dns_lookup.phpt b/tests/pledge_dns_lookup.phpt index 2a8850e..7a18f17 100644 --- a/tests/pledge_dns_lookup.phpt +++ b/tests/pledge_dns_lookup.phpt @@ -1,16 +1,23 @@ --TEST-- -pledge() function - dns lookup via pledge() +pledge() function - dns lookup via pledge(), also test "error" pledge --FILE-- --EXPECT-- bool(true) bool(true) +openbsd.orgbool(true) Abort trap (core dumped) Termsig=6 diff --git a/tests/unveil_increase_attempt.phpt b/tests/unveil_increase_attempt.phpt index 13965a5..7588f06 100644 --- a/tests/unveil_increase_attempt.phpt +++ b/tests/unveil_increase_attempt.phpt @@ -7,7 +7,7 @@ unveil(); unveil('/etc/', 'r'); ?> --EXPECTF-- -Fatal error: Uncaught Exception: An attempt to increase permissions was made, or the path was not accessible, or unveil() was called after locking in %s:%d +Fatal error: Uncaught UnveilException: An attempt to increase permissions was made, or the path was not accessible, or unveil() was called after locking in %s:%d Stack trace: #0 %s(%d): unveil('/etc/', 'r') #1 {main}