From c7ce5c3528e8da8762e6e7067001549e109397ba Mon Sep 17 00:00:00 2001 From: Christian Göttsche Date: Mon, 27 May 2024 21:33:00 +0200 Subject: Add option to specify the allowed "php" wrapper types In addition of the current possibility to filter wrappers by their protocol name, also add the option to filter the "php" wrapper by the requested kind. Especially the 'filter' backend can be disabled that way. --- config/default_php8.rules | 2 + doc/source/config.rst | 11 ++ doc/source/features.rst | 16 ++- src/snuffleupagus.c | 3 + src/sp_config.h | 2 + src/sp_config_keywords.c | 1 + src/sp_wrapper.c | 135 +++++++++++++++++++++ .../config/config_stream_wrapper_php.ini | 2 + src/tests/stream_wrapper/stream_wrapper_php.phpt | 76 ++++++++++++ 9 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/tests/stream_wrapper/config/config_stream_wrapper_php.ini create mode 100644 src/tests/stream_wrapper/stream_wrapper_php.phpt diff --git a/config/default_php8.rules b/config/default_php8.rules index 98cc0db..096f033 100644 --- a/config/default_php8.rules +++ b/config/default_php8.rules @@ -26,6 +26,8 @@ sp.xxe_protection.enable(); # PHP has a lot of wrappers, most of them aren't usually useful, you should # only enable the ones you're using. # sp.wrappers_whitelist.list("file,php,phar"); +# The "php" wrapper can be further filtered +# sp.wrappers_whitelist.php_list("stdout,stdin,stderr"); # Prevent sloppy comparisons. # sp.sloppy_comparison.enable(); diff --git a/doc/source/config.rst b/doc/source/config.rst index 9781046..75392d7 100644 --- a/doc/source/config.rst +++ b/doc/source/config.rst @@ -395,6 +395,17 @@ to explicitly whitelist some `stream wrappers ` +allows to explicitly allow the builtin `php streams `__. + +:: + + sp.wrappers_whitelist.php_list("stdout,stdin,stderr"); + + Eval white and blacklist ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/features.rst b/doc/source/features.rst index d7f6f7f..3855f2a 100644 --- a/doc/source/features.rst +++ b/doc/source/features.rst @@ -381,7 +381,7 @@ and using this feature to lock this up. Whitelist of stream-wrappers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Php comes with a `lot of different `__ +PHP comes with a `lot of different `__ `stream wrapper `__, and most of them are enabled by default. @@ -397,6 +397,20 @@ Examples of related vulnerabilities - `Data exfiltration via stream wrapper `__ - `Inclusion via zip/phar `__ +.. _php-stream-wrapper-allowlist-feature: + +Allowlist of php stream-wrapper +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The builtin `"php" stream wrapper `__ +has support for common streams, like ``stdin``, ``stdout`` or ``stderr``, but +also for the dangerous ``filter`` one. + +Examples of related vulnerability +""""""""""""""""""""""""""""""""" + +- `CNEXT exploits `__ + .. _eval-feature: White and blacklist in ``eval`` diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index e549692..8c09a37 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c @@ -113,6 +113,7 @@ static PHP_GINIT_FUNCTION(snuffleupagus) { SP_INIT_NULL(config_eval.blacklist); SP_INIT_NULL(config_eval.whitelist); SP_INIT_NULL(config_wrapper.whitelist); + SP_INIT_NULL(config_wrapper.php_stream_allowlist); #undef SP_INIT_NULL } @@ -175,6 +176,7 @@ static PHP_GSHUTDOWN_FUNCTION(snuffleupagus) { FREE_LST(config_eval.blacklist); FREE_LST(config_eval.whitelist); FREE_LST(config_wrapper.whitelist); + FREE_LST(config_wrapper.php_stream_allowlist); #undef FREE_LST @@ -388,6 +390,7 @@ static void dump_config(void) { add_assoc_bool(&arr, SP_TOKEN_SLOPPY_COMPARISON "." SP_TOKEN_ENABLE, SPCFG(sloppy).enable); ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_LIST, SPCFG(wrapper).whitelist); + ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_ALLOW_PHP_STREAMS, SPCFG(wrapper).php_stream_allowlist); #undef ADD_ASSOC_SPLIST diff --git a/src/sp_config.h b/src/sp_config.h index f245943..af227ba 100644 --- a/src/sp_config.h +++ b/src/sp_config.h @@ -70,6 +70,7 @@ typedef struct { typedef struct { sp_list_node *whitelist; + sp_list_node *php_stream_allowlist; bool enabled; size_t num_wrapper; // Used to verify if wrappers were added. } sp_config_wrapper; @@ -214,6 +215,7 @@ typedef struct { #define SP_TOKEN_EVAL_WHITELIST "eval_whitelist" #define SP_TOKEN_SLOPPY_COMPARISON "sloppy_comparison" #define SP_TOKEN_ALLOW_WRAPPERS "wrappers_whitelist" +#define SP_TOKEN_ALLOW_PHP_STREAMS "php_list" #define SP_TOKEN_INI_PROTECTION "ini_protection" #define SP_TOKEN_INI "ini" diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index bf54428..0150a1e 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c @@ -190,6 +190,7 @@ SP_PARSE_FN(parse_wrapper_whitelist) { sp_config_keyword config_keywords[] = { {parse_list, SP_TOKEN_LIST, &cfg->whitelist}, + {parse_list, SP_TOKEN_ALLOW_PHP_STREAMS, &cfg->php_stream_allowlist}, {0, 0, 0}}; SP_PROCESS_CONFIG_KEYWORDS_ERR(); diff --git a/src/sp_wrapper.c b/src/sp_wrapper.c index 9eb5cbc..54a3a7a 100644 --- a/src/sp_wrapper.c +++ b/src/sp_wrapper.c @@ -1,5 +1,7 @@ #include "php_snuffleupagus.h" +#define LOG_FEATURE "wrappers_whitelist" + static bool wrapper_is_whitelisted(const zend_string *const zs) { const sp_list_node *list = SPCFG(wrapper).whitelist; @@ -16,6 +18,131 @@ static bool wrapper_is_whitelisted(const zend_string *const zs) { return false; } +static bool sp_php_stream_is_filtered(void) { + const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist; + + return list != NULL; +} + +static bool sp_php_stream_is_whitelisted(const char *const kind) { + const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist; + + while (list) { + if (!strcasecmp(kind, ZSTR_VAL((const zend_string *)list->data))) { + return true; + } + list = list->next; + } + return false; +} + +/* + * Adopted from + * https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L176 + */ +static php_stream * sp_php_stream_url_wrap_php(php_stream_wrapper *wrapper, + const char *path, const char *mode, + int options, zend_string **opened_path, + php_stream_context *context STREAMS_DC) { + if (!strncasecmp(path, "php://", 6)) { + path += 6; + } + + if (!strncasecmp(path, "temp", 4)) { + if (!sp_php_stream_is_whitelisted("temp")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"temp\" dropped"); + return NULL; + } + } else if (!strcasecmp(path, "memory")) { + if (!sp_php_stream_is_whitelisted("memory")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"memory\" dropped"); + return NULL; + } + } else if (!strcasecmp(path, "output")) { + if (!sp_php_stream_is_whitelisted("output")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"output\" dropped"); + return NULL; + } + } else if (!strcasecmp(path, "input")) { + if (!sp_php_stream_is_whitelisted("input")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"input\" dropped"); + return NULL; + } + } else if (!strcasecmp(path, "stdin")) { + if (!sp_php_stream_is_whitelisted("stdin")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdin\" dropped"); + return NULL; + } + } else if (!strcasecmp(path, "stdout")) { + if (!sp_php_stream_is_whitelisted("stdout")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdout\" dropped"); + return NULL; + } + } else if (!strcasecmp(path, "stderr")) { + if (!sp_php_stream_is_whitelisted("stderr")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stderr\" dropped"); + return NULL; + } + } else if (!strncasecmp(path, "fd/", 3)) { + if (!sp_php_stream_is_whitelisted("fd")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"fd\" dropped"); + return NULL; + } + } else if (!strncasecmp(path, "filter/", 7)) { + if (!sp_php_stream_is_whitelisted("filter")) { + sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"filter\" dropped"); + return NULL; + } + } else { + sp_log_warn(LOG_FEATURE, "Call to unknown php stream type dropped"); + return NULL; + } + + extern PHPAPI const php_stream_wrapper php_stream_php_wrapper; + + return php_stream_php_wrapper.wops->stream_opener(wrapper, path, mode, options, opened_path, context STREAMS_DC); +} + +/* + * Adopted from + * https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L428-L446 + */ +static const php_stream_wrapper_ops sp_php_stdio_wops = { + sp_php_stream_url_wrap_php, + NULL, /* close */ + NULL, /* fstat */ + NULL, /* stat */ + NULL, /* opendir */ + "PHP", + NULL, /* unlink */ + NULL, /* rename */ + NULL, /* mkdir */ + NULL, /* rmdir */ + NULL +}; +static const php_stream_wrapper sp_php_stream_php_wrapper = { + &sp_php_stdio_wops, + NULL, + 0, /* is_url */ +}; + +static void sp_reregister_php_wrapper(void) { + if (!sp_php_stream_is_filtered()) { + return; + } + + if (php_unregister_url_stream_wrapper("php") != SUCCESS) { + sp_log_warn(LOG_FEATURE, "Failed to unregister stream wrapper \"php\""); + return; + } + + if (php_register_url_stream_wrapper("php", &sp_php_stream_php_wrapper) != SUCCESS) { + sp_log_warn(LOG_FEATURE, "Failed to register custom stream wrapper \"php\""); + } + + sp_log_debug(LOG_FEATURE, "Stream \"php\" successfully re-registered"); +} + void sp_disable_wrapper() { HashTable *orig = php_stream_get_url_stream_wrappers_hash(); HashTable *orig_complete = pemalloc(sizeof(HashTable), 1); @@ -50,6 +177,12 @@ PHP_FUNCTION(sp_stream_wrapper_register) { zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "S*", &protocol_name, ¶ms, ¶m_count); // ignore proper arguments here and just let the original handler deal with it if (!protocol_name || wrapper_is_whitelisted(protocol_name)) { + + // reject manual loading of "php" wrapper + if (!strcasecmp(ZSTR_VAL(protocol_name), "php") && sp_php_stream_is_filtered()) { + return; + } + orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("stream_wrapper_register")); orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); } @@ -61,5 +194,7 @@ int hook_stream_wrappers() { HOOK_FUNCTION("stream_wrapper_register", sp_internal_functions_hook, PHP_FN(sp_stream_wrapper_register)); + sp_reregister_php_wrapper(); + return SUCCESS; } diff --git a/src/tests/stream_wrapper/config/config_stream_wrapper_php.ini b/src/tests/stream_wrapper/config/config_stream_wrapper_php.ini new file mode 100644 index 0000000..bec516c --- /dev/null +++ b/src/tests/stream_wrapper/config/config_stream_wrapper_php.ini @@ -0,0 +1,2 @@ +sp.wrappers_whitelist.list("php"); +sp.wrappers_whitelist.php_list("stdin,stderr,stdout"); diff --git a/src/tests/stream_wrapper/stream_wrapper_php.phpt b/src/tests/stream_wrapper/stream_wrapper_php.phpt new file mode 100644 index 0000000..c82d2f6 --- /dev/null +++ b/src/tests/stream_wrapper/stream_wrapper_php.phpt @@ -0,0 +1,76 @@ +--TEST-- +Stream wrapper (php) +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/config_stream_wrapper_php.ini +--FILE-- + +--EXPECTF-- +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "input" dropped in %a/stream_wrapper_php.php on line 2 + +Warning: file_get_contents(php://input): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 2 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "output" dropped in %a/stream_wrapper_php.php on line 3 + +Warning: file_put_contents(php://output): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 3 +Hello from stderr #1 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line 5 + +Warning: file_put_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 5 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line 6 + +Warning: file_get_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 6 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "temp" dropped in %a/stream_wrapper_php.php on line 7 + +Warning: file_put_contents(php://temp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 7 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "temp" dropped in %a/stream_wrapper_php.php on line 8 + +Warning: file_get_contents(php://temp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 8 +Hello from stderr #2 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "filter" dropped in %a/stream_wrapper_php.php on line 12 + +Warning: file_put_contents(php://filter/write=string.toupper/resource=output.tmp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 12 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "filter" dropped in %a/stream_wrapper_php.php on line 13 + +Warning: file_get_contents(php://filter/read=string.toupper/resource=output.tmp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 13 +1 +Warning: file_put_contents(): Unable to find the wrapper "php" - did you forget to enable it when you configured PHP? in %a/stream_wrapper_php.php on line 17 + +Warning: file_put_contents(): file:// wrapper is disabled in the server configuration in %a/stream_wrapper_php.php on line 17 + +Warning: file_put_contents(php://stderr): %s to open stream: no suitable wrapper could be found in %a/stream_wrapper_php.php on line 17 +Hello from stderr #4 + +Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line 21 + +Warning: file_put_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 21 -- cgit v1.3