diff options
| author | Ben Fuhrmannek | 2021-08-07 15:56:57 +0200 |
|---|---|---|
| committer | Ben Fuhrmannek | 2021-08-07 15:56:57 +0200 |
| commit | bd8b5bb241ca359b65c1a3717c9905d034b9703b (patch) | |
| tree | 152cf1c0c91433ef7599097b4e9d12241c5dc628 | |
| parent | e8bb162220ac17cb9b8cc229666356e88f081887 (diff) | |
more ini protection features
Diffstat (limited to '')
| -rw-r--r-- | config/ini_protection.php8.rules | 20 | ||||
| -rw-r--r-- | src/sp_config.h | 8 | ||||
| -rw-r--r-- | src/sp_config_keywords.c | 21 | ||||
| -rw-r--r-- | src/sp_ini.c | 52 |
4 files changed, 76 insertions, 25 deletions
diff --git a/config/ini_protection.php8.rules b/config/ini_protection.php8.rules index 081048f..b4ddb30 100644 --- a/config/ini_protection.php8.rules +++ b/config/ini_protection.php8.rules | |||
| @@ -1,6 +1,20 @@ | |||
| 1 | ## INI protection - prevent unwanted runtime ini changes made by ini_set() or other functions or by .htaccess | 1 | ## INI protection - prevent unwanted runtime ini changes made by ini_set() or other functions or by .htaccess |
| 2 | sp.ini_protection.enable(); | 2 | sp.ini_protection.enable(); |
| 3 | 3 | ||
| 4 | ## simulation mode: only log violations | ||
| 5 | #sp.ini_protection.simulation(); | ||
| 6 | |||
| 7 | ## drop policy: drop request on rule violation | ||
| 8 | #sp.ini_protection.policy_drop(); | ||
| 9 | |||
| 10 | ## do not log violations. | ||
| 11 | ## this setting has no effect in simulation or drop mode | ||
| 12 | #sp.ini_protection.policy_silent_fail(); | ||
| 13 | |||
| 14 | ## do not log read-only violations | ||
| 15 | ## this setting has no effect in simulation or drop mode | ||
| 16 | sp.ini_protection.policy_silent_ro(); | ||
| 17 | |||
| 4 | ## access policy can be one of | 18 | ## access policy can be one of |
| 5 | ## .policy_readonly(): All entries are read-only by default. | 19 | ## .policy_readonly(): All entries are read-only by default. |
| 6 | ## Individual entries can be set read-write using .readwrite() or .rw() | 20 | ## Individual entries can be set read-write using .readwrite() or .rw() |
| @@ -10,13 +24,17 @@ sp.ini_protection.enable(); | |||
| 10 | 24 | ||
| 11 | ## sp.ini entries can have the following attributes | 25 | ## sp.ini entries can have the following attributes |
| 12 | ## .key("..."): mandatory ini name. | 26 | ## .key("..."): mandatory ini name. |
| 13 | ## .set("..."): set the value. This overrides php.ini. | 27 | ## .set("..."): set the initial value. This overrides php.ini. |
| 28 | ## checks are not performed for this initial value. | ||
| 14 | ## .min("...") / .max("..."): value must be an integer between .min and .max. | 29 | ## .min("...") / .max("..."): value must be an integer between .min and .max. |
| 15 | ## shorthand notation (e.g. 1k = 1024) is allowed | 30 | ## shorthand notation (e.g. 1k = 1024) is allowed |
| 16 | ## .regexp("..."): value must match the regular expression | 31 | ## .regexp("..."): value must match the regular expression |
| 32 | ## .allow_null(): allow setting a NULL-value | ||
| 17 | ## .msg("..."): message is shown in logs on rule violation instead of default message | 33 | ## .msg("..."): message is shown in logs on rule violation instead of default message |
| 18 | ## .readonly() / .ro() / .readwrite() / .rw(): set entry to read-only or read-write respectively | 34 | ## .readonly() / .ro() / .readwrite() / .rw(): set entry to read-only or read-write respectively |
| 19 | ## If no access keyword is provided, the entry inherits the default policy set by sp.ini_protection.policy_*-rules. | 35 | ## If no access keyword is provided, the entry inherits the default policy set by sp.ini_protection.policy_*-rules. |
| 36 | ## .drop(): drop request on rule violation for this entry | ||
| 37 | ## .simulation(): only log rule violation for this entry | ||
| 20 | 38 | ||
| 21 | ## FOR PRODUCTION SYSTEMS: disable error messages and version numbers | 39 | ## FOR PRODUCTION SYSTEMS: disable error messages and version numbers |
| 22 | sp.ini.key("display_errors").set("0").ro(); | 40 | sp.ini.key("display_errors").set("0").ro(); |
diff --git a/src/sp_config.h b/src/sp_config.h index bd2530a..0ba2e7f 100644 --- a/src/sp_config.h +++ b/src/sp_config.h | |||
| @@ -170,17 +170,21 @@ typedef struct { | |||
| 170 | zend_string *min; | 170 | zend_string *min; |
| 171 | zend_string *max; | 171 | zend_string *max; |
| 172 | sp_pcre *regexp; | 172 | sp_pcre *regexp; |
| 173 | bool simulation; | ||
| 174 | zend_string *msg; | 173 | zend_string *msg; |
| 175 | zend_string *set; | 174 | zend_string *set; |
| 175 | bool allow_null; | ||
| 176 | bool simulation; | ||
| 177 | bool drop; | ||
| 176 | PHP_INI_MH((*orig_onmodify)); | 178 | PHP_INI_MH((*orig_onmodify)); |
| 177 | } sp_ini_entry; | 179 | } sp_ini_entry; |
| 178 | 180 | ||
| 179 | typedef struct { | 181 | typedef struct { |
| 180 | bool enable; | 182 | bool enable; |
| 181 | bool simulation; | 183 | bool simulation; |
| 182 | // sp_ini_permission access_policy; | ||
| 183 | bool policy_readonly; | 184 | bool policy_readonly; |
| 185 | bool policy_silent_ro; | ||
| 186 | bool policy_silent_fail; | ||
| 187 | bool policy_drop; | ||
| 184 | HashTable *entries; // ht of sp_ini_entry | 188 | HashTable *entries; // ht of sp_ini_entry |
| 185 | } sp_config_ini; | 189 | } sp_config_ini; |
| 186 | 190 | ||
diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index e6eb05e..c547f10 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c | |||
| @@ -566,14 +566,19 @@ int parse_upload_validation(char *line) { | |||
| 566 | int parse_ini_protection(char *line) { | 566 | int parse_ini_protection(char *line) { |
| 567 | bool disable = false, enable = false; | 567 | bool disable = false, enable = false; |
| 568 | bool rw = false, ro = false; // rw is ignored, but declaring .policy_rw is valid for readability | 568 | bool rw = false, ro = false; // rw is ignored, but declaring .policy_rw is valid for readability |
| 569 | sp_config_ini *cfg = SNUFFLEUPAGUS_G(config).config_ini; | ||
| 569 | sp_config_functions sp_config_ini_protection[] = { | 570 | sp_config_functions sp_config_ini_protection[] = { |
| 570 | {parse_empty, SP_TOKEN_ENABLE, &(enable)}, | 571 | {parse_empty, SP_TOKEN_ENABLE, &(enable)}, |
| 571 | {parse_empty, SP_TOKEN_DISABLE, &(disable)}, | 572 | {parse_empty, SP_TOKEN_DISABLE, &(disable)}, |
| 572 | {parse_empty, SP_TOKEN_SIMULATION, &(SNUFFLEUPAGUS_G(config).config_ini->simulation)}, | 573 | {parse_empty, SP_TOKEN_SIMULATION, &cfg->simulation}, |
| 573 | {parse_empty, ".policy_readonly(", &ro}, | 574 | {parse_empty, ".policy_readonly(", &ro}, |
| 574 | {parse_empty, ".policy_ro(", &ro}, | 575 | {parse_empty, ".policy_ro(", &ro}, |
| 575 | {parse_empty, ".policy_readwrite(", &rw}, | 576 | {parse_empty, ".policy_readwrite(", &rw}, |
| 576 | {parse_empty, ".policy_rw(", &rw}, | 577 | {parse_empty, ".policy_rw(", &rw}, |
| 578 | {parse_empty, ".policy_silent_ro(", &cfg->policy_silent_ro}, | ||
| 579 | {parse_empty, ".policy_silent_fail(", &cfg->policy_silent_fail}, | ||
| 580 | {parse_empty, ".policy_no_log(", &cfg->policy_silent_fail}, | ||
| 581 | {parse_empty, ".policy_drop(", &cfg->policy_drop}, | ||
| 577 | {0, 0, 0}}; | 582 | {0, 0, 0}}; |
| 578 | 583 | ||
| 579 | int ret = parse_keywords(sp_config_ini_protection, line); | 584 | int ret = parse_keywords(sp_config_ini_protection, line); |
| @@ -585,15 +590,19 @@ int parse_ini_protection(char *line) { | |||
| 585 | return -1; | 590 | return -1; |
| 586 | } | 591 | } |
| 587 | if (enable || disable) { | 592 | if (enable || disable) { |
| 588 | SNUFFLEUPAGUS_G(config).config_ini->enable = (enable || !disable); | 593 | cfg->enable = (enable || !disable); |
| 589 | } | 594 | } |
| 590 | 595 | ||
| 591 | if (ro && rw) { | 596 | if (ro && rw) { |
| 592 | sp_log_err("config", "rule cannot be both read-write and read-only on line %zu", sp_line_no); | 597 | sp_log_err("config", "rule cannot be both read-write and read-only on line %zu", sp_line_no); |
| 593 | return -1; | 598 | return -1; |
| 594 | } | 599 | } |
| 595 | SNUFFLEUPAGUS_G(config).config_ini->policy_readonly = ro; | 600 | cfg->policy_readonly = ro; |
| 596 | 601 | ||
| 602 | if (cfg->policy_silent_fail && cfg->policy_drop) { | ||
| 603 | sp_log_err("config", "policy cannot be drop and silent at the same time on line %zu", sp_line_no); | ||
| 604 | return -1; | ||
| 605 | } | ||
| 597 | return ret; | 606 | return ret; |
| 598 | } | 607 | } |
| 599 | 608 | ||
| @@ -611,8 +620,10 @@ int parse_ini_entry(char *line) { | |||
| 611 | {parse_regexp, ".regexp(", &entry->regexp}, | 620 | {parse_regexp, ".regexp(", &entry->regexp}, |
| 612 | {parse_empty, ".readonly(", &ro}, | 621 | {parse_empty, ".readonly(", &ro}, |
| 613 | {parse_empty, ".ro(", &ro}, | 622 | {parse_empty, ".ro(", &ro}, |
| 614 | {parse_empty, ".readwrite()", &rw}, | 623 | {parse_empty, ".readwrite(", &rw}, |
| 615 | {parse_empty, ".rw()", &rw}, | 624 | {parse_empty, ".rw(", &rw}, |
| 625 | {parse_empty, ".drop(", &entry->drop}, | ||
| 626 | {parse_empty, ".allow_null(", &entry->allow_null}, | ||
| 616 | {0, 0, 0}}; | 627 | {0, 0, 0}}; |
| 617 | 628 | ||
| 618 | int ret = parse_keywords(sp_config_ini_protection, line); | 629 | int ret = parse_keywords(sp_config_ini_protection, line); |
diff --git a/src/sp_ini.c b/src/sp_ini.c index 05d7d99..5777ca3 100644 --- a/src/sp_ini.c +++ b/src/sp_ini.c | |||
| @@ -3,6 +3,15 @@ | |||
| 3 | #define SP_INI_HAS_CHECKS_COND(entry) (entry->min || entry->max || entry->regexp) | 3 | #define SP_INI_HAS_CHECKS_COND(entry) (entry->min || entry->max || entry->regexp) |
| 4 | #define SP_INI_ACCESS_READONLY_COND(entry, cfg) (entry->access == SP_READONLY || (!entry->access && cfg->policy_readonly)) | 4 | #define SP_INI_ACCESS_READONLY_COND(entry, cfg) (entry->access == SP_READONLY || (!entry->access && cfg->policy_readonly)) |
| 5 | 5 | ||
| 6 | #define sp_log_auto2(feature, is_simulation, drop, ...) \ | ||
| 7 | sp_log_msgf(feature, ((is_simulation || !drop) ? SP_LOG_WARN : SP_LOG_ERROR), \ | ||
| 8 | (is_simulation ? SP_TYPE_SIMULATION : (drop ? SP_TYPE_DROP : SP_TYPE_LOG)), \ | ||
| 9 | __VA_ARGS__) | ||
| 10 | #define sp_log_ini_check_violation(...) if (simulation || cfg->policy_drop || (entry && entry->drop) || !cfg->policy_silent_fail) { \ | ||
| 11 | sp_log_auto2("ini_protection", simulation, (cfg->policy_drop || (entry && entry->drop)), __VA_ARGS__); \ | ||
| 12 | } | ||
| 13 | |||
| 14 | |||
| 6 | static bool /* success */ sp_ini_check(zend_string *varname, zend_string *new_value, sp_ini_entry **sp_entry_p) { | 15 | static bool /* success */ sp_ini_check(zend_string *varname, zend_string *new_value, sp_ini_entry **sp_entry_p) { |
| 7 | if (!varname || ZSTR_LEN(varname) == 0) { | 16 | if (!varname || ZSTR_LEN(varname) == 0) { |
| 8 | return false; | 17 | return false; |
| @@ -17,40 +26,49 @@ static bool /* success */ sp_ini_check(zend_string *varname, zend_string *new_va | |||
| 17 | 26 | ||
| 18 | if (!entry) { | 27 | if (!entry) { |
| 19 | if (cfg->policy_readonly) { | 28 | if (cfg->policy_readonly) { |
| 20 | sp_log_auto("ini_protection", simulation, "INI setting is read-only"); | 29 | if (!cfg->policy_silent_ro) { |
| 21 | if (simulation) { return true; } | 30 | sp_log_ini_check_violation("INI setting is read-only"); |
| 22 | return false; | 31 | } |
| 32 | return simulation; | ||
| 23 | } | 33 | } |
| 24 | return true; | 34 | return true; |
| 25 | } | 35 | } |
| 26 | 36 | ||
| 37 | // we have an entry. | ||
| 38 | |||
| 27 | if (SP_INI_ACCESS_READONLY_COND(entry, cfg)) { | 39 | if (SP_INI_ACCESS_READONLY_COND(entry, cfg)) { |
| 28 | sp_log_auto("ini_protection", simulation, "%s", (entry->msg ? ZSTR_VAL(entry->msg) : "INI setting is read-only")); | 40 | if (!cfg->policy_silent_ro) { |
| 29 | if (simulation) { return true; } | 41 | sp_log_ini_check_violation("%s", (entry->msg ? ZSTR_VAL(entry->msg) : "INI setting is read-only")); |
| 30 | return false; | 42 | } |
| 43 | return simulation; | ||
| 31 | } | 44 | } |
| 32 | 45 | ||
| 33 | if (!new_value && SP_INI_HAS_CHECKS_COND(entry)) { | 46 | if (!new_value || ZSTR_LEN(new_value) == 0) { |
| 34 | sp_log_auto("ini_protection", simulation, "new INI value must not be NULL"); | 47 | if (entry->allow_null) { |
| 35 | if (simulation) { return true; } | 48 | return true; // allow NULL value and skip other tests |
| 36 | return false; | 49 | } |
| 50 | if (SP_INI_HAS_CHECKS_COND(entry)) { | ||
| 51 | sp_log_ini_check_violation("new INI value must not be NULL or empty"); | ||
| 52 | return simulation; | ||
| 53 | } | ||
| 54 | return true; // no new_value, but no checks to perform | ||
| 37 | } | 55 | } |
| 38 | 56 | ||
| 57 | // we have a new_value. | ||
| 58 | |||
| 39 | if (entry->min || entry->max) { | 59 | if (entry->min || entry->max) { |
| 40 | zend_long lvalue = zend_atol(ZSTR_VAL(new_value), ZSTR_LEN(new_value)); | 60 | zend_long lvalue = zend_atol(ZSTR_VAL(new_value), ZSTR_LEN(new_value)); |
| 41 | if ((entry->min && zend_atol(ZSTR_VAL(entry->min), ZSTR_LEN(entry->min)) > lvalue) || | 61 | if ((entry->min && zend_atol(ZSTR_VAL(entry->min), ZSTR_LEN(entry->min)) > lvalue) || |
| 42 | (entry->max && zend_atol(ZSTR_VAL(entry->max), ZSTR_LEN(entry->max)) < lvalue)) { | 62 | (entry->max && zend_atol(ZSTR_VAL(entry->max), ZSTR_LEN(entry->max)) < lvalue)) { |
| 43 | sp_log_auto("ini_protection", simulation, "%s", (entry->msg ? ZSTR_VAL(entry->msg) : "INI value out of range")); | 63 | sp_log_ini_check_violation("%s", (entry->msg ? ZSTR_VAL(entry->msg) : "INI value out of range")); |
| 44 | if (simulation) { return true; } | 64 | return simulation; |
| 45 | return false; | ||
| 46 | } | 65 | } |
| 47 | } | 66 | } |
| 48 | 67 | ||
| 49 | if (entry->regexp) { | 68 | if (entry->regexp) { |
| 50 | if (!sp_is_regexp_matching_len(entry->regexp, ZSTR_VAL(new_value), ZSTR_LEN(new_value))) { | 69 | if (!sp_is_regexp_matching_len(entry->regexp, ZSTR_VAL(new_value), ZSTR_LEN(new_value))) { |
| 51 | sp_log_auto("ini_protection", simulation, "%s", (entry->msg ? ZSTR_VAL(entry->msg) : "INI value does not match regex")); | 70 | sp_log_ini_check_violation("%s", (entry->msg ? ZSTR_VAL(entry->msg) : "INI value does not match regex")); |
| 52 | if (simulation) { return true; } | 71 | return simulation; |
| 53 | return false; | ||
| 54 | } | 72 | } |
| 55 | } | 73 | } |
| 56 | 74 | ||
| @@ -83,7 +101,7 @@ void sp_hook_ini() { | |||
| 83 | 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)); | 101 | 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)); |
| 84 | continue; | 102 | continue; |
| 85 | } | 103 | } |
| 86 | if (SP_INI_ACCESS_READONLY_COND(sp_entry, cfg)) { | 104 | 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)) { |
| 87 | ini_entry->modifiable = ini_entry->orig_modifiable = 0; | 105 | ini_entry->modifiable = ini_entry->orig_modifiable = 0; |
| 88 | } | 106 | } |
| 89 | PHP_INI_MH((*orig_onmodify)) = ini_entry->on_modify; | 107 | PHP_INI_MH((*orig_onmodify)) = ini_entry->on_modify; |
