1 #include "php_snuffleupagus.h"
2
3 static void should_disable(zend_execute_data* execute_data,
4 const char* complete_function_path,
5 const zend_string* builtin_param,
6 const char* builtin_param_name,
7 const sp_list_node* config,
8 const zend_string* current_filename);
9
10 static void should_drop_on_ret(const zval* return_value,
11 const sp_list_node* config,
12 const char* complete_function_path,
13 zend_execute_data* execute_data);
14
get_complete_function_path(zend_execute_data const * const execute_data)15 char* get_complete_function_path(zend_execute_data const* const execute_data) {
16 if (zend_is_executing() && !EG(current_execute_data)->func) {
17 return NULL; // LCOV_EXCL_LINE
18 }
19 if (!(execute_data->func->common.function_name)) {
20 return NULL;
21 }
22
23 char const* class_name;
24 char const* const function_name =
25 ZSTR_VAL(execute_data->func->common.function_name);
26 char* complete_path_function = NULL;
27
28 class_name = get_active_class_name(NULL);
29 if (*class_name) {
30 const size_t len = strlen(class_name) + 2 + strlen(function_name) + 1;
31 complete_path_function = emalloc(len);
32 snprintf(complete_path_function, len, "%s::%s", class_name, function_name);
33 } else {
34 complete_path_function = estrdup(function_name);
35 }
36 return complete_path_function;
37 }
38
is_functions_list_matching(zend_execute_data * execute_data,sp_list_node * functions_list)39 static bool is_functions_list_matching(zend_execute_data* execute_data,
40 sp_list_node* functions_list) {
41 zend_execute_data *orig_execute_data, *current;
42 orig_execute_data = current = execute_data;
43 sp_list_node const* it = functions_list;
44
45 while (current) {
46 if (it == NULL) { // every function in the list matched, we've got a match!
47 EG(current_execute_data) = orig_execute_data;
48 return true;
49 }
50
51 EG(current_execute_data) = current;
52
53 char* const complete_path_function = get_complete_function_path(current);
54 if (!complete_path_function) {
55 break;
56 }
57 int match = strcmp(((char*)it->data), complete_path_function);
58 efree(complete_path_function);
59
60 if (0 == match) {
61 it = it->next;
62 }
63 current = current->prev_execute_data;
64 }
65
66 EG(current_execute_data) = orig_execute_data;
67 return false;
68 }
69
is_local_var_matching(zend_execute_data * execute_data,const sp_disabled_function * const config_node)70 static bool is_local_var_matching(
71 zend_execute_data* execute_data,
72 const sp_disabled_function* const config_node) {
73 zval* var_value = {0};
74
75 var_value = sp_get_var_value(execute_data, config_node->var, false);
76 if (var_value) {
77 if (Z_TYPE_P(var_value) == IS_ARRAY) {
78 if (config_node->key || config_node->r_key) {
79 if (sp_match_array_key(var_value, config_node->key,
80 config_node->r_key)) {
81 return true;
82 }
83 } else if (sp_match_array_value(var_value, config_node->value,
84 config_node->r_value)) {
85 return true;
86 }
87 } else {
88 zend_string const* const var_value_str =
89 sp_zval_to_zend_string(var_value);
90 bool match = sp_match_value(var_value_str, config_node->value,
91 config_node->r_value);
92
93 if (true == match) {
94 return true;
95 }
96 }
97 }
98 return false;
99 }
100
is_param_matching(zend_execute_data * execute_data,sp_disabled_function const * const config_node,const zend_string * builtin_param,const char ** arg_name,const char * builtin_param_name,const zend_string ** arg_value_str)101 static bool is_param_matching(zend_execute_data* execute_data,
102 sp_disabled_function const* const config_node,
103 const zend_string* builtin_param,
104 const char** arg_name,
105 const char* builtin_param_name,
106 const zend_string** arg_value_str) {
107 int nb_param = ZEND_CALL_NUM_ARGS(execute_data);
108 int i = 0;
109 zval* arg_value;
110
111 if (config_node->pos != -1) {
112 if (config_node->pos > nb_param - 1) {
113 char* complete_function_path = get_complete_function_path(execute_data);
114 sp_log_warn("config",
115 "It seems that you wrote a rule filtering on the "
116 "%d%s argument of the function '%s', but it takes only %d "
117 "arguments. "
118 "Matching on _all_ arguments instead.",
119 config_node->pos, GET_SUFFIX(config_node->pos),
120 complete_function_path, nb_param);
121 efree(complete_function_path);
122 } else {
123 i = config_node->pos;
124 nb_param = (config_node->pos) + 1;
125 }
126 }
127
128 if (builtin_param) {
129 /* We're matching on a language construct (here named "builtin"),
130 * and they can only take a single argument, but PHP considers them
131 * differently than functions arguments. */
132 *arg_name = builtin_param_name;
133 *arg_value_str = builtin_param;
134 return sp_match_value(builtin_param, config_node->value,
135 config_node->r_value);
136 } else if (config_node->r_param || config_node->pos != -1) {
137 // We're matching on a function (and not a language construct)
138 for (; i < nb_param; i++) {
139 if (ZEND_USER_CODE(execute_data->func->type)) { // yay consistency
140 *arg_name = ZSTR_VAL(execute_data->func->common.arg_info[i].name);
141 } else {
142 *arg_name = execute_data->func->internal_function.arg_info[i].name;
143 }
144 const bool pcre_matching =
145 config_node->r_param &&
146 (true == sp_is_regexp_matching(config_node->r_param, *arg_name));
147
148 /* This is the parameter name we're looking for. */
149 if (true == pcre_matching || config_node->pos != -1) {
150 arg_value = ZEND_CALL_ARG(execute_data, i + 1);
151
152 if (config_node->param_type) { // Are we matching on the `type`?
153 if (config_node->param_type == Z_TYPE_P(arg_value)) {
154 return true;
155 }
156 } else if (Z_TYPE_P(arg_value) == IS_ARRAY) {
157 *arg_value_str = sp_zval_to_zend_string(arg_value);
158 if (config_node->key || config_node->r_key) {
159 if (sp_match_array_key(arg_value, config_node->key,
160 config_node->r_key)) {
161 return true;
162 }
163 } else if (sp_match_array_value(arg_value, config_node->value,
164 config_node->r_value)) {
165 return true;
166 }
167 } else {
168 *arg_value_str = sp_zval_to_zend_string(arg_value);
169 if (sp_match_value(*arg_value_str, config_node->value,
170 config_node->r_value)) {
171 return true;
172 }
173 }
174 }
175 }
176 } else if (config_node->param) {
177 *arg_name = config_node->param->value;
178 arg_value = sp_get_var_value(execute_data, config_node->param, true);
179
180 if (arg_value) {
181 *arg_value_str = sp_zval_to_zend_string(arg_value);
182 if (config_node->param_type) { // Are we matching on the `type`?
183 if (config_node->param_type == Z_TYPE_P(arg_value)) {
184 return true;
185 }
186 } else if (Z_TYPE_P(arg_value) == IS_ARRAY) {
187 if (config_node->key || config_node->r_key) {
188 if (sp_match_array_key(arg_value, config_node->key,
189 config_node->r_key)) {
190 return true;
191 }
192 } else if (sp_match_array_value(arg_value, config_node->value,
193 config_node->r_value)) {
194 return true;
195 }
196 } else if (sp_match_value(*arg_value_str, config_node->value,
197 config_node->r_value)) {
198 return true;
199 }
200 }
201 }
202 return false;
203 }
204
is_file_matching(zend_execute_data * const execute_data,sp_disabled_function const * const config_node,zend_string const * const current_filename)205 static zend_execute_data* is_file_matching(
206 zend_execute_data* const execute_data,
207 sp_disabled_function const* const config_node,
208 zend_string const* const current_filename) {
209 #define ITERATE(ex) \
210 ex = ex->prev_execute_data; \
211 while (ex && (!ex->func || !ZEND_USER_CODE(ex->func->type))) \
212 ex = ex->prev_execute_data; \
213 if (!ex) return NULL;
214
215 zend_execute_data* ex = execute_data;
216 if (config_node->filename) {
217 if (sp_zend_string_equals(current_filename, config_node->filename)) {
218 return ex; // LCOV_EXCL_LINE
219 }
220 ITERATE(ex);
221 if (zend_string_equals(ex->func->op_array.filename,
222 config_node->filename)) {
223 return ex; // LCOV_EXCL_LINE
224 }
225 } else if (config_node->r_filename) {
226 if (sp_is_regexp_matching_zend(config_node->r_filename, current_filename)) {
227 return ex;
228 }
229 ITERATE(ex);
230 if (sp_is_regexp_matching_zend(config_node->r_filename,
231 ex->func->op_array.filename)) {
232 return ex;
233 }
234 }
235 return NULL;
236 #undef ITERATE
237 }
238
check_is_builtin_name(sp_disabled_function const * const config_node)239 inline static bool check_is_builtin_name(
240 sp_disabled_function const* const config_node) {
241 if (EXPECTED(config_node->function)) {
242 return (zend_string_equals_literal(config_node->function, "include") ||
243 zend_string_equals_literal(config_node->function, "include_once") ||
244 zend_string_equals_literal(config_node->function, "require") ||
245 zend_string_equals_literal(config_node->function, "require_once") ||
246 zend_string_equals_literal(config_node->function, "echo"));
247 }
248 return false; // LCOV_EXCL_LINE
249 }
250
should_disable_ht(zend_execute_data * execute_data,const char * function_name,const zend_string * builtin_param,const char * builtin_param_name,const sp_list_node * config,const HashTable * ht)251 void should_disable_ht(zend_execute_data* execute_data,
252 const char* function_name,
253 const zend_string* builtin_param,
254 const char* builtin_param_name,
255 const sp_list_node* config, const HashTable* ht) {
256 const sp_list_node* ht_entry = NULL;
257 zend_string* current_filename;
258
259 if (!execute_data) {
260 return; // LCOV_EXCL_LINE
261 }
262
263 if (UNEXPECTED(builtin_param && !strcmp(function_name, "eval"))) {
264 current_filename = get_eval_filename(zend_get_executed_filename());
265 } else {
266 const char* tmp = zend_get_executed_filename();
267 current_filename = zend_string_init(tmp, strlen(tmp), 0);
268 }
269
270 ht_entry = zend_hash_str_find_ptr(ht, function_name, strlen(function_name));
271
272 if (ht_entry) {
273 should_disable(execute_data, function_name, builtin_param,
274 builtin_param_name, ht_entry, current_filename);
275 } else if (config && config->data) {
276 should_disable(execute_data, function_name, builtin_param,
277 builtin_param_name, config, current_filename);
278 }
279
280 efree(current_filename);
281 }
282
should_disable(zend_execute_data * execute_data,const char * complete_function_path,const zend_string * builtin_param,const char * builtin_param_name,const sp_list_node * config,const zend_string * current_filename)283 static void should_disable(zend_execute_data* execute_data,
284 const char* complete_function_path,
285 const zend_string* builtin_param,
286 const char* builtin_param_name,
287 const sp_list_node* config,
288 const zend_string* current_filename) {
289 char current_file_hash[SHA256_SIZE * 2 + 1] = {0};
290
291 while (config) {
292 sp_disabled_function const* const config_node =
293 (sp_disabled_function*)(config->data);
294 const char* arg_name = NULL;
295 const zend_string* arg_value_str = NULL;
296
297 /* The order matters, since when we have `config_node->functions_list`,
298 we also do have `config_node->function` */
299 if (config_node->functions_list) {
300 if (false == is_functions_list_matching(execute_data,
301 config_node->functions_list)) {
302 goto next;
303 }
304 } else if (config_node->function) {
305 if (0 !=
306 strcmp(ZSTR_VAL(config_node->function), complete_function_path)) {
307 goto next; // LCOV_EXCL_LINE
308 }
309 } else if (config_node->r_function) {
310 if (false == sp_is_regexp_matching(config_node->r_function,
311 complete_function_path)) {
312 goto next;
313 }
314 }
315 if (config_node->line) {
316 if (config_node->line != zend_get_executed_lineno()) {
317 goto next;
318 }
319 }
320 if (config_node->filename || config_node->r_filename) {
321 zend_execute_data* ex =
322 is_file_matching(execute_data, config_node, current_filename);
323 if (!ex) {
324 goto next;
325 }
326 }
327
328 if (config_node->cidr) {
329 const char* client_ip = get_ipaddr();
330 if (client_ip && false == cidr_match(client_ip, config_node->cidr)) {
331 goto next;
332 }
333 }
334 if (config_node->var) {
335 if (false == is_local_var_matching(execute_data, config_node)) {
336 goto next;
337 }
338 }
339
340 if (config_node->hash) {
341 if ('\0' == current_file_hash[0]) {
342 compute_hash(ZSTR_VAL(current_filename), current_file_hash);
343 }
344 if (0 != strncmp(current_file_hash, ZSTR_VAL(config_node->hash),
345 SHA256_SIZE)) {
346 goto next;
347 }
348 }
349
350 /* Check if we filter on parameter value*/
351 if (config_node->param || config_node->r_param ||
352 (config_node->pos != -1)) {
353 if (!builtin_param &&
354 #if PHP_VERSION_ID >= 80000
355 ZEND_ARG_IS_VARIADIC(execute_data->func->op_array.arg_info)
356 #else
357 execute_data->func->op_array.arg_info->is_variadic
358 #endif
359 ) {
360 sp_log_warn(
361 "disable_function",
362 "Snuffleupagus doesn't support variadic functions yet, sorry. "
363 "Check https://github.com/jvoisin/snuffleupagus/issues/164 for "
364 "details.");
365 } else if (false == is_param_matching(
366 execute_data, config_node, builtin_param,
367 &arg_name, builtin_param_name, &arg_value_str)) {
368 goto next;
369 }
370 }
371
372 if (config_node->r_value || config_node->value) {
373 if (check_is_builtin_name(config_node) && !config_node->var &&
374 !config_node->key && !config_node->r_key && !config_node->param &&
375 !config_node->r_param) {
376 if (false == is_param_matching(execute_data, config_node, builtin_param,
377 &arg_name, builtin_param_name,
378 &arg_value_str)) {
379 goto next;
380 }
381 }
382 }
383
384 /* Everything matched.*/
385 if (true == config_node->allow) {
386 return;
387 }
388
389 if (config_node->functions_list) {
390 sp_log_disable(ZSTR_VAL(config_node->function), arg_name, arg_value_str,
391 config_node);
392 } else {
393 sp_log_disable(complete_function_path, arg_name, arg_value_str,
394 config_node);
395 }
396
397 next:
398 config = config->next;
399 }
400 }
401
should_drop_on_ret_ht(const zval * return_value,const char * function_name,const sp_list_node * config,const HashTable * ht,zend_execute_data * execute_data)402 void should_drop_on_ret_ht(const zval* return_value, const char* function_name,
403 const sp_list_node* config, const HashTable* ht,
404 zend_execute_data* execute_data) {
405 const sp_list_node* ht_entry = NULL;
406
407 if (!function_name) {
408 return; // LCOV_EXCL_LINE
409 }
410
411 ht_entry = zend_hash_str_find_ptr(ht, function_name, strlen(function_name));
412
413 if (ht_entry) {
414 should_drop_on_ret(return_value, ht_entry, function_name, execute_data);
415 } else if (config && config->data) {
416 should_drop_on_ret(return_value, config, function_name, execute_data);
417 }
418 }
419
should_drop_on_ret(const zval * return_value,const sp_list_node * config,const char * complete_function_path,zend_execute_data * execute_data)420 static void should_drop_on_ret(const zval* return_value,
421 const sp_list_node* config,
422 const char* complete_function_path,
423 zend_execute_data* execute_data) {
424 const char* current_filename = zend_get_executed_filename(TSRMLS_C);
425 char current_file_hash[SHA256_SIZE * 2 + 1] = {0};
426 bool match_type = false, match_value = false;
427
428 while (config) {
429 const zend_string* ret_value_str = NULL;
430 sp_disabled_function const* const config_node =
431 (sp_disabled_function*)(config->data);
432
433 assert(config_node->function || config_node->r_function);
434
435 if (config_node->functions_list) {
436 if (false == is_functions_list_matching(execute_data,
437 config_node->functions_list)) {
438 goto next;
439 }
440 } else if (config_node->function) {
441 if (0 !=
442 strcmp(ZSTR_VAL(config_node->function), complete_function_path)) {
443 goto next; // LCOV_EXCL_LINE
444 }
445 } else if (config_node->r_function) {
446 if (false == sp_is_regexp_matching(config_node->r_function,
447 complete_function_path)) {
448 goto next;
449 }
450 }
451
452 if (config_node->filename) { /* Check the current file name. */
453 if (0 != strcmp(current_filename, ZSTR_VAL(config_node->filename))) {
454 goto next;
455 }
456 } else if (config_node->r_filename) {
457 if (false ==
458 sp_is_regexp_matching(config_node->r_filename, current_filename)) {
459 goto next;
460 }
461 }
462
463 if (config_node->hash) {
464 if ('\0' == current_file_hash[0]) {
465 compute_hash(current_filename, current_file_hash);
466 }
467 if (0 != strncmp(current_file_hash, ZSTR_VAL(config_node->hash),
468 SHA256_SIZE)) {
469 goto next;
470 }
471 }
472
473 ret_value_str = sp_zval_to_zend_string(return_value);
474
475 match_type = (config_node->ret_type) &&
476 (config_node->ret_type == Z_TYPE_P(return_value));
477 match_value = (config_node->ret || config_node->r_ret) &&
478 (true == sp_match_value(ret_value_str, config_node->ret,
479 config_node->r_ret));
480
481 if (true == match_type || true == match_value) {
482 if (true == config_node->allow) {
483 return;
484 }
485 sp_log_disable_ret(complete_function_path, ret_value_str, config_node);
486 }
487 next:
488 config = config->next;
489 }
490 }
491
ZEND_FUNCTION(check_disabled_function)492 ZEND_FUNCTION(check_disabled_function) {
493 zif_handler orig_handler;
494 const char* current_function_name = get_active_function_name(TSRMLS_C);
495
496 should_disable_ht(
497 execute_data, current_function_name, NULL, NULL,
498 SNUFFLEUPAGUS_G(config).config_disabled_functions_reg->disabled_functions,
499 SNUFFLEUPAGUS_G(config).config_disabled_functions_hooked);
500
501 orig_handler = zend_hash_str_find_ptr(
502 SNUFFLEUPAGUS_G(disabled_functions_hook), current_function_name,
503 strlen(current_function_name));
504 orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
505 should_drop_on_ret_ht(
506 return_value, current_function_name,
507 SNUFFLEUPAGUS_G(config)
508 .config_disabled_functions_reg_ret->disabled_functions,
509 SNUFFLEUPAGUS_G(config).config_disabled_functions_ret_hooked,
510 execute_data);
511 }
512
hook_functions_regexp(const sp_list_node * config)513 static int hook_functions_regexp(const sp_list_node* config) {
514 while (config && config->data) {
515 const zend_string* function_name =
516 ((sp_disabled_function*)config->data)->function;
517 const sp_pcre* function_name_regexp =
518 ((sp_disabled_function*)config->data)->r_function;
519
520 assert(function_name || function_name_regexp);
521
522 if (function_name) {
523 HOOK_FUNCTION(ZSTR_VAL(function_name), disabled_functions_hook,
524 PHP_FN(check_disabled_function));
525 } else {
526 HOOK_FUNCTION_BY_REGEXP(function_name_regexp, disabled_functions_hook,
527 PHP_FN(check_disabled_function));
528 }
529
530 config = config->next;
531 }
532 return SUCCESS;
533 }
534
hook_functions(HashTable * to_hook_ht,HashTable * hooked_ht)535 static int hook_functions(HashTable* to_hook_ht, HashTable* hooked_ht) {
536 zend_string* key;
537 zval* value;
538
539 ZEND_HASH_FOREACH_STR_KEY_VAL(to_hook_ht, key, value) {
540 bool hooked = !HOOK_FUNCTION(ZSTR_VAL(key), disabled_functions_hook,
541 PHP_FN(check_disabled_function));
542 bool is_builtin =
543 check_is_builtin_name(((sp_list_node*)Z_PTR_P(value))->data);
544 if (hooked || is_builtin) {
545 zend_symtable_add_new(hooked_ht, key, value);
546 zend_hash_del(to_hook_ht, key);
547 }
548 }
549 ZEND_HASH_FOREACH_END();
550 return SUCCESS;
551 }
552
ZEND_FUNCTION(eval_blacklist_callback)553 ZEND_FUNCTION(eval_blacklist_callback) {
554 zif_handler orig_handler;
555 const char* current_function_name = get_active_function_name(TSRMLS_C);
556 zend_string* tmp =
557 zend_string_init(current_function_name, strlen(current_function_name), 0);
558
559 if (true == check_is_in_eval_whitelist(tmp)) {
560 zend_string_release(tmp);
561 goto whitelisted;
562 }
563 zend_string_release(tmp);
564
565 if (SNUFFLEUPAGUS_G(in_eval) > 0) {
566 zend_string* filename = get_eval_filename(zend_get_executed_filename());
567 const int line_number = zend_get_executed_lineno(TSRMLS_C);
568 const sp_config_eval* config_eval = SNUFFLEUPAGUS_G(config).config_eval;
569
570 if (config_eval->dump) {
571 sp_log_request(config_eval->dump, config_eval->textual_representation,
572 SP_TOKEN_EVAL_BLACKLIST);
573 }
574 if (config_eval->simulation) {
575 sp_log_simulation("eval",
576 "A call to %s was tried in eval, in %s:%d, logging it.",
577 current_function_name, ZSTR_VAL(filename), line_number);
578 } else {
579 sp_log_drop("eval",
580 "A call to %s was tried in eval, in %s:%d, dropping it.",
581 current_function_name, ZSTR_VAL(filename), line_number);
582 }
583 efree(filename);
584 }
585
586 whitelisted:
587 orig_handler = zend_hash_str_find_ptr(
588 SNUFFLEUPAGUS_G(sp_eval_blacklist_functions_hook), current_function_name,
589 strlen(current_function_name));
590 orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
591 }
592
hook_disabled_functions(void)593 int hook_disabled_functions(void) {
594 TSRMLS_FETCH();
595
596 int ret = SUCCESS;
597
598 ret |=
599 hook_functions(SNUFFLEUPAGUS_G(config).config_disabled_functions,
600 SNUFFLEUPAGUS_G(config).config_disabled_functions_hooked);
601
602 ret |= hook_functions(
603 SNUFFLEUPAGUS_G(config).config_disabled_functions_ret,
604 SNUFFLEUPAGUS_G(config).config_disabled_functions_ret_hooked);
605
606 ret |= hook_functions_regexp(
607 SNUFFLEUPAGUS_G(config)
608 .config_disabled_functions_reg->disabled_functions);
609
610 ret |= hook_functions_regexp(
611 SNUFFLEUPAGUS_G(config)
612 .config_disabled_functions_reg_ret->disabled_functions);
613
614 if (NULL != SNUFFLEUPAGUS_G(config).config_eval->blacklist) {
615 sp_list_node* it = SNUFFLEUPAGUS_G(config).config_eval->blacklist;
616
617 while (it) {
618 hook_function(ZSTR_VAL((zend_string*)it->data),
619 SNUFFLEUPAGUS_G(sp_eval_blacklist_functions_hook),
620 PHP_FN(eval_blacklist_callback));
621 it = it->next;
622 }
623 }
624 return ret;
625 }
626
627 zend_write_func_t zend_write_default = NULL;
628
629 #if PHP_VERSION_ID >= 80000
hook_echo(const char * str,size_t str_length)630 size_t hook_echo(const char* str, size_t str_length) {
631 #else
632 int hook_echo(const char* str, size_t str_length) {
633 #endif
634 zend_string* zs = zend_string_init(str, str_length, 0);
635
636 should_disable_ht(
637 EG(current_execute_data), "echo", zs, NULL,
638 SNUFFLEUPAGUS_G(config).config_disabled_functions_reg->disabled_functions,
639 SNUFFLEUPAGUS_G(config).config_disabled_functions_hooked);
640
641 zend_string_release(zs);
642
643 return zend_write_default(str, str_length);
644 }
645