#include "php_snuffleupagus.h" #define SP_INI_HAS_CHECKS_COND(entry) (entry->min || entry->max || entry->regexp) #define SP_INI_ACCESS_READONLY_COND(entry, cfg) (entry->access == SP_READONLY || (!entry->access && cfg->policy_readonly)) #define sp_log_auto2(feature, is_simulation, drop, ...) \ sp_log_msgf(feature, ((is_simulation || !drop) ? SP_LOG_WARN : SP_LOG_ERROR), \ (is_simulation ? SP_TYPE_SIMULATION : (drop ? SP_TYPE_DROP : SP_TYPE_LOG)), \ __VA_ARGS__) #define sp_log_ini_check_violation(...) if (simulation || cfg->policy_drop || (entry && entry->drop) || !cfg->policy_silent_fail) { \ sp_log_auto2("ini_protection", simulation, (cfg->policy_drop || (entry && entry->drop)), __VA_ARGS__); \ } static const char* check_safe_key(const char* string) { for (const char* p = string; *p != '\0'; p++) { if (!isalnum((unsigned char)*p) && *p != '_' && *p != '.') { return NULL; } } return string; } static const char* check_safe_value(const char* string) { for (const char* p = string; *p != '\0'; p++) { if (*p == '\'' || *p == '"') { return NULL; } if (!isgraph((unsigned char)*p)) { return NULL; } } return string; } static bool /* success */ sp_ini_check(zend_string *const restrict varname, zend_string const *const restrict new_value, sp_ini_entry **sp_entry_p) { if (!varname || ZSTR_LEN(varname) == 0) { return false; } sp_config_ini const* const cfg = &(SPCFG(ini)); sp_ini_entry *entry = zend_hash_find_ptr(cfg->entries, varname); if (sp_entry_p) { *sp_entry_p = entry; } bool simulation = (cfg->simulation || (entry && entry->simulation)); if (!entry) { if (cfg->policy_readonly) { if (!cfg->policy_silent_ro) { sp_log_ini_check_violation("INI setting `%s` is read-only", check_safe_key(ZSTR_VAL(varname)) ? ZSTR_VAL(varname) : ""); } return simulation; } return true; } // we have an entry. if (SP_INI_ACCESS_READONLY_COND(entry, cfg)) { if (!cfg->policy_silent_ro) { if (entry->msg) { sp_log_ini_check_violation("%s", ZSTR_VAL(entry->msg)); } else { sp_log_ini_check_violation("INI setting `%s` is read-only", ZSTR_VAL(entry->key)); } } return simulation; } if (!new_value || ZSTR_LEN(new_value) == 0) { if (entry->allow_null) { return true; // allow NULL value and skip other tests } if (SP_INI_HAS_CHECKS_COND(entry)) { sp_log_ini_check_violation("new INI value for `%s` must not be NULL or empty", ZSTR_VAL(entry->key)); return simulation; } return true; // no new_value, but no checks to perform } // we have a new_value. if (entry->min || entry->max) { #if PHP_VERSION_ID >= 80200 zend_long lvalue = ZEND_STRTOL(ZSTR_VAL(new_value), NULL, 0); if ((entry->min && ZEND_STRTOL(ZSTR_VAL(entry->min), NULL, 0) > lvalue) || (entry->max && ZEND_STRTOL(ZSTR_VAL(entry->max), NULL, 0) < lvalue)) { #else zend_long lvalue = zend_atol(ZSTR_VAL(new_value), ZSTR_LEN(new_value)); if ((entry->min && zend_atol(ZSTR_VAL(entry->min), ZSTR_LEN(entry->min)) > lvalue) || (entry->max && zend_atol(ZSTR_VAL(entry->max), ZSTR_LEN(entry->max)) < lvalue)) { #endif if (entry->msg) { sp_log_ini_check_violation("%s", ZSTR_VAL(entry->msg)); } else { sp_log_ini_check_violation("INI value %lld for `%s` out of range", lvalue, ZSTR_VAL(entry->key)); } return simulation; } } if (entry->regexp) { if (!sp_is_regexp_matching_zstr(entry->regexp, new_value)) { if (entry->msg) { sp_log_ini_check_violation("%s", ZSTR_VAL(entry->msg)); } else { zend_string *base64_new_val = NULL; const char *safe_new_val = check_safe_value(ZSTR_VAL(new_value)); if (!safe_new_val) { base64_new_val = php_base64_encode((const unsigned char*)(ZSTR_VAL(new_value)), ZSTR_LEN(new_value)); safe_new_val = base64_new_val ? ZSTR_VAL(base64_new_val) : ""; } sp_log_ini_check_violation("INI value `%s`%s for `%s` does not match regex", safe_new_val, base64_new_val ? "(base64)" : "", ZSTR_VAL(entry->key)); if (base64_new_val) { zend_string_release(base64_new_val); } } return simulation; } } return true; } static PHP_INI_MH(sp_ini_onmodify) { sp_ini_entry *sp_entry = NULL; if (!sp_ini_check(entry->name, new_value, &sp_entry)) { return FAILURE; } if (sp_entry && sp_entry->orig_onmodify) { return sp_entry->orig_onmodify(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage); } return SUCCESS; } void sp_hook_ini() { sp_config_ini const* const cfg = &(SPCFG(ini)); sp_ini_entry *sp_entry; zend_ini_entry *ini_entry; ZEND_HASH_FOREACH_PTR(cfg->entries, sp_entry) if ((ini_entry = zend_hash_find_ptr(EG(ini_directives), sp_entry->key)) == NULL) { sp_log_warn("ini_protection", "Cannot hook INI var `%s`. Maybe a typo or the PHP extension providing this var is not loaded yet.", ZSTR_VAL(sp_entry->key)); continue; } if (SP_INI_ACCESS_READONLY_COND(sp_entry, cfg) && (cfg->policy_silent_ro || cfg->policy_silent_fail) && !sp_entry->drop && !(sp_entry->simulation || cfg->simulation)) { ini_entry->modifiable = ini_entry->orig_modifiable = 0; } PHP_INI_MH((*orig_onmodify)) = ini_entry->on_modify; if (SP_INI_HAS_CHECKS_COND(sp_entry) || SP_INI_ACCESS_READONLY_COND(sp_entry, cfg)) { // only hook on_modify if there is any check to perform sp_entry->orig_onmodify = ini_entry->on_modify; ini_entry->on_modify = sp_ini_onmodify; } if (sp_entry->set) { zend_string *duplicate = zend_string_copy(sp_entry->set); if (!orig_onmodify || orig_onmodify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, ZEND_INI_STAGE_STARTUP) == SUCCESS) { ini_entry->value = duplicate; } else { zend_string_release(duplicate); sp_log_warn("ini_protection", "Failed to set INI var `%s`.", ZSTR_VAL(sp_entry->key)); continue; } } ZEND_HASH_FOREACH_END(); } void sp_unhook_ini() { sp_ini_entry *sp_entry; ZEND_HASH_FOREACH_PTR(SPCFG(ini).entries, sp_entry) zend_ini_entry *ini_entry; if (!sp_entry->orig_onmodify) { // not hooked or no original onmodify continue; } if ((ini_entry = zend_hash_find_ptr(EG(ini_directives), sp_entry->key)) == NULL) { // unusual. ini entry is missing. continue; } ini_entry->on_modify = sp_entry->orig_onmodify; sp_entry->orig_onmodify = NULL; ZEND_HASH_FOREACH_END(); }