1 #include "php_snuffleupagus.h"
2 
sp_zend_string_equals(const zend_string * s1,const zend_string * s2)3 bool sp_zend_string_equals(const zend_string* s1, const zend_string* s2) {
4   // We can't use `zend_string_equals` here because it doesn't work on
5   // `const` zend_string.
6   return ZSTR_LEN(s1) == ZSTR_LEN(s2) &&
7          !memcmp(ZSTR_VAL(s1), ZSTR_VAL(s2), ZSTR_LEN(s1));
8 }
9 
10 static const char* default_ipaddr = "0.0.0.0";
get_ipaddr()11 const char* get_ipaddr() {
12   const char* client_ip = getenv("REMOTE_ADDR");
13   if (client_ip) {
14     return client_ip;
15   }
16 
17   const char* fwd_ip = getenv("HTTP_X_FORWARDED_FOR");
18   if (fwd_ip) {
19     return fwd_ip;
20   }
21 
22   /* Some hosters (like heroku, see
23    * https://github.com/jvoisin/snuffleupagus/issues/336) are clearing the
24    * environment variables, so we don't have access to them, hence why we're
25    * resorting to $_SERVER['REMOTE_ADDR'].
26    */
27   if (!Z_ISUNDEF(PG(http_globals)[TRACK_VARS_SERVER])) {
28     const zval* const globals_client_ip =
29         zend_hash_str_find(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]),
30                            "REMOTE_ADDR", sizeof("REMOTE_ADDR") - 1);
31     if (globals_client_ip) {
32       if (Z_TYPE_P(globals_client_ip) == IS_STRING) {
33         if (Z_STRLEN_P(globals_client_ip) != 0) {
34           return estrdup(Z_STRVAL_P(globals_client_ip));
35         }
36       }
37     }
38   }
39 
40   return default_ipaddr;
41 }
42 
sp_log_msgf(char const * restrict feature,int level,int type,const char * restrict fmt,...)43 void sp_log_msgf(char const* restrict feature, int level, int type,
44                  const char* restrict fmt, ...) {
45   char* msg;
46   va_list args;
47 
48   va_start(args, fmt);
49   vspprintf(&msg, 0, fmt, args);
50   va_end(args);
51 
52   const char* client_ip = get_ipaddr();
53   const char* logtype = NULL;
54   switch (type) {
55     case SP_TYPE_SIMULATION:
56       logtype = "simulation";
57       break;
58     case SP_TYPE_DROP:
59       logtype = "drop";
60       break;
61     case SP_TYPE_LOG:
62     default:
63       logtype = "log";
64       break;
65   }
66 
67   switch (SNUFFLEUPAGUS_G(config).log_media) {
68     case SP_SYSLOG: {
69       const char* error_filename = zend_get_executed_filename();
70       int syslog_level = (level == E_ERROR) ? LOG_ERR : LOG_INFO;
71       int error_lineno = zend_get_executed_lineno(TSRMLS_C);
72       openlog(PHP_SNUFFLEUPAGUS_EXTNAME, LOG_PID, LOG_AUTH);
73       syslog(syslog_level, "[snuffleupagus][%s][%s][%s] %s in %s on line %d",
74              client_ip, feature, logtype, msg, error_filename, error_lineno);
75       closelog();
76       if (type == SP_TYPE_DROP) {
77         zend_bailout();
78       }
79       break;
80     }
81     case SP_ZEND:
82     default:
83       zend_error(level, "[snuffleupagus][%s][%s][%s] %s", client_ip, feature,
84                  logtype, msg);
85       break;
86   }
87 }
88 
compute_hash(const char * const restrict filename,char * restrict file_hash)89 int compute_hash(const char* const restrict filename,
90                  char* restrict file_hash) {
91   unsigned char buf[1024];
92   unsigned char digest[SHA256_SIZE];
93   PHP_SHA256_CTX context;
94   size_t n;
95 
96   php_stream* stream =
97       php_stream_open_wrapper(filename, "rb", REPORT_ERRORS, NULL);
98   if (!stream) {
99     // LCOV_EXCL_START
100     sp_log_err("hash_computation",
101                "Can not open the file %s to compute its hash", filename);
102     return FAILURE;
103     // LCOV_EXCL_STOP
104   }
105 
106   PHP_SHA256Init(&context);
107   while ((n = php_stream_read(stream, (char*)buf, sizeof(buf))) > 0) {
108     PHP_SHA256Update(&context, buf, n);
109   }
110   PHP_SHA256Final(digest, &context);
111   php_stream_close(stream);
112   make_digest_ex(file_hash, digest, SHA256_SIZE);
113   return SUCCESS;
114 }
115 
construct_filename(char * filename,const zend_string * restrict folder,const zend_string * restrict textual)116 static int construct_filename(char* filename,
117                               const zend_string* restrict folder,
118                               const zend_string* restrict textual) {
119   PHP_SHA256_CTX context;
120   unsigned char digest[SHA256_SIZE] = {0};
121   char strhash[65] = {0};
122 
123   if (-1 == mkdir(ZSTR_VAL(folder), 0700) && errno != EEXIST) {
124     sp_log_warn("request_logging", "Unable to create the folder '%s'",
125                 ZSTR_VAL(folder));
126     return -1;
127   }
128 
129   /* We're using the sha256 sum of the rule's textual representation
130    * as filename, in order to only have one dump per rule, to mitigate
131    * DoS attacks. */
132   PHP_SHA256Init(&context);
133   PHP_SHA256Update(&context, (const unsigned char*)ZSTR_VAL(textual),
134                    ZSTR_LEN(textual));
135   PHP_SHA256Final(digest, &context);
136   make_digest_ex(strhash, digest, SHA256_SIZE);
137   snprintf(filename, PATH_MAX - 1, "%s/sp_dump.%s", ZSTR_VAL(folder), strhash);
138 
139   return 0;
140 }
141 
sp_log_request(const zend_string * restrict folder,const zend_string * restrict text_repr,char const * const from)142 int sp_log_request(const zend_string* restrict folder,
143                    const zend_string* restrict text_repr,
144                    char const* const from) {
145   FILE* file;
146   const char* current_filename = zend_get_executed_filename(TSRMLS_C);
147   const int current_line = zend_get_executed_lineno(TSRMLS_C);
148   char filename[PATH_MAX] = {0};
149   const struct {
150     char const* const str;
151     const int key;
152   } zones[] = {{"GET", TRACK_VARS_GET},       {"POST", TRACK_VARS_POST},
153                {"COOKIE", TRACK_VARS_COOKIE}, {"SERVER", TRACK_VARS_SERVER},
154                {"ENV", TRACK_VARS_ENV},       {NULL, 0}};
155 
156   if (0 != construct_filename(filename, folder, text_repr)) {
157     return -1;
158   }
159   if (NULL == (file = fopen(filename, "w+"))) {
160     sp_log_warn("request_logging", "Unable to open %s: %s", filename,
161                 strerror(errno));
162     return -1;
163   }
164 
165   fprintf(file, "RULE: sp%s%s\n", from, ZSTR_VAL(text_repr));
166 
167   fprintf(file, "FILE: %s:%d\n", current_filename, current_line);
168 
169   zend_execute_data* orig_execute_data = EG(current_execute_data);
170   zend_execute_data* current = EG(current_execute_data);
171   while (current) {
172     EG(current_execute_data) = current;
173     char* const complete_path_function = get_complete_function_path(current);
174     if (complete_path_function) {
175       const int current_line = zend_get_executed_lineno(TSRMLS_C);
176       fprintf(file, "STACKTRACE: %s:%d\n", complete_path_function,
177               current_line);
178     }
179     current = current->prev_execute_data;
180   }
181   EG(current_execute_data) = orig_execute_data;
182 
183   for (size_t i = 0; zones[i].str; i++) {
184     zval* variable_value;
185     zend_string* variable_key;
186 
187     if (Z_TYPE(PG(http_globals)[zones[i].key]) == IS_UNDEF) {
188       continue;
189     }
190 
191     HashTable* ht = Z_ARRVAL(PG(http_globals)[zones[i].key]);
192     fprintf(file, "%s:", zones[i].str);
193     ZEND_HASH_FOREACH_STR_KEY_VAL(ht, variable_key, variable_value) {
194       smart_str a;
195 
196       memset(&a, 0, sizeof(a));
197       php_var_export_ex(variable_value, 1, &a);
198       ZSTR_VAL(a.s)[ZSTR_LEN(a.s)] = '\0';
199       fprintf(file, "%s=%s ", ZSTR_VAL(variable_key), ZSTR_VAL(a.s));
200       zend_string_release(a.s);
201     }
202     ZEND_HASH_FOREACH_END();
203     fputs("\n", file);
204   }
205   fclose(file);
206 
207   return 0;
208 }
209 
zend_string_to_char(const zend_string * zs)210 static char* zend_string_to_char(const zend_string* zs) {
211   // Remove all \0 in a zend_string and replace them with '0' instead.
212 
213   if (ZSTR_LEN(zs) + 1 < ZSTR_LEN(zs)) {
214     // LCOV_EXCL_START
215     sp_log_err("overflow_error",
216                "Overflow tentative detected in zend_string_to_char");
217     zend_bailout();
218     // LCOV_EXCL_STOP
219   }
220 
221   char* copy = ecalloc(ZSTR_LEN(zs) + 1, 1);
222   for (size_t i = 0; i < ZSTR_LEN(zs); i++) {
223     if (ZSTR_VAL(zs)[i] == '\0') {
224       copy[i] = '0';
225     } else {
226       copy[i] = ZSTR_VAL(zs)[i];
227     }
228   }
229   return copy;
230 }
231 
sp_zval_to_zend_string(const zval * zv)232 const zend_string* sp_zval_to_zend_string(const zval* zv) {
233   switch (Z_TYPE_P(zv)) {
234     case IS_LONG: {
235       char* msg;
236       spprintf(&msg, 0, ZEND_LONG_FMT, Z_LVAL_P(zv));
237       zend_string* zs = zend_string_init(msg, strlen(msg), 0);
238       efree(msg);
239       return zs;
240     }
241     case IS_DOUBLE: {
242       char* msg;
243       spprintf(&msg, 0, "%f", Z_DVAL_P(zv));
244       zend_string* zs = zend_string_init(msg, strlen(msg), 0);
245       efree(msg);
246       return zs;
247     }
248     case IS_STRING: {
249       return Z_STR_P(zv);
250     }
251     case IS_FALSE:
252       return zend_string_init("FALSE", sizeof("FALSE") - 1, 0);
253     case IS_TRUE:
254       return zend_string_init("TRUE", sizeof("TRUE") - 1, 0);
255     case IS_NULL:
256       return zend_string_init("NULL", sizeof("NULL") - 1, 0);
257     case IS_OBJECT:
258       return zend_string_init("OBJECT", sizeof("OBJECT") - 1, 0);
259     case IS_ARRAY:
260       return zend_string_init("ARRAY", sizeof("ARRAY") - 1, 0);
261     case IS_RESOURCE:
262       return zend_string_init("RESOURCE", sizeof("RESOURCE") - 1, 0);
263     default:                              // LCOV_EXCL_LINE
264       return zend_string_init("", 0, 0);  // LCOV_EXCL_LINE
265   }
266 }
267 
sp_match_value(const zend_string * value,const zend_string * to_match,const sp_pcre * rx)268 bool sp_match_value(const zend_string* value, const zend_string* to_match,
269                     const sp_pcre* rx) {
270   if (to_match) {
271     return (sp_zend_string_equals(to_match, value));
272   } else if (rx) {
273     char* tmp = zend_string_to_char(value);
274     bool ret = sp_is_regexp_matching(rx, tmp);
275     efree(tmp);
276     return ret;
277   } else {
278     return true;
279   }
280   return false;
281 }
282 
sp_log_disable(const char * restrict path,const char * restrict arg_name,const zend_string * restrict arg_value,const sp_disabled_function * config_node)283 void sp_log_disable(const char* restrict path, const char* restrict arg_name,
284                     const zend_string* restrict arg_value,
285                     const sp_disabled_function* config_node) {
286   const zend_string* dump = config_node->dump;
287   const zend_string* alias = config_node->alias;
288   const int sim = config_node->simulation;
289 
290   if (dump) {
291     sp_log_request(config_node->dump, config_node->textual_representation,
292                    SP_TOKEN_DISABLE_FUNC);
293   }
294   if (arg_name) {
295     char* char_repr = NULL;
296     if (arg_value) {
297       char_repr = zend_string_to_char(arg_value);
298     }
299     if (alias) {
300       sp_log_auto(
301           "disabled_function", sim,
302           "Aborted execution on call of the function '%s', "
303           "because its argument '%s' content (%s) matched the rule '%s'",
304           path, arg_name, char_repr ? char_repr : "?", ZSTR_VAL(alias));
305     } else {
306       sp_log_auto("disabled_function", sim,
307                   "Aborted execution on call of the function '%s', "
308                   "because its argument '%s' content (%s) matched a rule",
309                   path, arg_name, char_repr ? char_repr : "?");
310     }
311     efree(char_repr);
312   } else {
313     if (alias) {
314       sp_log_auto("disabled_function", sim,
315                   "Aborted execution on call of the function '%s', "
316                   "because of the the rule '%s'",
317                   path, ZSTR_VAL(alias));
318     } else {
319       sp_log_auto("disabled_function", sim,
320                   "Aborted execution on call of the function '%s'", path);
321     }
322   }
323 }
324 
sp_log_disable_ret(const char * restrict path,const zend_string * restrict ret_value,const sp_disabled_function * config_node)325 void sp_log_disable_ret(const char* restrict path,
326                         const zend_string* restrict ret_value,
327                         const sp_disabled_function* config_node) {
328   const zend_string* dump = config_node->dump;
329   const zend_string* alias = config_node->alias;
330   const int sim = config_node->simulation;
331   char* char_repr = NULL;
332 
333   if (dump) {
334     sp_log_request(dump, config_node->textual_representation,
335                    SP_TOKEN_DISABLE_FUNC);
336   }
337   if (ret_value) {
338     char_repr = zend_string_to_char(ret_value);
339   }
340   if (alias) {
341     sp_log_auto(
342         "disabled_function", sim,
343         "Aborted execution on return of the function '%s', "
344         "because the function returned '%s', which matched the rule '%s'",
345         path, char_repr ? char_repr : "?", ZSTR_VAL(alias));
346   } else {
347     sp_log_auto("disabled_function", sim,
348                 "Aborted execution on return of the function '%s', "
349                 "because the function returned '%s', which matched a rule",
350                 path, char_repr ? char_repr : "?");
351   }
352   efree(char_repr);
353 }
354 
sp_match_array_key(const zval * zv,const zend_string * to_match,const sp_pcre * rx)355 bool sp_match_array_key(const zval* zv, const zend_string* to_match,
356                         const sp_pcre* rx) {
357   zend_string* key;
358   zend_ulong idx;
359 
360   ZEND_HASH_FOREACH_KEY(Z_ARRVAL_P(zv), idx, key) {
361     if (key) {
362       if (sp_match_value(key, to_match, rx)) {
363         return true;
364       }
365     } else {
366       char* idx_str = NULL;
367       spprintf(&idx_str, 0, ZEND_ULONG_FMT, idx);
368       zend_string* tmp = zend_string_init(idx_str, strlen(idx_str), 0);
369       if (sp_match_value(tmp, to_match, rx)) {
370         efree(idx_str);
371         return true;
372       }
373       efree(idx_str);
374     }
375   }
376   ZEND_HASH_FOREACH_END();
377   return false;
378 }
379 
sp_match_array_value(const zval * arr,const zend_string * to_match,const sp_pcre * rx)380 bool sp_match_array_value(const zval* arr, const zend_string* to_match,
381                           const sp_pcre* rx) {
382   zval* value;
383 
384   ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(arr), value) {
385     if (Z_TYPE_P(value) != IS_ARRAY) {
386       if (sp_match_value(sp_zval_to_zend_string(value), to_match, rx)) {
387         return true;
388       }
389     } else if (sp_match_array_value(value, to_match, rx)) {
390       return true;
391     }
392   }
393   ZEND_HASH_FOREACH_END();
394   return false;
395 }
396 
hook_function(const char * original_name,HashTable * hook_table,zif_handler new_function)397 int hook_function(const char* original_name, HashTable* hook_table,
398                   zif_handler new_function) {
399   zend_internal_function* func;
400   bool ret = FAILURE;
401 
402   /* The `mb` module likes to hook functions, like strlen->mb_strlen,
403    * so we have to hook both of them. */
404 
405   if ((func = zend_hash_str_find_ptr(CG(function_table),
406                                      VAR_AND_LEN(original_name)))) {
407     if (func->handler == new_function) {
408       return SUCCESS;  // the function is already hooked
409     } else {
410       if (zend_hash_str_add_new_ptr((hook_table), VAR_AND_LEN(original_name),
411                                     func->handler) == NULL) {
412         // LCOV_EXCL_START
413         sp_log_err("function_pointer_saving",
414                    "Could not save function pointer for %s", original_name);
415         return FAILURE;
416         // LCOV_EXCL_STOP
417       }
418       func->handler = new_function;
419       ret = SUCCESS;
420     }
421   }
422 
423 #if PHP_VERSION_ID < 80000
424   CG(compiler_options) |= ZEND_COMPILE_NO_BUILTIN_STRLEN;
425 #endif
426 
427   if (0 == strncmp(original_name, "mb_", 3) && !CG(multibyte)) {
428     if (zend_hash_str_find(CG(function_table),
429                            VAR_AND_LEN(original_name + 3))) {
430       return hook_function(original_name + 3, hook_table, new_function);
431     }
432   } else if (CG(multibyte)) {
433     // LCOV_EXCL_START
434     char* mb_name = ecalloc(strlen(original_name) + 3 + 1, 1);
435     if (NULL == mb_name) {
436       return FAILURE;
437     }
438     memcpy(mb_name, "mb_", sizeof("mb_") - 1);
439     memcpy(mb_name + 3, VAR_AND_LEN(original_name));
440     if (zend_hash_str_find(CG(function_table), VAR_AND_LEN(mb_name))) {
441       return hook_function(mb_name, hook_table, new_function);
442     }
443     free(mb_name);
444     // LCOV_EXCL_STOP
445   }
446 
447   return ret;
448 }
449 
hook_regexp(const sp_pcre * regexp,HashTable * hook_table,zif_handler new_function)450 int hook_regexp(const sp_pcre* regexp, HashTable* hook_table,
451                 zif_handler new_function) {
452   zend_string* key;
453 
454   ZEND_HASH_FOREACH_STR_KEY(CG(function_table), key)
455   if (key) {
456     if (true == sp_is_regexp_matching_len(regexp, key->val, key->len)) {
457       hook_function(key->val, hook_table, new_function);
458     }
459   }
460   ZEND_HASH_FOREACH_END();
461 
462   return SUCCESS;
463 }
464 
check_is_in_eval_whitelist(const zend_string * const function_name)465 bool check_is_in_eval_whitelist(const zend_string* const function_name) {
466   const sp_list_node* it = SNUFFLEUPAGUS_G(config).config_eval->whitelist;
467 
468   if (!it) {
469     return false;
470   }
471 
472   /* yes, we could use a HashTable instead, but since the list is pretty
473    * small, it doesn't make a difference in practise. */
474   while (it && it->data) {
475     if (sp_zend_string_equals(function_name, (const zend_string*)(it->data))) {
476       /* We've got a match, the function is whiteslited. */
477       return true;
478     }
479     it = it->next;
480   }
481   return false;
482 }
483