1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
#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) : "<invalid>");
}
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) : "<invalid>";
}
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();
}
|