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