From 1be112f371f860feab290cb333792c52e4e23c7c Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Tue, 19 Apr 2022 12:43:18 +0200 Subject: allow file:// prefix in include() wich readonly_exec mode --- src/sp_execute.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/sp_execute.c b/src/sp_execute.c index f1ed8d0..9cf44e1 100644 --- a/src/sp_execute.c +++ b/src/sp_execute.c @@ -17,9 +17,9 @@ ZEND_COLD static inline void terminate_if_writable(const char *filename) { sp_log_request(config_ro_exec->dump, config_ro_exec->textual_representation); } if (true == config_ro_exec->simulation) { - sp_log_simulation("readonly_exec", "Attempted execution of a writable file (%s).", filename); + sp_log_simulation("readonly_exec", "Attempted execution of a writable file (%s)", filename); } else { - sp_log_drop("readonly_exec", "Attempted execution of a writable file (%s).", filename); + sp_log_drop("readonly_exec", "Attempted execution of a writable file (%s)", filename); } } else { if (EACCES != errno) { @@ -224,13 +224,18 @@ static inline void sp_stream_open_checks(zend_string *zend_filename, zend_file_h return; } - // zend_string *zend_filename = zend_string_init(filename, strlen(filename), 0); const HashTable *disabled_functions_hooked = SPCFG(disabled_functions_hooked); switch (data->opline->opcode) { case ZEND_INCLUDE_OR_EVAL: if (SPCFG(readonly_exec).enable) { - terminate_if_writable(ZSTR_VAL(zend_filename)); + char *fn = ZSTR_VAL(zend_filename); + if (ZSTR_LEN(zend_filename) >= strlen("file://") && memcmp(fn, "file://", strlen("file://")) == 0) { + fn += strlen("file://"); + } else if (!php_memnstr(ZSTR_VAL(zend_filename), "://", strlen("://"), ZSTR_VAL(zend_filename) + ZSTR_LEN(zend_filename))) { + // ignore stream wrappers other than file:// for now + terminate_if_writable(fn); + } } switch (data->opline->extended_value) { case ZEND_INCLUDE: -- cgit v1.3 From bb59f4396a7dd51a2490a12fd2714c68e19f7b66 Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Tue, 19 Apr 2022 12:44:35 +0200 Subject: fixed test case due to output change --- src/tests/deny_writable/deny_writable_execution.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/tests/deny_writable/deny_writable_execution.phpt b/src/tests/deny_writable/deny_writable_execution.phpt index e65dc32..a629479 100644 --- a/src/tests/deny_writable/deny_writable_execution.phpt +++ b/src/tests/deny_writable/deny_writable_execution.phpt @@ -40,4 +40,4 @@ unlink("$dir/non_writable_file.txt"); unlink("$dir/writable_file.txt"); ?> --EXPECTF-- -Fatal error: [snuffleupagus][0.0.0.0][readonly_exec][drop] Attempted execution of a writable file (%a/deny_writable_execution.php). in %a/deny_writable_execution.php on line 2 +Fatal error: [snuffleupagus][0.0.0.0][readonly_exec][drop] Attempted execution of a writable file (%a/deny_writable_execution.php) in %a/deny_writable_execution.php on line 2 -- cgit v1.3 From 490dc4e2a43a5f8256f0b00d5efd3c2a84487905 Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Tue, 19 Apr 2022 19:01:52 +0200 Subject: extended checks for readonly_exec, enabled by default introduced config options: readonly_exec.extended_checks() or xchecks() readonly_exec.no_extended_checks() or noxchecks() --- src/snuffleupagus.c | 2 ++ src/sp_config.h | 1 + src/sp_config_keywords.c | 7 ++++- src/sp_execute.c | 77 ++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 73 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index ebb7f9c..c723199 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c @@ -348,6 +348,7 @@ static void dump_config() { add_assoc_bool(&arr, "readonly_exec.enable", SPCFG(readonly_exec).enable); add_assoc_bool(&arr, "readonly_exec.sim", SPCFG(readonly_exec).simulation); ADD_ASSOC_ZSTR(&arr, SP_TOKEN_READONLY_EXEC "." SP_TOKEN_DUMP, SPCFG(readonly_exec).dump); + add_assoc_bool(&arr, "readonly_exec.extended_checks", SPCFG(readonly_exec).extended_checks); add_assoc_bool(&arr, "global_strict.enable", SPCFG(global_strict).enable); @@ -499,6 +500,7 @@ static PHP_INI_MH(OnUpdateConfiguration) { // set some defaults SPCFG(show_old_php_warning) = true; + SPCFG(readonly_exec).extended_checks = true; char *str = new_value->val; diff --git a/src/sp_config.h b/src/sp_config.h index 6d48240..e7d4e4d 100644 --- a/src/sp_config.h +++ b/src/sp_config.h @@ -35,6 +35,7 @@ typedef struct { typedef struct { bool enable; bool simulation; + bool extended_checks; zend_string *dump; zend_string *textual_representation; } sp_config_readonly_exec; diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index f7be731..e0e5166 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c @@ -96,7 +96,7 @@ SP_PARSE_FN(parse_unserialize) { } SP_PARSE_FN(parse_readonly_exec) { - bool enable = false, disable = false; + bool enable = false, disable = false, xchecks = false, no_xchecks = false; sp_config_readonly_exec *cfg = (sp_config_readonly_exec*)retval; sp_config_keyword config_keywords[] = { @@ -105,6 +105,10 @@ SP_PARSE_FN(parse_readonly_exec) { {parse_empty, SP_TOKEN_SIMULATION, &(cfg->simulation)}, {parse_empty, SP_TOKEN_SIM, &(cfg->simulation)}, {parse_str, SP_TOKEN_DUMP, &(cfg->dump)}, + {parse_empty, "extended_checks", &(xchecks)}, + {parse_empty, "xchecks", &(xchecks)}, + {parse_empty, "no_extended_checks", &(no_xchecks)}, + {parse_empty, "noxchecks", &(no_xchecks)}, {0, 0, 0}}; SP_PROCESS_CONFIG_KEYWORDS_ERR(); @@ -112,6 +116,7 @@ SP_PARSE_FN(parse_readonly_exec) { cfg->textual_representation = sp_get_textual_representation(parsed_rule); SP_SET_ENABLE_DISABLE(enable, disable, cfg->enable); + if (xchecks) { cfg->extended_checks = true; } else if (no_xchecks) { cfg->extended_checks = false; } return SP_PARSER_STOP; } diff --git a/src/sp_execute.c b/src/sp_execute.c index 9cf44e1..3a474a3 100644 --- a/src/sp_execute.c +++ b/src/sp_execute.c @@ -1,4 +1,5 @@ #include "php_snuffleupagus.h" +#include "ext/standard/php_string.h" static void (*orig_execute_ex)(zend_execute_data *execute_data) = NULL; static void (*orig_zend_execute_internal)(zend_execute_data *execute_data, @@ -12,22 +13,72 @@ static zend_result (*orig_zend_stream_open)(zend_file_handle *handle) = NULL; // FIXME handle symlink ZEND_COLD static inline void terminate_if_writable(const char *filename) { const sp_config_readonly_exec *config_ro_exec = &(SPCFG(readonly_exec)); + char *errmsg = "unknown access problem"; + + // check write access if (0 == access(filename, W_OK)) { - if (config_ro_exec->dump) { - sp_log_request(config_ro_exec->dump, config_ro_exec->textual_representation); - } - if (true == config_ro_exec->simulation) { - sp_log_simulation("readonly_exec", "Attempted execution of a writable file (%s)", filename); - } else { - sp_log_drop("readonly_exec", "Attempted execution of a writable file (%s)", filename); - } + errmsg = "Attempted execution of a writable file"; + goto violation; + } + if (errno != EACCES) { + goto err; + } + + // other checks are 'extended checks' that can be enabled/disabled via config + if (!config_ro_exec->extended_checks) { + return; + } + + // check effective uid + struct stat buf; + if (0 != stat(filename, &buf)) { + goto err; + } + if (buf.st_uid == geteuid()) { + errmsg = "Attempted execution of file owned by process"; + goto violation; + } + + // check write access on directory + char *dirname = estrndup(filename, strlen(filename)); + php_dirname(dirname, strlen(dirname)); + if (0 == access(dirname, W_OK)) { + errmsg = "Attempted execution of file in writable directory"; + efree(dirname); + goto violation; + } + if (errno != EACCES) { + efree(dirname); + goto err; + } + + // check effecite uid of directory + if (0 != stat(dirname, &buf)) { + efree(dirname); + goto err; + } + efree(dirname); + if (buf.st_uid == geteuid()) { + errmsg = "Attempted execution of file in directory owned by process"; + goto violation; + } + + // we would actually need to check all parent directories as well, but that task is left for other tools + return; + +violation: + if (config_ro_exec->dump) { + sp_log_request(config_ro_exec->dump, config_ro_exec->textual_representation); + } + if (config_ro_exec->simulation) { + sp_log_simulation("readonly_exec", "%s (%s)", errmsg, filename); } else { - if (EACCES != errno) { - // LCOV_EXCL_START - sp_log_err("Writable execution", "Error while accessing %s: %s", filename, strerror(errno)); - // LCOV_EXCL_STOP - } + sp_log_drop("readonly_exec", "%s (%s)", errmsg, filename); } + return; + +err: + sp_log_err("readonly_exec", "Error while accessing %s: %s", filename, strerror(errno)); } inline static void is_builtin_matching( -- cgit v1.3 From 5cd62972be096e2157c791cf7dcf8975e3c20c45 Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Sun, 17 Jul 2022 14:05:20 +0200 Subject: added full relro protection if the linker supports -z --- src/config.m4 | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/config.m4 b/src/config.m4 index 1958979..619dcbd 100644 --- a/src/config.m4 +++ b/src/config.m4 @@ -31,6 +31,8 @@ CFLAGS="$CFLAGS -fstack-protector-strong" LDFLAGS="$LDFLAGS `pcre2-config --libs8`" +AX_CHECK_COMPILE_FLAG([-Wl,-z,relro,-z,now], [LDFLAGS="$LDFLAGS -Wl,-z,relro,-z,now"], {}, [-Werror]) + if test "$PHP_DEBUG" = "yes"; then AC_DEFINE(SP_DEBUG, 1, [Enable SP debug messages]) CFLAGS="$CFLAGS -g -ggdb -O0" -- cgit v1.3 From 04a66f65b58d57fc419f2f22b3e480b1565cd229 Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Mon, 18 Jul 2022 17:07:05 +0200 Subject: fixed crash when exporting function list (rare edge case problem) --- src/snuffleupagus.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index c723199..af4d8e9 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c @@ -273,7 +273,7 @@ static void add_df_to_arr(zval *arr, sp_disabled_function *df) { if (df->functions_list && df->functions_list->data) { zval arr_fl; array_init(&arr_fl); - for (sp_list_node *p = df->functions_list; p; p = p->next) { add_next_index_str(&arr_fl, p->data); } + for (sp_list_node *p = df->functions_list; p; p = p->next) { add_next_index_string(&arr_fl, (char*)p->data); } add_assoc_zval(&arr_df, "function_list", &arr_fl); } else { add_assoc_null(&arr_df, "function_list"); -- cgit v1.3 From e37ee22908fccfbf28314f59e52e89e50c533bb2 Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Mon, 18 Jul 2022 17:08:22 +0200 Subject: added config error for ini rules with identical key --- src/sp_config_keywords.c | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index e0e5166..2afefb5 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c @@ -494,6 +494,11 @@ SP_PARSE_FN(parse_ini_entry) { goto err; } + if (zend_hash_find_ptr(SPCFG(ini).entries, entry->key)) { + sp_log_err("config", "duplicate INI key '%s' on line %zu", ZSTR_VAL(entry->key), parsed_rule->lineno); + goto err; + } + if (ro && rw) { sp_log_err("config", "rule cannot be both read-write and read-only on line %zu", parsed_rule->lineno); goto err; -- cgit v1.3 From 714cae56b88d88c888b5edeeddb40f55d5409d07 Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Wed, 20 Jul 2022 10:43:06 +0200 Subject: add disabled functions return type to config export --- src/snuffleupagus.c | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index af4d8e9..74af0ed 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c @@ -291,6 +291,7 @@ static void add_df_to_arr(zval *arr, sp_disabled_function *df) { add_assoc_long(&arr_df, SP_TOKEN_LINE_NUMBER, df->line); ADD_ASSOC_ZSTR(&arr_df, SP_TOKEN_RET, df->ret); ADD_ASSOC_REGEXP(&arr_df, SP_TOKEN_RET_REGEXP, df->r_ret); + add_assoc_long(&arr_df, SP_TOKEN_RET_TYPE, df->ret_type); ADD_ASSOC_ZSTR(&arr_df, SP_TOKEN_VALUE, df->value); ADD_ASSOC_REGEXP(&arr_df, SP_TOKEN_VALUE_REGEXP, df->r_value); ADD_ASSOC_ZSTR(&arr_df, SP_TOKEN_KEY, df->key); -- cgit v1.3 From 49e074c5914d4dd90d9c03394e5aa161c74f28ca Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Wed, 20 Jul 2022 10:45:48 +0200 Subject: fixed mem leak while parsing cookie config --- src/sp_config_keywords.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index 2afefb5..c8922ae 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c @@ -228,6 +228,8 @@ SP_PARSE_FN(parse_cookie) { ZSTR_VAL(samesite), parsed_rule->lineno); goto err; } + zend_string_release(samesite); + samesite = NULL; } SPCFG(cookie).cookies = sp_list_insert(SPCFG(cookie).cookies, cookie); -- cgit v1.3 From 72109c9bf016145364b19162a5ff998fc5858a9c Mon Sep 17 00:00:00 2001 From: Ben Fuhrmannek Date: Wed, 20 Jul 2022 10:46:16 +0200 Subject: fixed cookie config parsing with same cookie name (update instead of ignore) --- src/sp_config_keywords.c | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index c8922ae..c3ef24e 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c @@ -232,7 +232,28 @@ SP_PARSE_FN(parse_cookie) { samesite = NULL; } - SPCFG(cookie).cookies = sp_list_insert(SPCFG(cookie).cookies, cookie); + // find other cookie entry with identical name or name_r + sp_cookie *entry = NULL; + sp_list_node *pList = NULL; + for (pList = SPCFG(cookie).cookies; pList; pList = pList->next) { + entry = pList->data; + if (!entry) { continue; } + if ((entry->name && cookie->name && sp_zend_string_equals(entry->name, cookie->name)) || + (entry->name_r && cookie->name_r && sp_zend_string_equals(entry->name_r->pattern, cookie->name_r->pattern))) { + break; + } + } + if (pList && entry) { + // override cookie settings if set + if (cookie->samesite) { entry->samesite = cookie->samesite; } + if (cookie->encrypt) { entry->encrypt = cookie->encrypt; } + if (cookie->simulation) { entry->simulation = cookie->simulation; } + sp_free_cookie(cookie); + pefree(cookie, 1); + cookie = NULL; + } else { + SPCFG(cookie).cookies = sp_list_insert(SPCFG(cookie).cookies, cookie); + } return SP_PARSER_STOP; -- cgit v1.3