diff options
| author | Christian Göttsche | 2024-05-27 21:33:00 +0200 |
|---|---|---|
| committer | jvoisin | 2024-06-09 17:16:16 +0200 |
| commit | c7ce5c3528e8da8762e6e7067001549e109397ba (patch) | |
| tree | 4c9606730af25a8f893193b7cc5cb718a20c3f35 /src | |
| parent | 849252c6a48b428dde3ad8930b40a2bdf9874cb7 (diff) | |
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.
Diffstat (limited to 'src')
| -rw-r--r-- | src/snuffleupagus.c | 3 | ||||
| -rw-r--r-- | src/sp_config.h | 2 | ||||
| -rw-r--r-- | src/sp_config_keywords.c | 1 | ||||
| -rw-r--r-- | src/sp_wrapper.c | 135 | ||||
| -rw-r--r-- | src/tests/stream_wrapper/config/config_stream_wrapper_php.ini | 2 | ||||
| -rw-r--r-- | src/tests/stream_wrapper/stream_wrapper_php.phpt | 76 |
6 files changed, 219 insertions, 0 deletions
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) { | |||
| 113 | SP_INIT_NULL(config_eval.blacklist); | 113 | SP_INIT_NULL(config_eval.blacklist); |
| 114 | SP_INIT_NULL(config_eval.whitelist); | 114 | SP_INIT_NULL(config_eval.whitelist); |
| 115 | SP_INIT_NULL(config_wrapper.whitelist); | 115 | SP_INIT_NULL(config_wrapper.whitelist); |
| 116 | SP_INIT_NULL(config_wrapper.php_stream_allowlist); | ||
| 116 | #undef SP_INIT_NULL | 117 | #undef SP_INIT_NULL |
| 117 | } | 118 | } |
| 118 | 119 | ||
| @@ -175,6 +176,7 @@ static PHP_GSHUTDOWN_FUNCTION(snuffleupagus) { | |||
| 175 | FREE_LST(config_eval.blacklist); | 176 | FREE_LST(config_eval.blacklist); |
| 176 | FREE_LST(config_eval.whitelist); | 177 | FREE_LST(config_eval.whitelist); |
| 177 | FREE_LST(config_wrapper.whitelist); | 178 | FREE_LST(config_wrapper.whitelist); |
| 179 | FREE_LST(config_wrapper.php_stream_allowlist); | ||
| 178 | #undef FREE_LST | 180 | #undef FREE_LST |
| 179 | 181 | ||
| 180 | 182 | ||
| @@ -388,6 +390,7 @@ static void dump_config(void) { | |||
| 388 | add_assoc_bool(&arr, SP_TOKEN_SLOPPY_COMPARISON "." SP_TOKEN_ENABLE, SPCFG(sloppy).enable); | 390 | add_assoc_bool(&arr, SP_TOKEN_SLOPPY_COMPARISON "." SP_TOKEN_ENABLE, SPCFG(sloppy).enable); |
| 389 | 391 | ||
| 390 | ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_LIST, SPCFG(wrapper).whitelist); | 392 | ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_LIST, SPCFG(wrapper).whitelist); |
| 393 | ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_ALLOW_PHP_STREAMS, SPCFG(wrapper).php_stream_allowlist); | ||
| 391 | 394 | ||
| 392 | #undef ADD_ASSOC_SPLIST | 395 | #undef ADD_ASSOC_SPLIST |
| 393 | 396 | ||
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 { | |||
| 70 | 70 | ||
| 71 | typedef struct { | 71 | typedef struct { |
| 72 | sp_list_node *whitelist; | 72 | sp_list_node *whitelist; |
| 73 | sp_list_node *php_stream_allowlist; | ||
| 73 | bool enabled; | 74 | bool enabled; |
| 74 | size_t num_wrapper; // Used to verify if wrappers were added. | 75 | size_t num_wrapper; // Used to verify if wrappers were added. |
| 75 | } sp_config_wrapper; | 76 | } sp_config_wrapper; |
| @@ -214,6 +215,7 @@ typedef struct { | |||
| 214 | #define SP_TOKEN_EVAL_WHITELIST "eval_whitelist" | 215 | #define SP_TOKEN_EVAL_WHITELIST "eval_whitelist" |
| 215 | #define SP_TOKEN_SLOPPY_COMPARISON "sloppy_comparison" | 216 | #define SP_TOKEN_SLOPPY_COMPARISON "sloppy_comparison" |
| 216 | #define SP_TOKEN_ALLOW_WRAPPERS "wrappers_whitelist" | 217 | #define SP_TOKEN_ALLOW_WRAPPERS "wrappers_whitelist" |
| 218 | #define SP_TOKEN_ALLOW_PHP_STREAMS "php_list" | ||
| 217 | #define SP_TOKEN_INI_PROTECTION "ini_protection" | 219 | #define SP_TOKEN_INI_PROTECTION "ini_protection" |
| 218 | #define SP_TOKEN_INI "ini" | 220 | #define SP_TOKEN_INI "ini" |
| 219 | 221 | ||
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) { | |||
| 190 | 190 | ||
| 191 | sp_config_keyword config_keywords[] = { | 191 | sp_config_keyword config_keywords[] = { |
| 192 | {parse_list, SP_TOKEN_LIST, &cfg->whitelist}, | 192 | {parse_list, SP_TOKEN_LIST, &cfg->whitelist}, |
| 193 | {parse_list, SP_TOKEN_ALLOW_PHP_STREAMS, &cfg->php_stream_allowlist}, | ||
| 193 | {0, 0, 0}}; | 194 | {0, 0, 0}}; |
| 194 | 195 | ||
| 195 | SP_PROCESS_CONFIG_KEYWORDS_ERR(); | 196 | 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 @@ | |||
| 1 | #include "php_snuffleupagus.h" | 1 | #include "php_snuffleupagus.h" |
| 2 | 2 | ||
| 3 | #define LOG_FEATURE "wrappers_whitelist" | ||
| 4 | |||
| 3 | static bool wrapper_is_whitelisted(const zend_string *const zs) { | 5 | static bool wrapper_is_whitelisted(const zend_string *const zs) { |
| 4 | const sp_list_node *list = SPCFG(wrapper).whitelist; | 6 | const sp_list_node *list = SPCFG(wrapper).whitelist; |
| 5 | 7 | ||
| @@ -16,6 +18,131 @@ static bool wrapper_is_whitelisted(const zend_string *const zs) { | |||
| 16 | return false; | 18 | return false; |
| 17 | } | 19 | } |
| 18 | 20 | ||
| 21 | static bool sp_php_stream_is_filtered(void) { | ||
| 22 | const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist; | ||
| 23 | |||
| 24 | return list != NULL; | ||
| 25 | } | ||
| 26 | |||
| 27 | static bool sp_php_stream_is_whitelisted(const char *const kind) { | ||
| 28 | const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist; | ||
| 29 | |||
| 30 | while (list) { | ||
| 31 | if (!strcasecmp(kind, ZSTR_VAL((const zend_string *)list->data))) { | ||
| 32 | return true; | ||
| 33 | } | ||
| 34 | list = list->next; | ||
| 35 | } | ||
| 36 | return false; | ||
| 37 | } | ||
| 38 | |||
| 39 | /* | ||
| 40 | * Adopted from | ||
| 41 | * https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L176 | ||
| 42 | */ | ||
| 43 | static php_stream * sp_php_stream_url_wrap_php(php_stream_wrapper *wrapper, | ||
| 44 | const char *path, const char *mode, | ||
| 45 | int options, zend_string **opened_path, | ||
| 46 | php_stream_context *context STREAMS_DC) { | ||
| 47 | if (!strncasecmp(path, "php://", 6)) { | ||
| 48 | path += 6; | ||
| 49 | } | ||
| 50 | |||
| 51 | if (!strncasecmp(path, "temp", 4)) { | ||
| 52 | if (!sp_php_stream_is_whitelisted("temp")) { | ||
| 53 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"temp\" dropped"); | ||
| 54 | return NULL; | ||
| 55 | } | ||
| 56 | } else if (!strcasecmp(path, "memory")) { | ||
| 57 | if (!sp_php_stream_is_whitelisted("memory")) { | ||
| 58 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"memory\" dropped"); | ||
| 59 | return NULL; | ||
| 60 | } | ||
| 61 | } else if (!strcasecmp(path, "output")) { | ||
| 62 | if (!sp_php_stream_is_whitelisted("output")) { | ||
| 63 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"output\" dropped"); | ||
| 64 | return NULL; | ||
| 65 | } | ||
| 66 | } else if (!strcasecmp(path, "input")) { | ||
| 67 | if (!sp_php_stream_is_whitelisted("input")) { | ||
| 68 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"input\" dropped"); | ||
| 69 | return NULL; | ||
| 70 | } | ||
| 71 | } else if (!strcasecmp(path, "stdin")) { | ||
| 72 | if (!sp_php_stream_is_whitelisted("stdin")) { | ||
| 73 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdin\" dropped"); | ||
| 74 | return NULL; | ||
| 75 | } | ||
| 76 | } else if (!strcasecmp(path, "stdout")) { | ||
| 77 | if (!sp_php_stream_is_whitelisted("stdout")) { | ||
| 78 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdout\" dropped"); | ||
| 79 | return NULL; | ||
| 80 | } | ||
| 81 | } else if (!strcasecmp(path, "stderr")) { | ||
| 82 | if (!sp_php_stream_is_whitelisted("stderr")) { | ||
| 83 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stderr\" dropped"); | ||
| 84 | return NULL; | ||
| 85 | } | ||
| 86 | } else if (!strncasecmp(path, "fd/", 3)) { | ||
| 87 | if (!sp_php_stream_is_whitelisted("fd")) { | ||
| 88 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"fd\" dropped"); | ||
| 89 | return NULL; | ||
| 90 | } | ||
| 91 | } else if (!strncasecmp(path, "filter/", 7)) { | ||
| 92 | if (!sp_php_stream_is_whitelisted("filter")) { | ||
| 93 | sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"filter\" dropped"); | ||
| 94 | return NULL; | ||
| 95 | } | ||
| 96 | } else { | ||
| 97 | sp_log_warn(LOG_FEATURE, "Call to unknown php stream type dropped"); | ||
| 98 | return NULL; | ||
| 99 | } | ||
| 100 | |||
| 101 | extern PHPAPI const php_stream_wrapper php_stream_php_wrapper; | ||
| 102 | |||
| 103 | return php_stream_php_wrapper.wops->stream_opener(wrapper, path, mode, options, opened_path, context STREAMS_DC); | ||
| 104 | } | ||
| 105 | |||
| 106 | /* | ||
| 107 | * Adopted from | ||
| 108 | * https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L428-L446 | ||
| 109 | */ | ||
| 110 | static const php_stream_wrapper_ops sp_php_stdio_wops = { | ||
| 111 | sp_php_stream_url_wrap_php, | ||
| 112 | NULL, /* close */ | ||
| 113 | NULL, /* fstat */ | ||
| 114 | NULL, /* stat */ | ||
| 115 | NULL, /* opendir */ | ||
| 116 | "PHP", | ||
| 117 | NULL, /* unlink */ | ||
| 118 | NULL, /* rename */ | ||
| 119 | NULL, /* mkdir */ | ||
| 120 | NULL, /* rmdir */ | ||
| 121 | NULL | ||
| 122 | }; | ||
| 123 | static const php_stream_wrapper sp_php_stream_php_wrapper = { | ||
| 124 | &sp_php_stdio_wops, | ||
| 125 | NULL, | ||
| 126 | 0, /* is_url */ | ||
| 127 | }; | ||
| 128 | |||
| 129 | static void sp_reregister_php_wrapper(void) { | ||
| 130 | if (!sp_php_stream_is_filtered()) { | ||
| 131 | return; | ||
| 132 | } | ||
| 133 | |||
| 134 | if (php_unregister_url_stream_wrapper("php") != SUCCESS) { | ||
| 135 | sp_log_warn(LOG_FEATURE, "Failed to unregister stream wrapper \"php\""); | ||
| 136 | return; | ||
| 137 | } | ||
| 138 | |||
| 139 | if (php_register_url_stream_wrapper("php", &sp_php_stream_php_wrapper) != SUCCESS) { | ||
| 140 | sp_log_warn(LOG_FEATURE, "Failed to register custom stream wrapper \"php\""); | ||
| 141 | } | ||
| 142 | |||
| 143 | sp_log_debug(LOG_FEATURE, "Stream \"php\" successfully re-registered"); | ||
| 144 | } | ||
| 145 | |||
| 19 | void sp_disable_wrapper() { | 146 | void sp_disable_wrapper() { |
| 20 | HashTable *orig = php_stream_get_url_stream_wrappers_hash(); | 147 | HashTable *orig = php_stream_get_url_stream_wrappers_hash(); |
| 21 | HashTable *orig_complete = pemalloc(sizeof(HashTable), 1); | 148 | HashTable *orig_complete = pemalloc(sizeof(HashTable), 1); |
| @@ -50,6 +177,12 @@ PHP_FUNCTION(sp_stream_wrapper_register) { | |||
| 50 | zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "S*", &protocol_name, ¶ms, ¶m_count); | 177 | zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "S*", &protocol_name, ¶ms, ¶m_count); |
| 51 | // ignore proper arguments here and just let the original handler deal with it | 178 | // ignore proper arguments here and just let the original handler deal with it |
| 52 | if (!protocol_name || wrapper_is_whitelisted(protocol_name)) { | 179 | if (!protocol_name || wrapper_is_whitelisted(protocol_name)) { |
| 180 | |||
| 181 | // reject manual loading of "php" wrapper | ||
| 182 | if (!strcasecmp(ZSTR_VAL(protocol_name), "php") && sp_php_stream_is_filtered()) { | ||
| 183 | return; | ||
| 184 | } | ||
| 185 | |||
| 53 | orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("stream_wrapper_register")); | 186 | orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("stream_wrapper_register")); |
| 54 | orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); | 187 | orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); |
| 55 | } | 188 | } |
| @@ -61,5 +194,7 @@ int hook_stream_wrappers() { | |||
| 61 | HOOK_FUNCTION("stream_wrapper_register", sp_internal_functions_hook, | 194 | HOOK_FUNCTION("stream_wrapper_register", sp_internal_functions_hook, |
| 62 | PHP_FN(sp_stream_wrapper_register)); | 195 | PHP_FN(sp_stream_wrapper_register)); |
| 63 | 196 | ||
| 197 | sp_reregister_php_wrapper(); | ||
| 198 | |||
| 64 | return SUCCESS; | 199 | return SUCCESS; |
| 65 | } | 200 | } |
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 @@ | |||
| 1 | sp.wrappers_whitelist.list("php"); | ||
| 2 | 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 @@ | |||
| 1 | --TEST-- | ||
| 2 | Stream wrapper (php) | ||
| 3 | --SKIPIF-- | ||
| 4 | <?php | ||
| 5 | if (!extension_loaded("snuffleupagus")) print "skip snuffleupagus extension missing"; | ||
| 6 | ?> | ||
| 7 | --INI-- | ||
| 8 | sp.configuration_file={PWD}/config/config_stream_wrapper_php.ini | ||
| 9 | --FILE-- | ||
| 10 | <?php | ||
| 11 | echo file_get_contents('php://input'); | ||
| 12 | file_put_contents('php://output', "Hello from stdout\n"); | ||
| 13 | file_put_contents('php://stderr', "Hello from stderr #1\n"); | ||
| 14 | file_put_contents('php://memory', "Bye from memory\n"); | ||
| 15 | echo file_get_contents('php://memory'); | ||
| 16 | file_put_contents('php://temp', "Bye from temp\n"); | ||
| 17 | echo file_get_contents('php://temp'); | ||
| 18 | |||
| 19 | file_put_contents('php://stderr', "Hello from stderr #2\n"); | ||
| 20 | |||
| 21 | file_put_contents('php://filter/write=string.toupper/resource=output.tmp', "Hello from stdout filtered\n"); | ||
| 22 | echo file_get_contents('php://filter/read=string.toupper/resource=output.tmp'); | ||
| 23 | |||
| 24 | $foo = stream_wrapper_unregister("php"); | ||
| 25 | fwrite(STDERR, $foo); | ||
| 26 | file_put_contents('php://stderr', "Hello from stderr #3\n"); | ||
| 27 | |||
| 28 | stream_wrapper_restore("php"); | ||
| 29 | file_put_contents('php://stderr', "Hello from stderr #4\n"); | ||
| 30 | file_put_contents('php://memory', "Bye from memory\n"); | ||
| 31 | ?> | ||
| 32 | --EXPECTF-- | ||
| 33 | 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 | ||
| 34 | |||
| 35 | Warning: file_get_contents(php://input): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 2 | ||
| 36 | |||
| 37 | 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 | ||
| 38 | |||
| 39 | Warning: file_put_contents(php://output): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 3 | ||
| 40 | Hello from stderr #1 | ||
| 41 | |||
| 42 | 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 | ||
| 43 | |||
| 44 | Warning: file_put_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 5 | ||
| 45 | |||
| 46 | 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 | ||
| 47 | |||
| 48 | Warning: file_get_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 6 | ||
| 49 | |||
| 50 | 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 | ||
| 51 | |||
| 52 | Warning: file_put_contents(php://temp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 7 | ||
| 53 | |||
| 54 | 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 | ||
| 55 | |||
| 56 | Warning: file_get_contents(php://temp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 8 | ||
| 57 | Hello from stderr #2 | ||
| 58 | |||
| 59 | 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 | ||
| 60 | |||
| 61 | 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 | ||
| 62 | |||
| 63 | 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 | ||
| 64 | |||
| 65 | 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 | ||
| 66 | 1 | ||
| 67 | 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 | ||
| 68 | |||
| 69 | Warning: file_put_contents(): file:// wrapper is disabled in the server configuration in %a/stream_wrapper_php.php on line 17 | ||
| 70 | |||
| 71 | 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 | ||
| 72 | Hello from stderr #4 | ||
| 73 | |||
| 74 | 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 | ||
| 75 | |||
| 76 | Warning: file_put_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line 21 | ||
