From 3b113be573cdbca20ce9ec9c0a6efb25ccf51db5 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Thu, 4 Jan 2018 15:59:59 +0100 Subject: Eval blacklist Add support for eval filtering, only blacklist for now--- doc/source/config.rst | 14 ++++++++++ doc/source/features.rst | 13 +++++++++ src/php_snuffleupagus.h | 3 ++ src/snuffleupagus.c | 9 ++++++ src/sp_config.c | 1 + src/sp_config.h | 12 ++++++++ src/sp_config_keywords.c | 19 +++++++++++++ src/sp_config_keywords.h | 1 + src/sp_disabled_functions.c | 40 +++++++++++++++++++++++++-- src/sp_execute.c | 3 ++ src/sp_harden_rand.c | 9 +++--- src/sp_unserialize.c | 2 +- src/tests/config/eval_backlist.ini | 1 + src/tests/config/eval_backlist_list.ini | 1 + src/tests/config/eval_backlist_simulation.ini | 1 + src/tests/eval_backlist.phpt | 16 +++++++++++ src/tests/eval_backlist_list.phpt | 16 +++++++++++ src/tests/eval_backlist_simulation.phpt | 17 ++++++++++++ src/tests/nested_eval_blacklist.phpt | 28 +++++++++++++++++++ 19 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 src/tests/config/eval_backlist.ini create mode 100644 src/tests/config/eval_backlist_list.ini create mode 100644 src/tests/config/eval_backlist_simulation.ini create mode 100644 src/tests/eval_backlist.phpt create mode 100644 src/tests/eval_backlist_list.phpt create mode 100644 src/tests/eval_backlist_simulation.phpt create mode 100644 src/tests/nested_eval_blacklist.phpt diff --git a/doc/source/config.rst b/doc/source/config.rst index e42cb99..d16474f 100644 --- a/doc/source/config.rst +++ b/doc/source/config.rst @@ -246,6 +246,20 @@ disable_xxe sp.disable_xxe.enable(); +Eval white and blacklist +^^^^^^^^^^^^^^^^^^^^^^^^ + * `default: disabled` + * :ref:`more ` + +``eval_filter`` allows to specify white and blacklist of functions allowed and +forbidden from being called inside ``eval``. The functions names are comma-separated. + +:: + + sp.eval_filter.blacklist("system,exec,shell_exec"); + sp.eval_filter.whitelist("strlen,strcmp").simulation(); + + Virtual-patching ---------------- diff --git a/doc/source/features.rst b/doc/source/features.rst index afe139a..8ecf57d 100644 --- a/doc/source/features.rst +++ b/doc/source/features.rst @@ -321,6 +321,19 @@ Snuffleupagus can prevent the execution of this kind of file. A good practice would be to use a different user to run PHP than for administrating the website, and using this feature to lock this up. +.. _eval-feature: + +White and blacklist in ``eval`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While `eval `__ is a +dangerous primitive, tricky to use right, with almost no legitimate usage +besides templating and building mathematical expressions based on user input, +it's broadly (mis)used all around the web. + +Snuffleupagus provides a white and blacklist mechanism, to explicitly allow +and forbid specific functions call from being issued inside ``eval``. + Protection against cross site request forgery ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/php_snuffleupagus.h b/src/php_snuffleupagus.h index d7c3e27..fb90d1c 100644 --- a/src/php_snuffleupagus.h +++ b/src/php_snuffleupagus.h @@ -58,10 +58,12 @@ extern zend_module_entry snuffleupagus_module_entry; #endif ZEND_BEGIN_MODULE_GLOBALS(snuffleupagus) +bool in_eval; sp_config config; bool is_config_valid; HashTable *disabled_functions_hook; HashTable *sp_internal_functions_hook; +HashTable *sp_eval_filter_functions_hook; ZEND_END_MODULE_GLOBALS(snuffleupagus) #define SNUFFLEUPAGUS_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(snuffleupagus, v) @@ -83,6 +85,7 @@ ZEND_TSRMLS_CACHE_EXTERN() #endif PHP_FUNCTION(check_disabled_function); +PHP_FUNCTION(eval_filter_callback); static inline void sp_terminate() { zend_bailout(); } diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index dd2d941..a3a2fa8 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c @@ -53,6 +53,8 @@ ZEND_DLEXPORT zend_extension zend_extension_entry = { STANDARD_ZEND_EXTENSION_PROPERTIES}; PHP_GINIT_FUNCTION(snuffleupagus) { + snuffleupagus_globals->in_eval = false; + #define SP_INIT(F) F = pecalloc(sizeof(*F), 1, 1); #define SP_INIT_HT(F) \ F = pemalloc(sizeof(*F), 1); \ @@ -60,6 +62,7 @@ PHP_GINIT_FUNCTION(snuffleupagus) { SP_INIT_HT(snuffleupagus_globals->disabled_functions_hook); SP_INIT_HT(snuffleupagus_globals->sp_internal_functions_hook); + SP_INIT_HT(snuffleupagus_globals->sp_eval_filter_functions_hook); SP_INIT(snuffleupagus_globals->config.config_unserialize); SP_INIT(snuffleupagus_globals->config.config_random); @@ -73,6 +76,7 @@ PHP_GINIT_FUNCTION(snuffleupagus) { SP_INIT(snuffleupagus_globals->config.config_disabled_functions_ret); SP_INIT(snuffleupagus_globals->config.config_cookie); SP_INIT(snuffleupagus_globals->config.config_disabled_constructs); + SP_INIT(snuffleupagus_globals->config.config_eval); snuffleupagus_globals->config.config_disabled_constructs->construct_include = sp_list_new(); @@ -83,6 +87,8 @@ PHP_GINIT_FUNCTION(snuffleupagus) { snuffleupagus_globals->config.config_disabled_functions_ret ->disabled_functions = sp_list_new(); snuffleupagus_globals->config.config_cookie->cookies = sp_list_new(); + snuffleupagus_globals->config.config_eval->blacklist = sp_list_new(); + snuffleupagus_globals->config.config_eval->whitelist = sp_list_new(); #undef SP_INIT #undef SP_INIT_HT @@ -100,6 +106,7 @@ PHP_MSHUTDOWN_FUNCTION(snuffleupagus) { pefree(SNUFFLEUPAGUS_G(F), 1); FREE_HT(disabled_functions_hook); + FREE_HT(sp_eval_filter_functions_hook); #undef FREE_HT @@ -124,6 +131,8 @@ PHP_MSHUTDOWN_FUNCTION(snuffleupagus) { FREE_LST_DISABLE(config.config_disabled_constructs->construct_include); FREE_LST_DISABLE(config.config_disabled_constructs->construct_eval); sp_list_free(SNUFFLEUPAGUS_G(config).config_cookie->cookies); + sp_list_free(SNUFFLEUPAGUS_G(config).config_eval->blacklist); + sp_list_free(SNUFFLEUPAGUS_G(config).config_eval->whitelist); #undef FREE_LST_DISABLE diff --git a/src/sp_config.c b/src/sp_config.c index 4d95062..dc5aae9 100644 --- a/src/sp_config.c +++ b/src/sp_config.c @@ -19,6 +19,7 @@ sp_config_tokens const sp_func[] = { {.func = parse_global, .token = SP_TOKEN_GLOBAL}, {.func = parse_auto_cookie_secure, .token = SP_TOKEN_AUTO_COOKIE_SECURE}, {.func = parse_disable_xxe, .token = SP_TOKEN_DISABLE_XXE}, + {.func = parse_eval_filter, .token = SP_TOKEN_EVAL}, {NULL, NULL}}; /* Top level keyword parsing */ diff --git a/src/sp_config.h b/src/sp_config.h index 2417cf9..a4a4f10 100644 --- a/src/sp_config.h +++ b/src/sp_config.h @@ -111,6 +111,12 @@ typedef struct { sp_cidr *cidr; } sp_disabled_function; +typedef struct { + sp_list_node *blacklist; + sp_list_node *whitelist; + bool simulation; +} sp_config_eval; + typedef struct { sp_list_node *disabled_functions; // list of sp_disabled_function } sp_config_disabled_functions; @@ -145,6 +151,7 @@ typedef struct { sp_config_global_strict *config_global_strict; sp_config_disable_xxe *config_disable_xxe; sp_config_disabled_constructs *config_disabled_constructs; + sp_config_eval *config_eval; } sp_config; typedef struct { @@ -170,6 +177,7 @@ typedef struct { #define SP_TOKEN_UNSERIALIZE_HMAC ".unserialize_hmac" #define SP_TOKEN_UPLOAD_VALIDATION ".upload_validation" #define SP_TOKEN_DISABLE_XXE ".disable_xxe" +#define SP_TOKEN_EVAL ".eval_filter" // common tokens #define SP_TOKEN_ENABLE ".enable(" @@ -222,6 +230,10 @@ typedef struct { // upload_validator #define SP_TOKEN_UPLOAD_SCRIPT ".script(" +// eval blacklist +#define SP_TOKEN_EVAL_BLACKLIST ".blacklist(" +#define SP_TOKEN_EVAL_WHITELIST ".whitelist(" + int sp_parse_config(const char *); int parse_array(sp_disabled_function *); diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index 998b692..85e04ab 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c @@ -102,6 +102,25 @@ int parse_global(char *line) { return parse_keywords(sp_config_funcs_global, line); } +int parse_eval_filter(char *line) { + char *token; + char *rest; + sp_config_functions sp_config_funcs[] = { + {parse_str, SP_TOKEN_EVAL_BLACKLIST, &rest}, + {parse_empty, SP_TOKEN_SIMULATION, + &(SNUFFLEUPAGUS_G(config).config_eval->simulation)}, + {0}}; + int ret = parse_keywords(sp_config_funcs, line); + if (0 != ret) { + return ret; + } + + while ((token = strtok_r(rest, ",", &rest))) { + sp_list_insert(SNUFFLEUPAGUS_G(config).config_eval->blacklist, token); + } + return SUCCESS; +} + int parse_cookie(char *line) { int ret = 0; char *samesite = NULL; diff --git a/src/sp_config_keywords.h b/src/sp_config_keywords.h index 8286997..d7ea36a 100644 --- a/src/sp_config_keywords.h +++ b/src/sp_config_keywords.h @@ -12,5 +12,6 @@ int parse_unserialize(char *line); int parse_readonly_exec(char *line); int parse_disabled_functions(char *line); int parse_upload_validation(char *line); +int parse_eval_filter(char *line); #endif // __SP_CONFIG_KEYWORDS_H diff --git a/src/sp_disabled_functions.c b/src/sp_disabled_functions.c index 829f938..45b8954 100644 --- a/src/sp_disabled_functions.c +++ b/src/sp_disabled_functions.c @@ -431,8 +431,8 @@ ZEND_FUNCTION(check_disabled_function) { } orig_handler = zend_hash_str_find_ptr( - SNUFFLEUPAGUS_G(disabled_functions_hook), current_function_name, - strlen(current_function_name)); + SNUFFLEUPAGUS_G(disabled_functions_hook), current_function_name, + strlen(current_function_name)); orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); if (true == should_drop_on_ret(return_value, execute_data)) { sp_terminate(); @@ -460,6 +460,31 @@ static int hook_functions(const sp_list_node* config) { return SUCCESS; } +ZEND_FUNCTION(eval_filter_callback) { + void (*orig_handler)(INTERNAL_FUNCTION_PARAMETERS); + const char* current_function_name = get_active_function_name(TSRMLS_C); + + if (SNUFFLEUPAGUS_G(in_eval) == true) { + const char* filename = get_eval_filename(zend_get_executed_filename()); + const int line_number = zend_get_executed_lineno(TSRMLS_C); + if (1 == SNUFFLEUPAGUS_G(config).config_eval->simulation) { + sp_log_msg("eval", SP_LOG_SIMULATION, + "A call to %s was tried in eval, in %s:%d, dropping it.", + current_function_name, filename, line_number); + } else { + sp_log_msg("eval", SP_LOG_DROP, + "A call to %s was tried in eval, in %s:%d, dropping it.", + current_function_name, filename, line_number); + sp_terminate(); + } + } + + orig_handler = zend_hash_str_find_ptr( + SNUFFLEUPAGUS_G(sp_eval_filter_functions_hook), current_function_name, + strlen(current_function_name)); + orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + int hook_disabled_functions(void) { TSRMLS_FETCH(); @@ -470,5 +495,16 @@ int hook_disabled_functions(void) { ret |= hook_functions(SNUFFLEUPAGUS_G(config) .config_disabled_functions_ret->disabled_functions); + if (NULL != SNUFFLEUPAGUS_G(config).config_eval->blacklist->data) { + sp_list_node* it = SNUFFLEUPAGUS_G(config).config_eval->blacklist; + + while (it) { + hook_function((char*)it->data, + SNUFFLEUPAGUS_G(sp_eval_filter_functions_hook), + PHP_FN(eval_filter_callback), false); + it = it->next; + } + } + return ret; } diff --git a/src/sp_execute.c b/src/sp_execute.c index 7dd0798..a50bfd5 100644 --- a/src/sp_execute.c +++ b/src/sp_execute.c @@ -68,6 +68,7 @@ static void sp_execute_ex(zend_execute_data *execute_data) { } if (execute_data->func->op_array.type == ZEND_EVAL_CODE) { + SNUFFLEUPAGUS_G(in_eval) = true; sp_list_node *config = SNUFFLEUPAGUS_G(config).config_disabled_constructs->construct_eval; char *filename = get_eval_filename((char *)zend_get_executed_filename()); @@ -86,6 +87,8 @@ static void sp_execute_ex(zend_execute_data *execute_data) { if (true == should_drop_on_ret(execute_data->return_value, execute_data)) { sp_terminate(); } + + SNUFFLEUPAGUS_G(in_eval) = false; } static int sp_stream_open(const char *filename, zend_file_handle *handle) { diff --git a/src/sp_harden_rand.c b/src/sp_harden_rand.c index 3727bef..cb57591 100644 --- a/src/sp_harden_rand.c +++ b/src/sp_harden_rand.c @@ -56,9 +56,8 @@ PHP_FUNCTION(sp_rand) { /* call the original `rand` function, * since we might no be the only ones to hook it*/ - orig_handler = - zend_hash_str_find_ptr(SNUFFLEUPAGUS_G(sp_internal_functions_hook), - "rand", strlen("rand")); + orig_handler = zend_hash_str_find_ptr( + SNUFFLEUPAGUS_G(sp_internal_functions_hook), "rand", strlen("rand")); orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); random_int_wrapper(INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -70,8 +69,8 @@ PHP_FUNCTION(sp_mt_rand) { /* call the original `mt_rand` function, * since we might no be the only ones to hook it*/ orig_handler = - zend_hash_str_find_ptr(SNUFFLEUPAGUS_G(sp_internal_functions_hook), - "mt_rand", strlen("mt_rand")); + zend_hash_str_find_ptr(SNUFFLEUPAGUS_G(sp_internal_functions_hook), + "mt_rand", strlen("mt_rand")); orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); random_int_wrapper(INTERNAL_FUNCTION_PARAM_PASSTHRU); diff --git a/src/sp_unserialize.c b/src/sp_unserialize.c index 476bebc..f78b046 100644 --- a/src/sp_unserialize.c +++ b/src/sp_unserialize.c @@ -7,7 +7,7 @@ PHP_FUNCTION(sp_serialize) { /* Call the original `serialize` function. */ orig_handler = zend_hash_str_find_ptr( - SNUFFLEUPAGUS_G(sp_internal_functions_hook), "serialize", 9); + SNUFFLEUPAGUS_G(sp_internal_functions_hook), "serialize", 9); orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); /* Compute the HMAC of the textual representation of the serialized data*/ diff --git a/src/tests/config/eval_backlist.ini b/src/tests/config/eval_backlist.ini new file mode 100644 index 0000000..1e34b5b --- /dev/null +++ b/src/tests/config/eval_backlist.ini @@ -0,0 +1 @@ +sp.eval_filter.blacklist("strlen"); diff --git a/src/tests/config/eval_backlist_list.ini b/src/tests/config/eval_backlist_list.ini new file mode 100644 index 0000000..da5650d --- /dev/null +++ b/src/tests/config/eval_backlist_list.ini @@ -0,0 +1 @@ +sp.eval_filter.blacklist("strcmp,strlen"); diff --git a/src/tests/config/eval_backlist_simulation.ini b/src/tests/config/eval_backlist_simulation.ini new file mode 100644 index 0000000..fafebd5 --- /dev/null +++ b/src/tests/config/eval_backlist_simulation.ini @@ -0,0 +1 @@ +sp.eval_filter.blacklist("strlen").simulation(); diff --git a/src/tests/eval_backlist.phpt b/src/tests/eval_backlist.phpt new file mode 100644 index 0000000..20b2c92 --- /dev/null +++ b/src/tests/eval_backlist.phpt @@ -0,0 +1,16 @@ +--TEST-- +Eval blacklist +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/eval_backlist.ini +--FILE-- + +--EXPECTF-- +Outside of eval: 14 +[snuffleupagus][0.0.0.0][eval][drop] A call to strlen was tried in eval, in%atests/eval_backlist.php:1, dropping it. diff --git a/src/tests/eval_backlist_list.phpt b/src/tests/eval_backlist_list.phpt new file mode 100644 index 0000000..b1c7bfd --- /dev/null +++ b/src/tests/eval_backlist_list.phpt @@ -0,0 +1,16 @@ +--TEST-- +Eval blacklist - with a list of functions +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/eval_backlist_list.ini +--FILE-- + +--EXPECTF-- +Outside of eval: 14 +[snuffleupagus][0.0.0.0][eval][drop] A call to strlen was tried in eval, in %a/tests/eval_backlist_list.php:1, dropping it. diff --git a/src/tests/eval_backlist_simulation.phpt b/src/tests/eval_backlist_simulation.phpt new file mode 100644 index 0000000..ddeae60 --- /dev/null +++ b/src/tests/eval_backlist_simulation.phpt @@ -0,0 +1,17 @@ +--TEST-- +Eval blacklist +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/eval_backlist_simulation.ini +--FILE-- + +--EXPECTF-- +Outside of eval: 14 +[snuffleupagus][0.0.0.0][eval][simulation] A call to strlen was tried in eval, in %a/tests/eval_backlist_simulation.php:1, dropping it. +After eval: 4 diff --git a/src/tests/nested_eval_blacklist.phpt b/src/tests/nested_eval_blacklist.phpt new file mode 100644 index 0000000..b12bf93 --- /dev/null +++ b/src/tests/nested_eval_blacklist.phpt @@ -0,0 +1,28 @@ +--TEST-- +Eval blacklist - nested eval +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/eval_backlist.ini +--FILE-- + +--EXPECTF-- +Outside of eval: 14 +Inception lvl 1... +Inception lvl 2... +Inception lvl 3... +[snuffleupagus][0.0.0.0][eval][drop] A call to strlen was tried in eval, in %a/tests/nested_eval_blacklist.php(5) : eval()'d code(4) : eval()'d code:3, dropping it. -- cgit v1.3