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