/* +----------------------------------------------------------------------+ | Hardened-PHP Project's varfilter extension | +----------------------------------------------------------------------+ | Copyright (c) 2004-2005 Stefan Esser | +----------------------------------------------------------------------+ | This source file is subject to version 2.02 of the PHP license, | | that is bundled with this package in the file LICENSE, and is | | available at through the world-wide-web at | | http://www.php.net/license/2_02.txt. | | If you did not receive a copy of the PHP license and are unable to | | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ | Author: Stefan Esser | +----------------------------------------------------------------------+ $Id: varfilter.c,v 1.1 2004/11/14 13:27:16 ionic Exp $ */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h" #include "ext/standard/info.h" #include "php_varfilter.h" #include "hardening_patch.h" ZEND_DECLARE_MODULE_GLOBALS(varfilter) /* True global resources - no need for thread safety here */ static int le_varfilter; /* {{{ varfilter_module_entry */ zend_module_entry varfilter_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif "varfilter", NULL, PHP_MINIT(varfilter), PHP_MSHUTDOWN(varfilter), PHP_RINIT(varfilter), /* Replace with NULL if there's nothing to do at request start */ PHP_RSHUTDOWN(varfilter), /* Replace with NULL if there's nothing to do at request end */ PHP_MINFO(varfilter), #if ZEND_MODULE_API_NO >= 20010901 "0.3.2", /* Replace with version number for your extension */ #endif STANDARD_MODULE_PROPERTIES }; /* }}} */ #ifdef COMPILE_DL_VARFILTER ZEND_GET_MODULE(varfilter) #endif /* {{{ PHP_INI */ PHP_INI_BEGIN() /* for backward compatibility */ STD_PHP_INI_ENTRY("varfilter.max_request_variables", "200", PHP_INI_PERDIR, OnUpdateLong, max_request_variables, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("varfilter.max_varname_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_varname_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("varfilter.max_value_length", "65000", PHP_INI_PERDIR, OnUpdateLong, max_value_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("varfilter.max_array_depth", "100", PHP_INI_PERDIR, OnUpdateLong, max_array_depth, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("varfilter.max_totalname_length", "256", PHP_INI_PERDIR, OnUpdateLong, max_totalname_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("varfilter.max_array_index_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_array_index_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.request.max_vars", "200", PHP_INI_PERDIR, OnUpdateLong, max_request_variables, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.request.max_varname_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_varname_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.request.max_value_length", "65000", PHP_INI_PERDIR, OnUpdateLong, max_value_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.request.max_array_depth", "100", PHP_INI_PERDIR, OnUpdateLong, max_array_depth, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.request.max_totalname_length", "256", PHP_INI_PERDIR, OnUpdateLong, max_totalname_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.request.max_array_index_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_array_index_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.cookie.max_vars", "100", PHP_INI_PERDIR, OnUpdateLong, max_cookie_vars, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.cookie.max_name_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_cookie_name_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.cookie.max_totalname_length", "256", PHP_INI_PERDIR, OnUpdateLong, max_cookie_totalname_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.cookie.max_value_length", "10000", PHP_INI_PERDIR, OnUpdateLong, max_cookie_value_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.cookie.max_array_depth", "100", PHP_INI_PERDIR, OnUpdateLong, max_cookie_array_depth, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.cookie.max_array_index_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_cookie_array_index_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.get.max_vars", "100", PHP_INI_PERDIR, OnUpdateLong, max_get_vars, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.get.max_name_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_get_name_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.get.max_totalname_length", "256", PHP_INI_PERDIR, OnUpdateLong, max_get_totalname_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.get.max_value_length", "512", PHP_INI_PERDIR, OnUpdateLong, max_get_value_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.get.max_array_depth", "50", PHP_INI_PERDIR, OnUpdateLong, max_get_array_depth, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.get.max_array_index_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_get_array_index_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.post.max_vars", "200", PHP_INI_PERDIR, OnUpdateLong, max_post_vars, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.post.max_name_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_post_name_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.post.max_totalname_length", "256", PHP_INI_PERDIR, OnUpdateLong, max_post_totalname_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.post.max_value_length", "65000", PHP_INI_PERDIR, OnUpdateLong, max_post_value_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.post.max_array_depth", "100", PHP_INI_PERDIR, OnUpdateLong, max_post_array_depth, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.post.max_array_index_length", "64", PHP_INI_PERDIR, OnUpdateLong, max_post_array_index_length, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.upload.max_uploads", "25", PHP_INI_PERDIR, OnUpdateLong, max_uploads, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.upload.disallow_elf_files", "1", PHP_INI_SYSTEM, OnUpdateBool, disallow_elf_files, zend_varfilter_globals, varfilter_globals) STD_PHP_INI_ENTRY("hphp.upload.verification_script", NULL, PHP_INI_SYSTEM, OnUpdateString, verification_script, zend_varfilter_globals, varfilter_globals) PHP_INI_END() /* }}} */ /* {{{ php_varfilter_init_globals */ static void php_varfilter_init_globals(zend_varfilter_globals *varfilter_globals) { varfilter_globals->max_request_variables = 200; varfilter_globals->max_varname_length = 64; varfilter_globals->max_value_length = 10000; varfilter_globals->max_array_depth = 100; varfilter_globals->max_totalname_length = 256; varfilter_globals->max_array_index_length = 64; varfilter_globals->max_cookie_vars = 100; varfilter_globals->max_cookie_name_length = 64; varfilter_globals->max_cookie_totalname_length = 256; varfilter_globals->max_cookie_value_length = 10000; varfilter_globals->max_cookie_array_depth = 100; varfilter_globals->max_cookie_array_index_length = 64; varfilter_globals->max_get_vars = 100; varfilter_globals->max_get_name_length = 64; varfilter_globals->max_get_totalname_length = 256; varfilter_globals->max_get_value_length = 512; varfilter_globals->max_get_array_depth = 50; varfilter_globals->max_get_array_index_length = 64; varfilter_globals->max_post_vars = 200; varfilter_globals->max_post_name_length = 64; varfilter_globals->max_post_totalname_length = 256; varfilter_globals->max_post_value_length = 65000; varfilter_globals->max_post_array_depth = 100; varfilter_globals->max_post_array_index_length = 64; varfilter_globals->max_uploads = 25; varfilter_globals->disallow_elf_files = 1; varfilter_globals->verification_script = NULL; } /* }}} */ /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(varfilter) { ZEND_INIT_MODULE_GLOBALS(varfilter, php_varfilter_init_globals, NULL); REGISTER_INI_ENTRIES(); sapi_register_input_filter(varfilter_input_filter); sapi_register_pre_upload_filter(varfilter_pre_upload_filter); sapi_register_upload_content_filter(varfilter_upload_content_filter); sapi_register_post_upload_filter(varfilter_post_upload_filter); return SUCCESS; } /* }}} */ /* {{{ PHP_MSHUTDOWN_FUNCTION */ PHP_MSHUTDOWN_FUNCTION(varfilter) { UNREGISTER_INI_ENTRIES(); return SUCCESS; } /* }}} */ /* Remove if there's nothing to do at request start */ /* {{{ PHP_RINIT_FUNCTION */ PHP_RINIT_FUNCTION(varfilter) { VARFILTER_G(cur_request_variables) = 0; VARFILTER_G(cur_get_vars) = 0; VARFILTER_G(cur_post_vars) = 0; VARFILTER_G(cur_cookie_vars) = 0; VARFILTER_G(cur_uploads) = 0; return SUCCESS; } /* }}} */ /* Remove if there's nothing to do at request end */ /* {{{ PHP_RSHUTDOWN_FUNCTION */ PHP_RSHUTDOWN_FUNCTION(varfilter) { return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(varfilter) { php_info_print_table_start(); php_info_print_table_header(2, "Hardening-Patch's variable filter support", "enabled"); php_info_print_table_end(); DISPLAY_INI_ENTRIES(); } /* }}} */ /* {{{ normalize_varname */ static void normalize_varname(char *varname) { char *s=varname, *index=NULL, *indexend=NULL, *p; /* overjump leading space */ while (*s == ' ') { s++; } /* and remove it */ if (s != varname) { memmove(varname, s, strlen(s)+1); } for (p=varname; *p && *p != '['; p++) { switch(*p) { case ' ': case '.': *p='_'; break; } } /* find index */ index = strchr(varname, '['); if (index) { index++; s=index; } else { return; } /* done? */ while (index) { while (*index == ' ' || *index == '\r' || *index == '\n' || *index=='\t') { index++; } indexend = strchr(index, ']'); indexend = indexend ? indexend + 1 : index + strlen(index); if (s != index) { memmove(s, index, strlen(index)+1); s += indexend-index; } else { s = indexend; } if (*s == '[') { s++; index = s; } else { index = NULL; } } *s++='\0'; } /* }}} */ /* {{{ SAPI_PRE_UPLOAD_FILTER_FUNC */ SAPI_PRE_UPLOAD_FILTER_FUNC(varfilter_pre_upload_filter) { /* Drop this fileupload if the limit is reached */ if (VARFILTER_G(max_uploads) && VARFILTER_G(max_uploads) <= VARFILTER_G(cur_uploads)) { php_security_log(S_FILES, "configured fileupload limit exceeded - file dropped"); return FAILURE; } return SUCCESS; } /* }}} */ /* {{{ SAPI_UPLOAD_CONTENT_FILTER_FUNC */ SAPI_UPLOAD_CONTENT_FILTER_FUNC(varfilter_upload_content_filter) { if (VARFILTER_G(disallow_elf_files)) { if (offset == 0 && buffer_len > 10) { if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') { php_security_log(S_FILES, "uploaded file is an ELF executable - file dropped"); return FAILURE; } } } return SUCCESS; } /* }}} */ /* {{{ SAPI_POST_UPLOAD_FILTER_FUNC */ SAPI_POST_UPLOAD_FILTER_FUNC(varfilter_post_upload_filter) { int retval = SUCCESS; if (VARFILTER_G(verification_script)) { char cmd[8192]; FILE *in; int first=1; ap_php_snprintf(cmd, sizeof(cmd), "%s %s", VARFILTER_G(verification_script), tmpfilename); if ((in=VCWD_POPEN(cmd, "r"))==NULL) { php_security_log(S_FILES, "unable to execute fileupload verification script - file dropped"); return FAILURE; } retval = FAILURE; /* read and forget the result */ while (1) { int readbytes = fread(cmd, 1, sizeof(cmd), in); if (readbytes<=0) { break; } if (first) { retval = atoi(cmd) == 1 ? SUCCESS : FAILURE; first = 0; } } pclose(in); } if (retval != SUCCESS) { php_security_log(S_FILES, "fileupload verification script disallows file - file dropped"); return FAILURE; } VARFILTER_G(cur_uploads)++; return SUCCESS; } /* }}} */ /* {{{ SAPI_INPUT_FILTER_FUNC */ SAPI_INPUT_FILTER_FUNC(varfilter_input_filter) { char *index, *prev_index = NULL, *copy_var; unsigned int var_len, total_len, depth = 0, rv; /* Drop this variable if the limit is reached */ if (VARFILTER_G(max_request_variables) && VARFILTER_G(max_request_variables) <= VARFILTER_G(cur_request_variables)) { php_security_log(S_VARS, "configured request variable limit exceeded - dropped %s", var); return 0; } switch (arg) { case PARSE_GET: if (VARFILTER_G(max_get_vars) && VARFILTER_G(max_get_vars) <= VARFILTER_G(cur_get_vars)) { php_security_log(S_VARS, "configured GET variable limit exceeded - dropped %s", var); return 0; } break; case PARSE_COOKIE: if (VARFILTER_G(max_cookie_vars) && VARFILTER_G(max_cookie_vars) <= VARFILTER_G(cur_cookie_vars)) { php_security_log(S_VARS, "configured COOKIE variable limit exceeded - dropped %s", var); return 0; } break; case PARSE_POST: if (VARFILTER_G(max_post_vars) && VARFILTER_G(max_post_vars) <= VARFILTER_G(cur_post_vars)) { php_security_log(S_VARS, "configured POST variable limit exceeded - dropped %s", var); return 0; } break; } /* Drop this variable if it exceeds the value length limit */ if (VARFILTER_G(max_value_length) && VARFILTER_G(max_value_length) < val_len) { php_security_log(S_VARS, "configured request variable value length limit exceeded - dropped %s", var); return 0; } switch (arg) { case PARSE_GET: if (VARFILTER_G(max_get_value_length) && VARFILTER_G(max_get_value_length) < val_len) { php_security_log(S_VARS, "configured GET variable value length limit exceeded - dropped %s", var); return 0; } break; case PARSE_COOKIE: if (VARFILTER_G(max_cookie_value_length) && VARFILTER_G(max_cookie_value_length) < val_len) { php_security_log(S_VARS, "configured COOKIE variable value length limit exceeded - dropped %s", var); return 0; } break; case PARSE_POST: if (VARFILTER_G(max_post_value_length) && VARFILTER_G(max_post_value_length) < val_len) { php_security_log(S_VARS, "configured POST variable value length limit exceeded - dropped %s", var); return 0; } break; } /* Normalize the variable name */ normalize_varname(var); /* Find length of variable name */ index = strchr(var, '['); total_len = strlen(var); var_len = index ? index-var : total_len; /* Drop this variable if it exceeds the varname/total length limit */ if (VARFILTER_G(max_varname_length) && VARFILTER_G(max_varname_length) < var_len) { php_security_log(S_VARS, "configured request variable name length limit exceeded - dropped %s", var); return 0; } if (VARFILTER_G(max_totalname_length) && VARFILTER_G(max_totalname_length) < total_len) { php_security_log(S_VARS, "configured request variable total name length limit exceeded - dropped %s", var); return 0; } switch (arg) { case PARSE_GET: if (VARFILTER_G(max_get_name_length) && VARFILTER_G(max_get_name_length) < var_len) { php_security_log(S_VARS, "configured GET variable name length limit exceeded - dropped %s", var); return 0; } if (VARFILTER_G(max_get_totalname_length) && VARFILTER_G(max_get_totalname_length) < var_len) { php_security_log(S_VARS, "configured GET variable total name length limit exceeded - dropped %s", var); return 0; } break; case PARSE_COOKIE: if (VARFILTER_G(max_cookie_name_length) && VARFILTER_G(max_cookie_name_length) < var_len) { php_security_log(S_VARS, "configured COOKIE variable name length limit exceeded - dropped %s", var); return 0; } if (VARFILTER_G(max_cookie_totalname_length) && VARFILTER_G(max_cookie_totalname_length) < var_len) { php_security_log(S_VARS, "configured COOKIE variable total name length limit exceeded - dropped %s", var); return 0; } break; case PARSE_POST: if (VARFILTER_G(max_post_name_length) && VARFILTER_G(max_post_name_length) < var_len) { php_security_log(S_VARS, "configured POST variable name length limit exceeded - dropped %s", var); return 0; } if (VARFILTER_G(max_post_totalname_length) && VARFILTER_G(max_post_totalname_length) < var_len) { php_security_log(S_VARS, "configured POST variable total name length limit exceeded - dropped %s", var); return 0; } break; } /* Find out array depth */ while (index) { unsigned int index_length; depth++; index = strchr(index+1, '['); if (prev_index) { index_length = index ? index - 1 - prev_index - 1: strlen(prev_index); if (VARFILTER_G(max_array_index_length) && VARFILTER_G(max_array_index_length) < index_length) { php_security_log(S_VARS, "configured request variable array index length limit exceeded - dropped %s", var); return 0; } switch (arg) { case PARSE_GET: if (VARFILTER_G(max_get_array_index_length) && VARFILTER_G(max_get_array_index_length) < index_length) { php_security_log(S_VARS, "configured GET variable array index length limit exceeded - dropped %s", var); return 0; } break; case PARSE_COOKIE: if (VARFILTER_G(max_cookie_array_index_length) && VARFILTER_G(max_cookie_array_index_length) < index_length) { php_security_log(S_VARS, "configured COOKIE variable array index length limit exceeded - dropped %s", var); return 0; } break; case PARSE_POST: if (VARFILTER_G(max_post_array_index_length) && VARFILTER_G(max_post_array_index_length) < index_length) { php_security_log(S_VARS, "configured POST variable array index length limit exceeded - dropped %s", var); return 0; } break; } prev_index = index; } } /* Drop this variable if it exceeds the array depth limit */ if (VARFILTER_G(max_array_depth) && VARFILTER_G(max_array_depth) < depth) { php_security_log(S_VARS, "configured request variable array depth limit exceeded - dropped %s", var); return 0; } switch (arg) { case PARSE_GET: if (VARFILTER_G(max_get_array_depth) && VARFILTER_G(max_get_array_depth) < depth) { php_security_log(S_VARS, "configured GET variable array depth limit exceeded - dropped %s", var); return 0; } break; case PARSE_COOKIE: if (VARFILTER_G(max_cookie_array_depth) && VARFILTER_G(max_cookie_array_depth) < depth) { php_security_log(S_VARS, "configured COOKIE variable array depth limit exceeded - dropped %s", var); return 0; } break; case PARSE_POST: if (VARFILTER_G(max_post_array_depth) && VARFILTER_G(max_post_array_depth) < depth) { php_security_log(S_VARS, "configured POST variable array depth limit exceeded - dropped %s", var); return 0; } break; } /* Drop this variable if it is one of GLOBALS, _GET, _POST, ... */ /* This is to protect several silly scripts that do globalizing themself */ switch (var_len) { case 18: if (memcmp(var, "HTTP_RAW_POST_DATA", 18)==0) goto protected_varname; break; case 17: if (memcmp(var, "HTTP_SESSION_VARS", 17)==0) goto protected_varname; break; case 16: if (memcmp(var, "HTTP_SERVER_VARS", 16)==0) goto protected_varname; if (memcmp(var, "HTTP_COOKIE_VARS", 16)==0) goto protected_varname; break; case 15: if (memcmp(var, "HTTP_POST_FILES", 15)==0) goto protected_varname; break; case 14: if (memcmp(var, "HTTP_POST_VARS", 14)==0) goto protected_varname; break; case 13: if (memcmp(var, "HTTP_GET_VARS", 13)==0) goto protected_varname; if (memcmp(var, "HTTP_ENV_VARS", 13)==0) goto protected_varname; break; case 8: if (memcmp(var, "_SESSION", 8)==0) goto protected_varname; if (memcmp(var, "_REQUEST", 8)==0) goto protected_varname; break; case 7: if (memcmp(var, "GLOBALS", 7)==0) goto protected_varname; if (memcmp(var, "_COOKIE", 7)==0) goto protected_varname; if (memcmp(var, "_SERVER", 7)==0) goto protected_varname; break; case 6: if (memcmp(var, "_FILES", 6)==0) goto protected_varname; break; case 5: if (memcmp(var, "_POST", 5)==0) goto protected_varname; break; case 4: if (memcmp(var, "_ENV", 4)==0) goto protected_varname; if (memcmp(var, "_GET", 4)==0) goto protected_varname; break; } /* Okay let PHP register this variable */ VARFILTER_G(cur_request_variables)++; switch (arg) { case PARSE_GET: VARFILTER_G(cur_get_vars)++; break; case PARSE_COOKIE: VARFILTER_G(cur_cookie_vars)++; break; case PARSE_POST: VARFILTER_G(cur_post_vars)++; break; } if (new_val_len) { *new_val_len = val_len; } return 1; protected_varname: php_security_log(S_VARS, "tried to register forbidden variable '%s' through %s variables", var, arg == PARSE_GET ? "GET" : arg == PARSE_POST ? "POST" : "COOKIE"); return 0; } /* }}} */ /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: noet sw=4 ts=4 fdm=marker * vim<600: noet sw=4 ts=4 */