/* * ModSecurity for Apache 1.x, http://www.modsecurity.org/ * Copyright (c) 2002-2007 Breach Security, Inc. (http://www.breach.com) * * You should have received a copy of the licence along with this * program (stored in the file "LICENSE"). If the file is missing, * or if you have any other questions related to the licence, please * write to Breach Security, Inc. at support@breach.com. * */ #include #include #include #include #include #include #if !(defined(WIN32) || defined(NETWARE)) #include #define ENABLE_VAR_SCRIPT_VARIABLES #endif #include "httpd.h" #include "http_config.h" #include "http_request.h" #include "http_protocol.h" #include "http_core.h" #include "http_main.h" #include "http_log.h" #include "util_script.h" #include "ap_md5.h" #if defined(USE_PCRE) #include "pcre.h" #endif #if defined(NETWARE) #include LONG locking_sem = 0; #endif #ifdef WIN32 #include #endif #if defined(NETWARE) #define CREATEMODE ( S_IREAD | S_IWRITE ) #elif defined(WIN32) #define CREATEMODE ( _S_IREAD | _S_IWRITE ) #else #define CREATEMODE ( S_IRUSR | S_IWUSR | S_IRGRP ) #endif #if defined(NETWARE) #define CREATEMODE_DIR ( S_IREAD | S_IWRITE | S_IEXEC ) #elif defined(WIN32) #define CREATEMODE_DIR ( _S_IREAD | _S_IWRITE | _S_IEXEC ) #else #define CREATEMODE_DIR ( S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP ) #endif #if !defined(DISABLE_HTACCESS_CONFIG) #define CMD_SCOPE_ANY OR_OPTIONS #else #define CMD_SCOPE_ANY (RSRC_CONF | ACCESS_CONF) #endif #ifndef PIPE_BUF #define PIPE_BUF (512) #endif module MODULE_VAR_EXPORT security_module; static char *real_server_signature = NULL; #define SMALL_BUF_SIZE 255 #define MODULE_NAME "ModSecurity" #define MODULE_RELEASE "1.9.5" #define MODULE_NAME_FULL (MODULE_NAME " v" MODULE_RELEASE " (Apache 1.3.x)") #define UNICODE_ERROR_CHARACTERS_MISSING -1 #define UNICODE_ERROR_INVALID_ENCODING -2 #define UNICODE_ERROR_OVERLONG_CHARACTER -3 #define MODSEC_SKIP -2000 #define MODSEC_ALLOW -2001 #define NOT_SET -1 #define NOT_SET_P (void *)-1 #define FILTERING_OFF 0 #define FILTERING_ON 1 #define FILTERING_DYNAMIC_ONLY 2 #define AUDITLOG_OFF 0 #define AUDITLOG_ON 1 #define AUDITLOG_DYNAMIC_OR_RELEVANT 2 #define AUDITLOG_RELEVANT_ONLY 3 #define ACTION_NONE 0 #define ACTION_DENY 1 #define ACTION_REDIRECT 2 #define ACTION_ALLOW 3 #define ACTION_SKIP 4 #define ACTION_PROXY 5 #define VAR_ACTION_ALLOW 1 #define VAR_ACTION_DENY 0 #define VAR_UNKNOWN 0 #define VAR_ARG 1 #define VAR_HEADER 2 #define VAR_ENV 3 #define VAR_ARGS 4 #define VAR_POST_PAYLOAD 5 #define VAR_ARGS_NAMES 6 #define VAR_ARGS_VALUES 7 #define VAR_ARGS_SELECTIVE 8 #define VAR_OUTPUT 9 #define VAR_COOKIES_NAMES 10 #define VAR_COOKIES_VALUES 11 #define VAR_COOKIE 12 #define VAR_HEADERS 13 #define VAR_HEADERS_COUNT 14 #define VAR_HEADERS_NAMES 15 #define VAR_HEADERS_VALUES 16 #define VAR_FILES_COUNT 17 #define VAR_FILES_NAMES 18 #define VAR_FILES_SIZES 19 #define VAR_ARGS_COUNT 20 #define VAR_REMOTE_ADDR 21 #define VAR_REMOTE_HOST 22 #define VAR_REMOTE_USER 23 #define VAR_REMOTE_IDENT 24 #define VAR_REQUEST_METHOD 25 #define VAR_SCRIPT_FILENAME 26 #define VAR_PATH_INFO 27 #define VAR_QUERY_STRING 28 #define VAR_AUTH_TYPE 29 #define VAR_DOCUMENT_ROOT 30 #define VAR_SERVER_ADMIN 31 #define VAR_SERVER_NAME 32 #define VAR_SERVER_ADDR 33 #define VAR_SERVER_PORT 34 #define VAR_SERVER_PROTOCOL 35 #define VAR_SERVER_SOFTWARE 36 #define VAR_TIME_YEAR 37 #define VAR_TIME_MON 38 #define VAR_TIME_DAY 39 #define VAR_TIME_HOUR 40 #define VAR_TIME_MIN 41 #define VAR_TIME_SEC 42 #define VAR_TIME_WDAY 43 #define VAR_TIME 44 #define VAR_API_VERSION 45 #define VAR_THE_REQUEST 46 #define VAR_REQUEST_URI 47 #define VAR_REQUEST_FILENAME 48 #define VAR_IS_SUBREQ 49 #define VAR_HANDLER 50 #define VAR_SCRIPT_UID 51 #define VAR_SCRIPT_GID 52 #define VAR_SCRIPT_USERNAME 53 #define VAR_SCRIPT_GROUPNAME 54 #define VAR_SCRIPT_MODE 55 #define VAR_COOKIES_COUNT 56 #define VAR_FILE_NAME 57 #define VAR_FILE_SIZE 58 #define VAR_OUTPUT_STATUS 59 #define VAR_REQUEST_BASENAME 60 #define VAR_SCRIPT_BASENAME 61 #define MULTIPART_BUF_SIZE 4096 #define REQBODY_FILE_NONE 0 #define REQBODY_FILE_DELETE 1 #define REQBODY_FILE_LEAVE 2 #define MULTIPART_FORMDATA 1 #define MULTIPART_FILE 2 #define COOKIES_V0 0 #define COOKIES_V1 1 #define NOTE_MESSAGE "mod_security-message" #define NOTE_EXECUTED "mod_security-executed" #define NOTE_ACTION "mod_security-action" #define NOTE_MSR "mod_security-msr" #define NOTE_ACTED "mod_security-relevant" #define NOTE_BODY "mod_security-body" #define FATAL_ERROR "Unable to allocate memory" #define UNKNOWN_CSID 0 #define MB_CSID 800 /* First multibyte character set */ #define UNI3_CSID 873 /* Unicode 3.x character set ID */ #define SJIS1_CSID 832 /* SJIS character set ID */ #define SJIS2_CSID 834 /* SJIS+YEN character set ID */ #define BIG5_CSID 865 /* BIG5 character set ID */ #define GBK_CSID 852 /* GBK character set ID */ #define GB2312_CSID 850 /* GB2312 character set ID */ #define ZHT32EUC_CSID 860 /* Chinese 4-byte character set */ #define JEUC1_CSID 830 /* JEUC character set ID */ #define JEUC2_CSID 831 /* JEUC+YEN character set ID */ #define JA16VMS_CSID 829 /* VMS 2-byte Japanese */ #define SIGNATURE_NORMAL 0 #define SIGNATURE_EXTERNAL 1 #define KEEP_FILES_OFF 0 #define KEEP_FILES_ON 1 #define KEEP_FILES_RELEVANT_ONLY 2 #define INHERITANCE_IMPORT 1 #define INHERITANCE_REMOVE 2 #ifndef TRUE #define TRUE 1 #define FALSE 0 #endif #define AUDITLOG_SERIAL 0 #define AUDITLOG_CONCURRENT 1 #define AUDITLOG_PART_FIRST 'A' #define AUDITLOG_PART_HEADER 'A' #define AUDITLOG_PART_REQUEST_HEADERS 'B' #define AUDITLOG_PART_REQUEST_BODY 'C' #define AUDITLOG_PART_RESPONSE_HEADERS 'D' #define AUDITLOG_PART_RESPONSE_BODY 'E' #define AUDITLOG_PART_A_RESPONSE_HEADERS 'F' #define AUDITLOG_PART_A_RESPONSE_BODY 'G' #define AUDITLOG_PART_TRAILER 'H' #define AUDITLOG_PART_LAST 'H' #define AUDITLOG_PART_ENDMARKER 'Z' #define ABSOLUTE_VALUE 1 #define RELATIVE_VALUE 2 #define RELATIVE_VALUE_POSITIVE 3 #define RELATIVE_VALUE_NEGATIVE 4 static const char * const all_variables[] = { "UNKOWN", "ARG", "HEADER", "ENV", "ARGS", "POST_PAYLOAD", "ARGS_NAMES", "ARGS_VALUES", "ARGS_SELECTIVE", "OUTPUT", "COOKIES_NAMES", /* 10 */ "COOKIES_VALUES", "COOKIE", "HEADERS", "HEADERS_COUNT", "HEADERS_NAMES", "HEADERS_VALUES", "FILES_COUNT", "FILES_NAMES", "FILES_SIZES", "ARGS_COUNT", /* 20 */ "REMOTE_ADDR", "REMOTE_HOST", "REMOTE_USER", "REMOTE_IDENT", "REQUEST_METHOD", "SCRIPT_FILENAME", "PATH_INFO", "QUERY_STRING", "AUTH_TYPE", "DOCUMENT_ROOT", /* 30 */ "SERVER_ADMIN", "SERVER_NAME", "SERVER_ADDR", "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE", "TIME_YEAR", "TIME_MON", "TIME_DAY", "TIME_HOUR", /* 40 */ "TIME_MIN", "TIME_SEC", "TIME_WDAY", "TIME", "API_VERSION", "THE_REQUEST", "REQUEST_URI", "REQUEST_FILENAME", "IS_SUBREQ", "HANDLER", /* 50 */ "SCRIPT_UID", "SCRIPT_GID", "SCRIPT_USERNAME", "SCRIPT_GROUPNAME", "SCRIPT_MODE", "COOKIES_COUNT", "FILE_NAME", "FILE_SIZE", "OUTPUT_STATUS", "REQUEST_BASENAME", /* 60 */ "SCRIPT_BASENAME", NULL }; static const char * const severities[] = { "EMERGENCY", "ALERT", "CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG", NULL, }; static const char * const status_lines[] = { "100 Continue", "101 Switching Protocols", "102 Processing", #define MY_LEVEL_200 3 "200 OK", "201 Created", "202 Accepted", "203 Non-Authoritative Information", "204 No Content", "205 Reset Content", "206 Partial Content", "207 Multi-Status", #define MY_LEVEL_300 11 "300 Multiple Choices", "301 Moved Permanently", "302 Found", "303 See Other", "304 Not Modified", "305 Use Proxy", "306 unused", "307 Temporary Redirect", #define MY_LEVEL_400 19 "400 Bad Request", "401 Authorization Required", "402 Payment Required", "403 Forbidden", "404 Not Found", "405 Method Not Allowed", "406 Not Acceptable", "407 Proxy Authentication Required", "408 Request Time-out", "409 Conflict", "410 Gone", "411 Length Required", "412 Precondition Failed", "413 Request Entity Too Large", "414 Request-URI Too Large", "415 Unsupported Media Type", "416 Requested Range Not Satisfiable", "417 Expectation Failed", "418 unused", "419 unused", "420 unused", "421 unused", "422 Unprocessable Entity", "423 Locked", "424 Failed Dependency", #define MY_LEVEL_500 44 "500 Internal Server Error", "501 Method Not Implemented", "502 Bad Gateway", "503 Service Temporarily Unavailable", "504 Gateway Time-out", "505 HTTP Version Not Supported", "506 Variant Also Negotiates", "507 Insufficient Storage", "508 unused", "509 unused", "510 Not Extended", #define MY_RESPONSE_CODES 50 NULL }; typedef struct { char *name; int type; int action; } variable; typedef struct { request_rec *r; char *command; char *args; } exec_data; typedef struct { char *buffer; char *sofar; long length; long remaining; int is_multipart; char *tmp_file_name; int tmp_file_mode; int is_put; int tmp_file_fd; } request_body; typedef struct { int log; int auditlog; int action; int status; int pause; int skip_count; int is_chained; char *redirect_url; char *proxy_url; int exec; char *exec_string; char *id; char *rev; char *msg; int severity; char *note_name; char *note_value; char *env_name; char *env_value; int mandatory; int logparts; char *logparts_value; } actionset_t; typedef struct signature signature; struct signature { actionset_t *actionset; int actions_restricted; char *pattern; regex_t *regex; int is_selective; int is_negative; int is_allow; int requires_parsed_args; array_header *variables; int type; /* 0 for normal rules, INHERITANCE_IMPORT if this rule is only * a placeholder for a rule that will be imported from the * parent context at runtime, or INHERITANCE_REMOVE if this rule * is only a placeholder for a rule that will be removed from the * configuration at runtime. */ int is_inheritance_placeholder; /* the ID of the rule that is being explicitly imported or removed */ const char *inheritance_id; signature *first_sig_in_chain; }; typedef struct sec_dir_config sec_dir_config; struct sec_dir_config { int filter_engine; int configuration_helper; int scan_post; actionset_t *actionset; actionset_t *actionset_signatures; array_header *signatures; char *path; /* -- Audit log -- */ /* Whether audit log should be enabled in the context or not */ int auditlog_flag; /* AUDITLOG_SERIAL (old, default) or AUDITLOG_CONCURRENT (new) */ int auditlog_type; /* The name of the audit log file (for the old type), or the * name of the index file (for the new audit log type) */ char *auditlog_name; /* The file descriptor for the file above */ int auditlog_fd; /* For the new-style audit log only, the path where * audit log entries will be stored */ char *auditlog_storage_dir; /* A list of parts to include in the new-style audit log * entry. By default, it contains 'ABCFHZ'. Have a look at * the AUDITLOG_PART_* constants above to decipher the * meaning. */ char *auditlog_parts; /* A regular expression that determines if a response * status is treated as relevant. */ regex_t *auditlog_relevant_regex; /* -- Debug log -- */ int filter_debug_level; char *debuglog_name; int debuglog_fd; int filters_clear; int range_start; int range_end; int check_encoding; int check_unicode_encoding; char *upload_dir; int upload_keep_files; char *upload_approve_script; int normalize_cookies; int check_cookie_format; int cookie_format; int charset_id; int multibyte_replacement_byte; pool *p; /* if set to 1, the rules from this context will * always be inherited (and cannot be removed) by * the children contexts. The default is 0. */ int inheritance_mandatory; /* This array holds the list of mandatory signatures * inherited from the parent context. */ array_header *inherited_mandatory_signatures; int actions_restricted; }; /* TODO Since we are not allowing any of these to be configured * on the per-virtual host basis we should simply makes * these variables global. */ typedef struct { int server_response_token; char *chroot_dir; int chroot_completed; char *chroot_lock; char *server_signature; char *guardian_log_name; int guardian_log_fd; char *guardian_log_condition; } sec_srv_config; typedef struct { /* part type, can be MULTIPART_FORMDATA or MULTIPART_FILE */ int type; /* the name */ char *name; /* variables only, variable value */ char *value; array_header *value_parts; /* files only, the content type (where available) */ char *content_type; /* files only, the name of the temporary file holding data */ char *tmp_file_name; int tmp_file_fd; unsigned int tmp_file_size; /* files only, filename as supplied by the browser */ char *filename; char *last_header_name; table *headers; } multipart_part; typedef struct modsec_rec modsec_rec; typedef struct { modsec_rec *msr; request_rec *r; sec_dir_config *dcfg; pool *p; /* this array keeps parts */ array_header *parts; /* mime boundary used to detect when * parts end and new begin */ char *boundary; /* internal buffer and other variables * used while parsing */ char buf[MULTIPART_BUF_SIZE + 2]; int buf_contains_line; char *bufptr; int bufleft; /* pointer that keeps track of a part while * it is being built */ multipart_part *mpp; /* part parsing state; 0 means we are reading * headers, 1 means we are collecting data */ int mpp_state; /* because of the way this parsing algorithm * works we hold back the last two bytes of * each data chunk so that we can discard it * later if the next data chunk proves to be * a boundary; the first byte is an indicator * 0 - no content, 1 - two data bytes available */ char reserve[4]; int seen_data; int is_complete; } multipart_data; struct modsec_rec { request_rec *r; char *request_uri; char *_post_payload; char *_fake_post_payload; int should_body_exist; int is_body_read; int is_multipart; int post_payload_dynamic_off; sec_dir_config *dcfg; sec_srv_config *scfg; table *parsed_args; table *parsed_cookies; char *tmp_message; multipart_data *mpd; int external_filter_fd; /* positive if relevant, zero or negative otherwise */ int is_relevant; /* NOT_SET (-1) do nothing, 0 suppress audit logging, 1 force audit logging */ int explicit_auditlog; /* whether or not the current request is dynamic; this field * is NOT_SET by default because sometimes we don't need to * know (and finding out is expensive). Therefore the possible * values are NOT_SET (we don't know), 0, and 1. */ int is_dynamic; int is_enabled; request_body *ctx_in; char *new_auditlog_boundary; char *new_auditlog_filename; int new_auditlog_fd; unsigned int new_auditlog_size; AP_MD5_CTX new_auditlog_md5ctx; array_header *messages; const char *cache_request_uri; const char *cache_path_info; const char *cache_the_request; const char *cache_query_string; const char *cache_request_basename; const char *cache_script_basename; table *cache_headers_in; }; /* -------------- Function prototypes ---------------------------------------- */ static int check_single_signature(modsec_rec *msr, signature *sig); static int _check_single_signature(modsec_rec *msr, signature *sig, char **error_msg); static int check_sig_against_string(modsec_rec *msr, signature *_sig, const char *s, int var_type, char *var_name); static int fd_lock(request_rec *r, int fd); static int fd_unlock(request_rec *r, int fd); static void sec_debug_log(request_rec *r, int level, const char *text, ...); static char *normalise_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg); static char *normalise(request_rec *r, sec_dir_config *dcfg, char *_uri, char **error_msg); static char *normalise_relaxed_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg); static char *normalise_relaxed(request_rec *r, sec_dir_config *dcfg, char *_uri, char **error_msg); static char *normalise_other_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg); static char *normalise_urlencoding_relaxed_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg); static char *normalise_urlencoding_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg); static char *remove_binary_content(request_rec *r, char *data); static int parse_arguments(char *s, table *parsed_args, request_rec *r, sec_dir_config *dcfg, char **error_msg); static char *unescape_regex_hex_inplace(char *input); static int detect_unicode_character(request_rec *r, char *p_read); static unsigned char x2c(unsigned char *what); static int parse_cookies(modsec_rec *msr, char **error_msg); static int parse_cookies_v0(modsec_rec *msr, char *cookie_header, char **error_msg); static int parse_cookies_v1(modsec_rec *msr, char *cookie_header, char **error_msg); static const char *get_env_var(request_rec *r, char *name); static const char *get_variable(modsec_rec *msr, variable *v, int var_type); static char *process_action(char *name, char *value, actionset_t *actionset, pool *_pool); static char *parse_actionset(char *p2, actionset_t *actionset, pool *_pool); static void sec_child_init(server_rec *s, pool *p); static void sec_init(server_rec *s, pool *p); static void sec_set_dir_defaults(sec_dir_config *dcfg); static char *get_temp_folder(pool *p); static char *get_file_basename(pool *p, char *filename); static int sec_copy_file(char *from, char *to); static char *construct_put_filename(modsec_rec *msr); static modsec_rec *sec_create_context(request_rec *r); static int sec_initialise(modsec_rec *msr); static int sec_check_access(request_rec *r); static int sec_check_all_signatures(modsec_rec *msr); static int request_body_file_cleanup(modsec_rec *msr); static int multipart_init(multipart_data *mpd, modsec_rec *msr, char **error_msg); static int multipart_complete(multipart_data *mpd, char **error_msg); static int multipart_cleanup(multipart_data *mpd); static int multipart_process_chunk(multipart_data *mpd, char *buf, int size, char **error_msg); static int multipart_process_part_header(multipart_data *mpd, char **error_msg); static int multipart_process_part_data(multipart_data *mpd, char **error_msg); static int multipart_parse_content_disposition(multipart_data *mpd, char *value); static char *multipart_construct_filename(multipart_data *mpd); static int multipart_process_boundary(multipart_data *mpd, int last_part, char **error_msg); static int verify_uploaded_file(request_rec *r, char *file_path, char *approver_script, char **error_msg); static int multipart_verify_uploaded_files(request_rec *r, multipart_data *mpd, char *approver_script, char **error_msg); static int multipart_get_variables(multipart_data *mpd, table *parsed_args, sec_dir_config *dcfg, char **error_msg); static int multipart_contains_files(multipart_data *mpd); static multipart_part *multipart_get_part(multipart_data *mpd, char *name); static int multipart_check_files_names(modsec_rec *msr, signature *sig, variable *var); static int multipart_check_files_sizes(modsec_rec *msr, signature *sig, variable *var); static int change_server_signature(server_rec *s, sec_srv_config *scfg); static int convert_charset_to_id(char *name); static char *filter_multibyte_inplace(int cs_id, char replacement_byte, char *outbuf); static char *filter_multibyte_other(int charset_id, char replacement_byte, char *inptr); static char *filter_multibyte_unicode(int charset_id, char replacement_byte, char *inptr); static void sec_sleep(int msec_pause); static int sec_mkstemp(char *template); static char *log_escape(pool *p, char *text); static char *log_escape_nq(pool *p, char *text); static char *log_escape_header_name(pool *p, char *text); static char *_log_escape(pool *p, char *text, int escape_quotes, int escape_colon); static void *sec_create_srv_config(pool *p, server_rec *s); static void *sec_merge_srv_config(pool *p, void *_parent, void *_child); static void *sec_create_dir_config(pool *p, char *path); static void *sec_merge_dir_config(struct pool *p, void *_parent, void *_child); static int read_post_payload(modsec_rec *msr, char **p); static int sec_exec_child(void *_ed, child_info *pinfo); static int sec_logger(request_rec *_r); static char *current_logtime(request_rec *r); static char *current_filetime(request_rec *r); static int perform_action(modsec_rec *msr, actionset_t *dcfg_actionset, signature *sig); static char *construct_fake_urlencoded(modsec_rec *msr, table *args); static int sec_table_count(table *_table); static int is_response_status_relevant(request_rec *r, sec_dir_config *dcfg, int status); static char *construct_rule_metadata(modsec_rec *msr, actionset_t *actionset, signature *sig); static void sec_guardian_logger(request_rec *r, request_rec *origr, modsec_rec *msr); static int sec_audit_logger_serial(request_rec *r, request_rec *origr, sec_dir_config *dcfg, modsec_rec *msr); static void sec_audit_logger_concurrent(request_rec *r, request_rec *origr, sec_dir_config *dcfg, modsec_rec *msr); static void sec_auditlog_init(modsec_rec *msr); static int sec_auditlog_write(modsec_rec *msr, char *data, unsigned int len); static void store_msr(request_rec *r, modsec_rec *msr); static modsec_rec *find_msr(request_rec *r); static char *construct_auditlog_filename(request_rec *r, char *uniqueid); static char *create_auditlog_boundary(request_rec *r); static char *bytes2hex(pool *p, unsigned char *data, int len); static int sec_dir_make_recursive(char *filename, int mode, request_rec *r); static actionset_t *merge_actionsets(pool *p, actionset_t *parent, actionset_t *child, int actions_restricted); static int sec_remove_lf_crlf_inplace(char *text); static char *ap_pstrmemdup(pool *p, char *start, unsigned int len); static void *my_pregcomp(pool *p, char *pattern); static int my_regexec(void *preg, const char *s); static void my_call_exec(request_rec *r, child_info *pinfo, char *command, char **env); static int is_token_char(char c); static int is_valid_parts_specification(char *p); static const char *get_response_protocol(request_rec *r); static int parse_severity(char *input); static void init_empty_actionset(actionset_t *actionset); static void init_default_actionset(actionset_t *actionset); /* ---------------------------------------------------------------------------- */ static int my_index_of_response(int status) { static int shortcut[6] = { 0, MY_LEVEL_200, MY_LEVEL_300, MY_LEVEL_400, MY_LEVEL_500, MY_RESPONSE_CODES }; int i, pos; if (status < 100) return MY_LEVEL_500; for (i = 0; i < 5; i++) { status -= 100; if (status < 100) { pos = (status + shortcut[i]); if (pos < shortcut[i + 1]) { return pos; } else { return MY_LEVEL_500; } } } return MY_LEVEL_500; } void init_empty_actionset(actionset_t *actionset) { memset(actionset, 0, sizeof(actionset_t)); actionset->action = NOT_SET; actionset->log = NOT_SET; actionset->auditlog = NOT_SET; actionset->status = NOT_SET; actionset->pause = NOT_SET; actionset->exec = NOT_SET; actionset->id = NULL; actionset->rev = NULL; actionset->msg = NULL; actionset->severity = NOT_SET; actionset->skip_count = 1; } void init_default_actionset(actionset_t *actionset) { memset(actionset, 0, sizeof(actionset_t)); actionset->log = 1; actionset->action = ACTION_DENY; actionset->status = HTTP_FORBIDDEN; actionset->auditlog = NOT_SET; } int parse_severity(char *input) { int i = 0; if ((input[0] >= '0')&&(input[0] <= '7')&&(input[1] == '\0')) { return atoi(input); } i = 0; while(severities[i] != NULL) { if (strcmp(severities[i], input) == 0) { return i; } i++; } return -1; } int is_valid_parts_specification(char *p) { char c, *t = p; while((c = *t++) != '\0') { if ((c != AUDITLOG_PART_ENDMARKER)&&((c < AUDITLOG_PART_FIRST)||(c > AUDITLOG_PART_LAST))) { return 0; } } return 1; } int is_token_char(char c) { /* CTLs not allowed */ if ((c <= 32)||(c >= 127)) return 0; switch(c) { case '(' : case ')' : case '<' : case '>' : case '@' : case ',' : case ';' : case ':' : case '\\' : case '"' : case '/' : case '[' : case ']' : case '?' : case '=' : return 0; } return 1; } #if defined(DISABLE_SUEXEC) void my_call_exec(request_rec *r, child_info *pinfo, char *command, char **env) { const char *args[5]; /* TODO this only supports one argument but that's * all we need for now */ args[0] = command; args[1] = r->args; args[2] = NULL; execve(command, (char ** const)&args, env); } #else void my_call_exec(request_rec *r, child_info *pinfo, char *command, char **env) { ap_call_exec(r, pinfo, command, env, 0); } #endif #if defined(USE_PCRE) static void my_pcre_cleanup(void *preg) { if (preg != NULL) free(preg); } void *my_pregcomp(pool *p, char *pattern) { const char *errptr = NULL; int erroffset; void *preg; preg = pcre_compile(pattern, PCRE_CASELESS, &errptr, &erroffset, NULL); if (preg == NULL) return NULL; ap_register_cleanup(p, (void *)preg, (void (*)(void *))my_pcre_cleanup, ap_null_cleanup); return preg; } int my_regexec(void *preg, const char *s) { int rc; rc = pcre_exec(preg, NULL, s, strlen(s), 0, 0, NULL, 0); if (rc == PCRE_ERROR_NOMATCH) return REG_NOMATCH; else return 0; } #else void *my_pregcomp(pool *p, char *pattern) { return ap_pregcomp(p, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB); } int my_regexec(void *preg, const char *s) { return ap_regexec(preg, s, 0, NULL, 0); } #endif static char *ap_pstrmemdup(pool *p, char *data, unsigned int len) { char *mem = ap_palloc(p, len + 1); if (data == NULL) return NULL; memcpy(mem, data, len); mem[len] = '\0'; return mem; } int sec_remove_lf_crlf_inplace(char *text) { char *p = text; int count = 0; if (text == NULL) return -1; while(*p != '\0') { count++; p++; } if (count > 0) { if (*(p - 1) == '\n') { *(p - 1) = '\0'; if (count > 1) { if (*(p - 2) == '\r') *(p - 2) = '\0'; } } } return 1; } int sec_copy_file(char *from, char *to) { char buf[1025]; int from_fd, to_fd; int i; from_fd = open(from, O_RDONLY); if (from_fd < 0) return -1; to_fd = open(to, O_CREAT | O_EXCL | O_WRONLY, CREATEMODE); if (to_fd < 0) { close(from_fd); return -1; } do { i = read(from_fd, buf, 1024); if (i <= 0) { if ((i == 0)||(i == EINTR)) continue; else { close(from_fd); close(to_fd); return -1; } } else { if (write(to_fd, buf, i) != i) { close(from_fd); close(to_fd); /* TODO remove target file */ return -1; } } } while(i != 0); close(from_fd); close(to_fd); return 1; } int sec_dir_make_recursive(char *filename, int mode, request_rec *r) { struct stat buf; char *f, *t; int rc; sec_debug_log(r, 9, "sec_dir_make_recursive: %s", filename); f = ap_pstrdup(r->pool, filename); /* replace backslashes with forward slashes */ while((t = strchr(f, '\\')) != NULL) { *t = '/'; } /* if the file/directory already exists we won't have much to do */ if (stat(filename, &buf) == 0) { /* the file exists - is it a directory? */ if (S_ISDIR(buf.st_mode) == 0) return -1; /* it is, we return OK */ sec_debug_log(r, 9, "sec_dir_make_recursive: directory exists: %s", filename); return 0; } /* remove the last part of the path */ t = strrchr(f, '/'); if (t == NULL) return -1; *t = '\0'; /* create parent directories (recursively) */ if (sec_dir_make_recursive(f, mode, r) < 0) return -1; /* finally, create this directory */ #ifdef WIN32 rc = mkdir(filename); #else rc = mkdir(filename, mode); #endif sec_debug_log(r, 9, "sec_dir_make_recursive: mkdir: %s (rc %i)", filename, rc); return rc; } /** * Creates a random 8-character string that * consists of hexadecimal numbers, to be used * as an audit log boundary. */ char *create_auditlog_boundary(request_rec *r) { unsigned long data = rand(); return bytes2hex(r->pool, (unsigned char *)&data, 4); } /** * Converts a series of bytes into its hexadecimal * representation. */ char *bytes2hex(pool *p, unsigned char *data, int len) { static unsigned char b2hex[] = "0123456789abcdef"; char *hex = ap_palloc(p, (len * 2) + 1); int i, j; if (hex == NULL) return NULL; j = 0; for(i = 0; i < len; i++) { hex[j++] = b2hex[data[i] >> 4]; hex[j++] = b2hex[data[i] & 0x0f]; } hex[j] = 0; return hex; } /** * Converts a byte given as its hexadecimal representation * into a proper byte. Does not check for overflows. */ unsigned char x2c(unsigned char *what) { register unsigned char digit; digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A') + 10 : (what[0] - '0')); digit *= 16; digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A') + 10 : (what[1] - '0')); return(digit); } /** * Converts a single byte into its hexadecimal representation. * Will overwrite two bytes at the destination. */ static unsigned char *c2x(unsigned what, unsigned char *where) { static const char c2x_table[] = "0123456789abcdef"; what = what & 0xff; *where++ = c2x_table[what >> 4]; *where++ = c2x_table[what & 0x0f]; return where; } int sec_table_count(table *_table) { const array_header *arr = NULL; table_entry *te = NULL; int i, count = 0; arr = ap_table_elts(_table); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { count++; } return count; } char *construct_fake_urlencoded(modsec_rec *msr, table *args) { table_entry *te; array_header *arr; int k; char *body; unsigned int body_len; if (args == NULL) return NULL; /* calculate buffer size */ body_len = 1; arr = ap_table_elts(args); te = (table_entry *)arr->elts; for(k = 0; k < arr->nelts; k++) { body_len += 4; body_len += strlen(te[k].key); body_len += strlen(te[k].val); } /* allocate the buffer */ body = ap_palloc(msr->r->pool, body_len + 1); if ((body == NULL)||(body_len + 1 == 0)) return NULL; *body = 0; /* loop through the variables * and create a single string out of them */ arr = ap_table_elts(args); te = (table_entry *)arr->elts; for(k = 0; k < arr->nelts; k++) { if (*body != 0) { strncat(body, "&", body_len - strlen(body)); } strncat(body, te[k].key, body_len - strlen(body)); strncat(body, "=", body_len - strlen(body)); strncat(body, te[k].val, body_len - strlen(body)); } return body; } char *construct_rule_metadata(modsec_rec *msr, actionset_t *_actionset, signature *sig) { char *id = "", *rev = "", *msg = "", *severity = ""; actionset_t *actionset = _actionset; /* If we were called because of a match in a rule * that is part of a chain, look up the first rule * in the chain to find the meta data. */ if ((sig != NULL) &&(sig->first_sig_in_chain != NULL) &&(sig->first_sig_in_chain->actionset != NULL) ) { actionset = sig->first_sig_in_chain->actionset; } if (actionset->id != NULL) id = ap_psprintf(msr->r->pool, " [id \"%s\"]", log_escape(msr->r->pool, actionset->id)); if (actionset->rev != NULL) rev = ap_psprintf(msr->r->pool, "[rev \"%s\"]", log_escape(msr->r->pool, actionset->rev)); if (actionset->msg != NULL) msg = ap_psprintf(msr->r->pool, " [msg \"%s\"]", log_escape(msr->r->pool, actionset->msg)); if ((actionset->severity >= 0)&&(actionset->severity <= 7)) severity = ap_psprintf(msr->r->pool, " [severity \"%s\"]", severities[actionset->severity]); return ap_pstrcat(msr->r->pool, id, rev, msg, severity, NULL); } actionset_t *merge_actionsets(pool *p, actionset_t *parent, actionset_t *child, int actions_restricted) { actionset_t *actionset = ap_pcalloc(p, sizeof(actionset_t)); if (actionset == NULL) return NULL; /* start with the parent */ memcpy(actionset, parent, sizeof(actionset_t)); /* these actions are always allowed, and can only be set per-rule */ if (child->id != NULL) actionset->id = child->id; if (child->rev != NULL) actionset->rev = child->rev; if (child->msg != NULL) actionset->msg = child->msg; if (child->severity != NOT_SET) actionset->severity = child->severity; if (child->action == ACTION_SKIP) { actionset->action = ACTION_SKIP; actionset->skip_count = child->skip_count; } actionset->is_chained = child->is_chained; /* these actions can be restricted */ if (actions_restricted == 0) { if (child->note_name != NULL) { actionset->note_name = child->note_name; actionset->note_value = child->note_value; } if (child->env_name != NULL) { actionset->env_name = child->env_name; actionset->env_value = child->env_value; } if (child->mandatory) actionset->mandatory = child->mandatory; if (child->log != NOT_SET) actionset->log = child->log; if (child->auditlog != NOT_SET) actionset->auditlog = child->auditlog; if (child->status != NOT_SET) actionset->status = child->status; if (child->pause != NOT_SET) actionset->pause = child->pause; if (child->exec != NOT_SET) { actionset->exec = child->exec; actionset->exec_string = child->exec_string; } if (child->redirect_url != NULL) actionset->redirect_url = child->redirect_url; if (child->proxy_url != NULL) actionset->proxy_url = child->proxy_url; if (child->action != NOT_SET) actionset->action = child->action; if (child->logparts != NOT_SET) { actionset->logparts = child->logparts; actionset->logparts_value = child->logparts_value; } } /* Chained rules must always try to deny * access in order for chaining to work * properly */ if (actionset->is_chained) { actionset->action = ACTION_DENY; actionset->status = HTTP_FORBIDDEN; } return actionset; } int perform_action(modsec_rec *msr, actionset_t *dcfg_actionset, signature *sig) { actionset_t *actionset = dcfg_actionset; char *message = NULL; request_rec *r = msr->r; int log_level = 1; int is_chained = 0; int rc = OK; /* Use the per-signature actionset if available */ if ((sig != NULL)&&(sig->actionset != NULL)) { actionset = sig->actionset; } if (msr->tmp_message == NULL) { msr->tmp_message = "Unknown error"; } /* is audit logging explicitly configured? */ if (actionset->auditlog != NOT_SET) { sec_debug_log(r, 9, "perform_action: set explicit_auditlog to %i", actionset->auditlog); msr->explicit_auditlog = actionset->auditlog; } if (actionset->log == 0) { /* only change the audit logging setting if it is not set already */ if (msr->explicit_auditlog == NOT_SET) { sec_debug_log(r, 9, "perform_action: set explicit_auditlog to 0"); msr->explicit_auditlog = 0; } log_level = 2; } if (actionset->env_name != NULL) { if (actionset->env_name[0] == '!') { /* delete existing variable */ ap_table_unset(msr->r->subprocess_env, actionset->env_name + 1); } else { /* create new variable */ ap_table_set(msr->r->subprocess_env, actionset->env_name, actionset->env_value); } } if (actionset->note_name != NULL) { if (actionset->note_name[0] == '!') { /* delete existing note */ ap_table_unset(msr->r->notes, actionset->note_name + 1); } else { /* create new note */ ap_table_set(msr->r->notes, actionset->note_name, actionset->note_value); } } /* perform action */ switch(actionset->action) { case ACTION_SKIP : message = ap_psprintf(r->pool, "Skipping %i statements. %s%s", actionset->skip_count, msr->tmp_message, construct_rule_metadata(msr, actionset, sig)); rc = MODSEC_SKIP; break; case ACTION_ALLOW : message = ap_psprintf(r->pool, "Access allowed. %s%s", msr->tmp_message, construct_rule_metadata(msr, actionset, sig)); rc = MODSEC_ALLOW; break; case ACTION_DENY : rc = actionset->status; if (actionset->is_chained) { is_chained = 1; message = ap_psprintf(r->pool, "Warning (chained rule). %s%s", msr->tmp_message, construct_rule_metadata(msr, actionset, sig)); } else { message = ap_psprintf(r->pool, "Access denied with code %i. %s%s", rc, msr->tmp_message, construct_rule_metadata(msr, actionset, sig)); } break; case ACTION_REDIRECT : message = ap_psprintf(r->pool, "Access denied with redirect to [%s]. %s%s", actionset->redirect_url, msr->tmp_message, construct_rule_metadata(msr, actionset, sig)); ap_table_setn(r->headers_out, "Location", actionset->redirect_url); /* rc = HTTP_MOVED_TEMPORARILY; */ rc = REDIRECT; break; case ACTION_PROXY : if (ap_find_linked_module("mod_proxy.c") == NULL) { sec_debug_log(r, 1, "Proxy action requsted but mod_proxy not found [%s].", actionset->proxy_url); } else { r->filename = ap_psprintf(r->pool, "proxy:%s", actionset->proxy_url); r->proxyreq = PROXY_PASS; r->handler = "proxy-server"; rc = OK; } break; case ACTION_NONE : default : message = ap_psprintf(r->pool, "Warning. %s%s", msr->tmp_message, construct_rule_metadata(msr, actionset, sig)); rc = OK; break; } if (is_chained == 0) { sec_debug_log(r, log_level, "%s", message); ap_table_addn(r->headers_in, NOTE_MESSAGE, message); *(char **)ap_push_array(msr->messages) = message; msr->is_relevant++; } else { sec_debug_log(r, 3, "%s", message); } if ((rc != OK)&&(rc != MODSEC_ALLOW)&&(rc != MODSEC_SKIP)) { char *action = ap_psprintf(msr->r->pool, "%i", rc); ap_table_setn(r->headers_in, NOTE_ACTION, action); } /* execute the external script */ if (actionset->exec) { exec_data *ed = NULL; BUFF *p1, *p2, *p3; char buf[4097]; ed = ap_pcalloc(r->pool, sizeof(exec_data)); ed->r = r; ed->command = actionset->exec_string; ed->args = NULL; sec_debug_log(r, 1, "Executing command \"%s\"", log_escape(msr->r->pool, ed->command)); ap_table_setn(r->headers_in, NOTE_EXECUTED, ed->command); if (!ap_bspawn_child(r->main ? r->main->pool : r->pool, sec_exec_child, (void *)ed, kill_after_timeout, &p1, &p2, &p3)) { ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r->server, "mod_security: couldn't spawn child process: %s", actionset->exec_string); return HTTP_INTERNAL_SERVER_ERROR; } while(ap_bgets(buf, 4096, p2) > 0) continue; while(ap_bgets(buf, 4096, p3) > 0) continue; } if (actionset->pause != 0) { sec_debug_log(r, log_level, "Pausing \"%s\" for %i ms", log_escape(r->pool, r->uri), actionset->pause); sec_sleep(actionset->pause); } /* update audit log parts */ if (actionset->logparts != 0) { if (actionset->logparts == ABSOLUTE_VALUE) { msr->dcfg->auditlog_parts = actionset->logparts_value; } else if (actionset->logparts == RELATIVE_VALUE_POSITIVE) { msr->dcfg->auditlog_parts = ap_pstrcat(r->pool, msr->dcfg->auditlog_parts, actionset->logparts_value, NULL); } else if (actionset->logparts == RELATIVE_VALUE_NEGATIVE) { char c, *t = actionset->logparts_value; while((c = *t++) != '\0') { char *s = msr->dcfg->auditlog_parts; char *d = msr->dcfg->auditlog_parts; while(*s != '\0') { if (*s != c) { *d++ = *s++; } else { s++; } } *d = '\0'; } } sec_debug_log(r, 4, "Using new value for audit log parts: %s", msr->dcfg->auditlog_parts); } msr->tmp_message = NULL; return rc; } int sec_mkstemp(char *template) { #if !(defined(WIN32)||defined(NETWARE)) return mkstemp(template); #else if (mktemp(template) == NULL) return -1; return open(template, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); #endif } #ifdef WIN32 char *strtok_r(char *s, const char *sep, char **lasts) { char *sbegin, *send; /* These two parameters must always be valid */ if ((sep == NULL)||(lasts == NULL)) return NULL; /* Either of the following two must not be NULL */ if ((s == NULL)&&(*lasts == NULL)) return NULL; sbegin = s ? s : *lasts; /* Advance through the separator at the beginning */ sbegin += strspn(sbegin, sep); if (*sbegin == '\0') { *lasts = NULL; return NULL; } /* Find the next separator */ send = strpbrk(sbegin, sep); if (send != NULL) *send++ = 0; *lasts = send; return sbegin; } #endif void sec_sleep(int msec_pause) { if (msec_pause <= 0) return; #ifdef WIN32 _sleep(msec_pause); #elif defined NETWARE delay(msec_pause); #else usleep(msec_pause * 1000); #endif } /* * Change the signature of the server if change * was requested in configuration. We do this by * locating the signature in server memory and * writing over it. */ int change_server_signature(server_rec *s, sec_srv_config *scfg) { char *server_version; if (scfg->server_signature == NULL) return 0; server_version = (char *)ap_get_server_version(); if (server_version == NULL) { ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, s, "SecServerSignature: ap_get_server_version returned NULL"); return -1; } if (strlen(server_version) >= strlen(scfg->server_signature)) { strcpy(server_version, scfg->server_signature); return 1; } else { ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, s, "SecServerSignature: the existing signature is too short. Please set ServerTokens to Full"); return -1; } } int convert_charset_to_id(char *name) { if (strcasecmp(name, "utf-8") == 0) return UNI3_CSID; if (strcasecmp(name, "shift-jis") == 0) return SJIS1_CSID; if (strcasecmp(name, "shift_jis") == 0) return SJIS2_CSID; if (strcasecmp(name, "big5") == 0) return BIG5_CSID; if (strcasecmp(name, "gbk") == 0) return GBK_CSID; if (strcasecmp(name, "gb2312") == 0) return GB2312_CSID; if (strcasecmp(name, "euc-tw") == 0) return ZHT32EUC_CSID; if (strcasecmp(name, "euc-jp") == 0) return JEUC1_CSID; if (strcasecmp(name, "eucjis") == 0) return JEUC2_CSID; if (strcasecmp(name, "jis0208") == 0) return JA16VMS_CSID; return -1; } char *filter_multibyte_unicode(int charset_id, char replacement_byte, char *inptr) { char *outptr = inptr; int i, j, k, n; i = strlen(inptr); j = 0; /* Unicode */ while(j < i) { k = inptr[j] & 0xFF; if (k < 0x80) { j++; *outptr++ = (char)k; } else if (k < 0xC0) { j++; *outptr++ = replacement_byte; } else { if (k < 0xE0) n = 2; else if (k < 0xF0) n = 3; else if (k < 0xF8) n = 4; else if (k < 0xFC) n = 5; else if (k < 0xFE) n = 6; else n = 1; if (i - j >= n) { j += n; } else { i = j; } *outptr++ = replacement_byte; } } *outptr = 0; return inptr; } char *filter_multibyte_other(int charset_id, char replacement_byte, char *inptr) { char *outptr = inptr; int i, j, k, n; i = strlen(inptr); j = 0; while(j < i) { k = inptr[j] & 0xFF; if (k < 0x80) { j++; *outptr++ = (char)k; } else { n = 2; if ((k == 0x8E)&&(charset_id == ZHT32EUC_CSID)) { n = 4; } else if ((k == 0x8F)&&((charset_id == JEUC1_CSID)||(charset_id == JEUC2_CSID))) { n = 3; } else if ( ((k == 0x80)||(k == 0xFF)) && ((charset_id == BIG5_CSID)||(charset_id == GBK_CSID)||(charset_id == GB2312_CSID)) ) { n = 1; } else if ( ((k == 0x80)||((k >= 0xA0) && (k < 0xE0))) && ((charset_id == SJIS1_CSID)||(charset_id == SJIS2_CSID)) ) { n = 1; } if (i - j >= n) { j += n; } else { i = j; } *outptr++ = (n == 1) ? (char)k : replacement_byte; } } *outptr = 0; return inptr; } char *filter_multibyte_inplace(int charset_id, char replacement_byte, char *inptr) { if (charset_id < MB_CSID) return inptr; /* not multibyte charset */ if (charset_id == UNI3_CSID) return filter_multibyte_unicode(charset_id, replacement_byte, inptr); else return filter_multibyte_other(charset_id, replacement_byte, inptr); } int parse_cookies(modsec_rec *msr, char **error_msg) { char *header = NULL, *header_copy = NULL; if (error_msg == NULL) return -1; *error_msg = NULL; header = (char *)ap_table_get(msr->r->headers_in, "Cookie"); /* Return here if no cookies found */ if (header == NULL) return 0; header_copy = ap_pstrdup(msr->r->pool, header); if (header_copy == NULL) return -1; sec_debug_log(msr->r, 6, "Raw cookie header \"%s\"", log_escape(msr->r->pool, header)); if (msr->dcfg->cookie_format == COOKIES_V0) return parse_cookies_v0(msr, header_copy, error_msg); else if (msr->dcfg->cookie_format == COOKIES_V1) return parse_cookies_v1(msr, header_copy, error_msg); else { *error_msg = ap_psprintf(msr->r->pool, "Unknown cookie format: %i", msr->dcfg->cookie_format); return -1; } } int parse_cookies_v0(modsec_rec *msr, char *cookie_header, char **error_msg) { request_rec *r = msr->r; sec_dir_config *dcfg = msr->dcfg; char *attr_name = NULL, *attr_value = NULL; char *saveptr = NULL; int cookie_count = 0; char *p = NULL; p = strtok_r(cookie_header, ";", &saveptr); while(p != NULL) { attr_name = NULL; attr_value = NULL; /* ignore whitespace at the beginning of cookie name */ while(isspace(*p)) p++; attr_name = p; attr_value = strstr(p, "="); if (attr_value != NULL) { /* terminate cookie name */ *attr_value = 0; /* move over to the beginning of the value */ attr_value++; } if (dcfg->normalize_cookies) { char *my_error_msg = NULL; if (attr_name != NULL) { if (normalise_inplace(r, dcfg, attr_name, &my_error_msg) == NULL) { *error_msg = ap_psprintf(r->pool, "Error normalising cookie name: %s", my_error_msg); return -1; } } if (attr_value != NULL) { if (normalise_inplace(r, dcfg, attr_value, &my_error_msg) == NULL) { *error_msg = ap_psprintf(r->pool, "Error normalising cookie value: %s", my_error_msg); return -1; } } } /* we ignore cookies with empty names */ if ((attr_name != NULL)&&(strlen(attr_name) != 0)) { if (attr_value != NULL) { sec_debug_log(r, 4, "Adding cookie \"%s\"=\"%s\"", log_escape(r->pool, attr_name), log_escape(r->pool, attr_value)); ap_table_add(msr->parsed_cookies, attr_name, attr_value); } else { sec_debug_log(r, 4, "Adding cookie \"%s\" (empty)", log_escape(r->pool, attr_name)); ap_table_add(msr->parsed_cookies, attr_name, ""); } cookie_count++; } p = strtok_r(NULL, ";", &saveptr); } return cookie_count; } int parse_cookies_v1(modsec_rec *msr, char *cookie_header, char **error_msg) { request_rec *r = msr->r; sec_dir_config *dcfg = msr->dcfg; char *attr_name = NULL, *attr_value = NULL, *p = NULL; char *prev_attr_name = NULL; int cookie_count = 0; p = cookie_header; while(*p != 0) { attr_name = NULL; attr_value = NULL; /* attribute name */ /* remove space from the beginning */ while((isspace(*p))&&(*p != 0)) p++; attr_name = p; while((*p != 0)&&(*p != '=')&&(*p != ';')&&(*p != ',')) p++; /* if we've reached the end of string */ if (*p == 0) goto add_cookie; /* if there is no cookie value supplied */ if ((*p == ';')||(*p == ',')) { *p++ = 0; /* terminate the name */ goto add_cookie; } /* terminate the attribute name, * writing over the = character */ *p++ = 0; /* attribute value */ /* skip over the whitespace at the beginning */ while((isspace(*p))&&(*p != 0)) p++; /* no value supplied */ if (*p == 0) goto add_cookie; if (*p == '"') { if (*++p == 0) goto add_cookie; attr_value = p; while((*p != 0)&&(*p != '"')) p++; if (*p != 0) *p++ = 0; else { /* TODO invalid cookie format, missing " at the end * I don't think this is very relevant though */ } } else { attr_value = p; while((*p != 0)&&(*p != ',')&&(*p != ';')) p++; if (*p != 0) *p++ = 0; /* remove the whitespace from the end of cookie value */ if (attr_value != NULL) { char *t = attr_value; int i = 0; while(*t != 0) { t++; i++; } while((i-- > 0)&&(isspace(*(--t)))) *t = 0; } } add_cookie: /* remove the whitespace from the end of cookie name */ if (attr_name != NULL) { char *t = attr_name; int i = 0; while(*t != 0) { t++; i++; } while((i-- > 0)&&(isspace(*(--t)))) *t = 0; } /* perform cookie name & value normalization if requested */ if (dcfg->normalize_cookies) { char *my_error_msg = NULL; if (attr_name != NULL) { if (normalise_inplace(r, dcfg, attr_name, &my_error_msg) == NULL) { *error_msg = ap_psprintf(r->pool, "Error normalising cookie name: %s", my_error_msg); return -1; } } if (attr_value != NULL) { if (normalise_inplace(r, dcfg, attr_value, &my_error_msg) == NULL) { *error_msg = ap_psprintf(r->pool, "Error normalising cookie value: %s", my_error_msg); return -1; } } } /* add the cookie to the list now */ if ((attr_name != NULL)&&(strlen(attr_name) != 0)) { /* handle special attribute names */ if (attr_name[0] == '$') { if (prev_attr_name != NULL) { /* cookie keyword, we change the name we use * so they can have a unique name in the cookie table */ attr_name = ap_psprintf(r->pool, "$%s_%s", prev_attr_name, attr_name + 1); } } if (attr_value != NULL) { sec_debug_log(r, 4, "Adding cookie \"%s\"=\"%s\"", log_escape(r->pool, attr_name), log_escape(r->pool, attr_value)); ap_table_add(msr->parsed_cookies, attr_name, attr_value); } else { sec_debug_log(r, 4, "Adding cookie \"%s\" (empty)", log_escape(r->pool, attr_name)); ap_table_add(msr->parsed_cookies, attr_name, ""); } cookie_count++; /* only keep the cookie names for later */ if (attr_name[0] != '$') prev_attr_name = attr_name; } /* at this point the *p is either 0 (in which case we exit), or * right after the current cookie ended - we need to look for * the next cookie */ while( (*p != 0)&&( (*p == ',')||(*p == ';')||(isspace(*p)) ) ) p++; } return cookie_count; } const char *get_env_var(request_rec *r, char *name) { const char *result = ap_table_get(r->notes, name); if (result == NULL) { result = ap_table_get(r->subprocess_env, name); } if (result == NULL) { result = getenv(name); } return result; } const char *get_variable(modsec_rec *msr, variable *v, int var_type) { request_rec *r = msr->r; sec_dir_config *dcfg_proper = msr->dcfg; sec_dir_config *dcfg = (sec_dir_config *)ap_pcalloc(r->pool, sizeof(sec_dir_config)); char *my_error_msg = NULL; const char *result = NULL; struct tm *tm; time_t tc; memcpy(dcfg, dcfg_proper, sizeof(sec_dir_config)); dcfg->check_encoding = 0; dcfg->check_unicode_encoding = 0; dcfg->check_cookie_format = 0; dcfg->range_start = 0; dcfg->range_end = 255; switch (var_type) { case VAR_ARG : /* we don't normalise parameter values becaue * they are stored normalised */ result = ap_table_get(msr->parsed_args, v->name); break; case VAR_HEADER : result = ap_table_get(msr->cache_headers_in, v->name); break; case VAR_ENV : result = ap_table_get(r->notes, v->name); if (result == NULL) { result = ap_table_get(r->subprocess_env, v->name); } if (result == NULL) { result = getenv(v->name); } break; case VAR_REMOTE_ADDR : result = r->connection->remote_ip; break; case VAR_REMOTE_HOST : result = ap_get_remote_host(r->connection, r->per_dir_config, REMOTE_NAME); break; case VAR_REMOTE_USER : result = r->connection->user; break; case VAR_REMOTE_IDENT : result = ap_get_remote_logname(r); break; case VAR_REQUEST_METHOD : result = r->method; break; case VAR_REQUEST_URI : if (msr->cache_request_uri != NULL) result = msr->cache_request_uri; else { result = r->unparsed_uri; if (result != NULL) { result = normalise(r, dcfg, (char *)result, &my_error_msg); msr->cache_request_uri = result; } } break; case VAR_AUTH_TYPE : result = r->connection->ap_auth_type; break; case VAR_IS_SUBREQ : result = (r->main != NULL ? "true" : "false"); break; case VAR_DOCUMENT_ROOT : result = ap_document_root(r); break; case VAR_SERVER_ADMIN : result = r->server->server_admin; break; case VAR_SERVER_NAME : result = ap_get_server_name(r); break; case VAR_SERVER_ADDR : result = r->connection->local_ip; break; case VAR_SERVER_PORT : result = ap_psprintf(r->pool, "%i", (int)ap_get_server_port(r)); break; case VAR_SERVER_PROTOCOL : result = r->protocol; break; case VAR_SERVER_SOFTWARE : result = ap_get_server_version(); break; case VAR_API_VERSION : result = ap_psprintf(r->pool, "%d:%d", MODULE_MAGIC_NUMBER_MAJOR, MODULE_MAGIC_NUMBER_MINOR); break; case VAR_TIME_YEAR : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%02d%02d", (tm->tm_year / 100) + 19, tm->tm_year % 100); break; case VAR_TIME : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%02d%02d%02d%02d%02d%02d%02d", (tm->tm_year / 100) + 19, (tm->tm_year % 100), tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); break; case VAR_TIME_WDAY : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%d", tm->tm_wday); break; case VAR_TIME_SEC : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%02d", tm->tm_sec); break; case VAR_TIME_MIN : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%02d", tm->tm_min); break; case VAR_TIME_HOUR : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%02d", tm->tm_hour); break; case VAR_TIME_MON : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%02d", tm->tm_mon + 1); break; case VAR_TIME_DAY : tc = time(NULL); tm = localtime(&tc); result = ap_psprintf(r->pool, "%02d", tm->tm_mday); break; case VAR_SCRIPT_FILENAME : case VAR_REQUEST_FILENAME : result = r->filename; break; case VAR_PATH_INFO : if (msr->cache_path_info != NULL) result = msr->cache_path_info; else { result = r->path_info; if (result != NULL) { result = normalise(r, dcfg, (char *)result, &my_error_msg); msr->cache_path_info = result; } } break; case VAR_THE_REQUEST : if (msr->cache_the_request != NULL) result = msr->cache_the_request; else { result = r->the_request; if (result != NULL) { result = normalise(r, dcfg, (char *)result, &my_error_msg); msr->cache_the_request = result; } } break; case VAR_QUERY_STRING : if (msr->cache_query_string != NULL) result = msr->cache_query_string; else { result = r->args; if (result != NULL) { result = normalise(r, dcfg, (char *)result, &my_error_msg); msr->cache_query_string = result; } } break; case VAR_HANDLER : result = r->handler; break; case VAR_COOKIE : /* cookies were escaped earlier */ result = ap_table_get(msr->parsed_cookies, v->name); break; #ifdef ENABLE_VAR_SCRIPT_VARIABLES case VAR_SCRIPT_UID : result = ap_psprintf(r->pool, "%i", r->finfo.st_uid); break; case VAR_SCRIPT_GID : result = ap_psprintf(r->pool, "%i", r->finfo.st_gid); break; case VAR_SCRIPT_USERNAME : { struct passwd *userdata = getpwuid(r->finfo.st_uid); if (userdata != NULL) { result = ap_pstrdup(r->pool, userdata->pw_name); } } break; case VAR_SCRIPT_GROUPNAME : { struct group *groupdata = getgrgid(r->finfo.st_gid); if (groupdata != NULL) { result = ap_pstrdup(r->pool, groupdata->gr_name); } } break; case VAR_SCRIPT_MODE : result = ap_psprintf(r->pool, "%04o", r->finfo.st_mode & 07777); break; #endif case VAR_HEADERS_COUNT : result = ap_psprintf(r->pool, "%i", sec_table_count(msr->cache_headers_in)); break; case VAR_FILES_COUNT : if (msr->mpd != NULL) result = ap_psprintf(r->pool, "%i", multipart_contains_files(msr->mpd)); else result = "0"; break; case VAR_ARGS_COUNT : result = ap_psprintf(r->pool, "%i", sec_table_count(msr->parsed_args)); break; case VAR_COOKIES_COUNT : result = ap_psprintf(r->pool, "%i", sec_table_count(msr->parsed_cookies)); break; case VAR_FILE_NAME : if (v->name == NULL) { sec_debug_log(r, 1, "get_variable: Variable FILE_NAME requires name"); } else if (msr->mpd != NULL) { multipart_part *part = multipart_get_part(msr->mpd, v->name); if ((part != NULL)&&(part->type == MULTIPART_FILE)&&(part->filename != NULL)) result = ap_pstrdup(r->pool, part->filename); } break; case VAR_FILE_SIZE : if (v->name == NULL) { sec_debug_log(r, 1, "get_variable: Variable FILE_SIZE requires name"); } else if (msr->mpd != NULL) { multipart_part *part = multipart_get_part(msr->mpd, v->name); if ((part != NULL)&&(part->type == MULTIPART_FILE)&&(part->filename != NULL)) result = ap_psprintf(r->pool, "%u", part->tmp_file_size); } break; case VAR_REQUEST_BASENAME : if (msr->cache_request_basename == NULL) { char *p = NULL, *path = r->parsed_uri.path; if (path != NULL) { p = strrchr(path, '/'); if (p != NULL) path = p + 1; p = strrchr(path, '\\'); if (p != NULL) path = p + 1; msr->cache_request_basename = normalise(r, dcfg, (char *)path, &my_error_msg); } } result = msr->cache_request_basename; break; case VAR_SCRIPT_BASENAME : if (msr->cache_script_basename == NULL) { char *p = NULL, *path = r->filename; if (path != NULL) { p = strrchr(path, '/'); if (p != NULL) path = p + 1; p = strrchr(path, '\\'); if (p != NULL) path = p + 1; msr->cache_script_basename = normalise(r, dcfg, (char *)path, &my_error_msg); } } result = msr->cache_script_basename; break; default : sec_debug_log(r, 1, "get_variable: unresolved variable type %i (internal error)", var_type); break; } if (result == NULL) { result = ""; } return result; } void *sec_create_srv_config(pool *p, server_rec *s) { sec_srv_config *scfg = (sec_srv_config *)ap_pcalloc(p, sizeof(sec_srv_config)); if (scfg == NULL) return NULL; #ifdef CONF_DEBUG fprintf(stderr, "sec_create_srv_config: server_hostname=%s, new_cfg=%x\n", s->server_hostname, (unsigned int)scfg); #endif scfg->server_response_token = 0; scfg->server_signature = NULL; scfg->chroot_dir = NULL; scfg->chroot_completed = 0; scfg->chroot_lock = ap_server_root_relative(p, "logs/modsec_chroot.lock"); scfg->guardian_log_name = NULL; scfg->guardian_log_fd = -1; scfg->guardian_log_condition = NULL; return scfg; } void *sec_merge_srv_config(pool *p, void *_parent, void *_child) { sec_srv_config *parent = (sec_srv_config *)_parent; sec_srv_config *new = (sec_srv_config *)ap_pcalloc(p, sizeof(sec_srv_config)); if (new == NULL) return NULL; #ifdef CONF_DEBUG fprintf(stdout, "sec_merge_srv_config: parent=%x, child=%x, new=%x\n", (unsigned int)_parent, (unsigned int)_child, (unsigned int)new); #endif new->server_signature = parent->server_signature; return new; } void sec_set_dir_defaults(sec_dir_config *dcfg) { if (dcfg == NULL) return; /* return immediatelly if we've already been here */ if (dcfg->configuration_helper == 1) return; dcfg->configuration_helper = 1; if (dcfg->filter_engine == NOT_SET) dcfg->filter_engine = 0; if (dcfg->scan_post == NOT_SET) dcfg->scan_post = 0; if (dcfg->auditlog_flag == NOT_SET) dcfg->auditlog_flag = 0; if (dcfg->filter_debug_level == NOT_SET) dcfg->filter_debug_level = 0; if (dcfg->filters_clear == NOT_SET) dcfg->filters_clear = 0; if (dcfg->actionset == NOT_SET_P) { dcfg->actionset = (actionset_t *)ap_pcalloc(dcfg->p, sizeof(actionset_t)); init_default_actionset(dcfg->actionset); } if (dcfg->auditlog_name == NOT_SET_P) dcfg->auditlog_name = NULL; if (dcfg->debuglog_name == NOT_SET_P) dcfg->debuglog_name = NULL; if (dcfg->range_start == NOT_SET) dcfg->range_start = 0; if (dcfg->range_end == NOT_SET) dcfg->range_end = 255; if (dcfg->check_encoding == NOT_SET) dcfg->check_encoding = 0; if (dcfg->check_unicode_encoding == NOT_SET) dcfg->check_unicode_encoding = 0; if (dcfg->upload_dir == NOT_SET_P) dcfg->upload_dir = NULL; if (dcfg->upload_keep_files == NOT_SET) dcfg->upload_keep_files = KEEP_FILES_OFF; if (dcfg->upload_approve_script == NOT_SET_P) dcfg->upload_approve_script = NULL; if (dcfg->normalize_cookies == NOT_SET) dcfg->normalize_cookies = 0; if (dcfg->check_cookie_format == NOT_SET) dcfg->check_cookie_format = 0; if (dcfg->cookie_format == NOT_SET) dcfg->cookie_format = 0; if (dcfg->charset_id == NOT_SET) dcfg->charset_id = UNKNOWN_CSID; if (dcfg->multibyte_replacement_byte == NOT_SET) dcfg->multibyte_replacement_byte = 0x0A; /* Note that the code below does not work as expected for us because we are * using dynamic, per-request, configuration finalisation, but configuration * merging takes place before request is processed. */ /* if (dcfg->inheritance_mandatory == NOT_SET) dcfg->inheritance_mandatory = 0; */ if (dcfg->auditlog_type == NOT_SET) dcfg->auditlog_type = AUDITLOG_SERIAL; if (dcfg->auditlog_storage_dir == NOT_SET_P) dcfg->auditlog_storage_dir = NULL; if (dcfg->auditlog_parts == NOT_SET_P) dcfg->auditlog_parts = "ABCFHZ"; if (dcfg->actions_restricted == NOT_SET) dcfg->actions_restricted = 0; } void *sec_create_dir_config(pool *p, char *path) { sec_dir_config *dcfg = (sec_dir_config *)ap_pcalloc(p, sizeof(*dcfg)); if (dcfg == NULL) return NULL; #ifdef CONF_DEBUG fprintf(stdout, "sec_create_dir_config: %s, new dcfg=%x\n", path, (unsigned int)dcfg); #endif dcfg->p = p; dcfg->configuration_helper = NOT_SET; dcfg->filter_engine = NOT_SET; dcfg->scan_post = NOT_SET; dcfg->actionset = NOT_SET_P; dcfg->actionset_signatures = NOT_SET_P; dcfg->signatures = ap_make_array(p, 10, sizeof(signature *)); dcfg->inherited_mandatory_signatures = ap_make_array(p, 10, sizeof(signature *)); if (path == NULL) { dcfg->path = ap_pstrdup(p, "(null)"); } else { dcfg->path = ap_pstrdup(p, path); } dcfg->auditlog_flag = NOT_SET; dcfg->auditlog_name = NOT_SET_P; dcfg->auditlog_fd = NOT_SET; dcfg->filter_debug_level = NOT_SET; dcfg->filters_clear = NOT_SET; dcfg->debuglog_name = NOT_SET_P; dcfg->debuglog_fd = NOT_SET; dcfg->range_start = NOT_SET; dcfg->range_end = NOT_SET; dcfg->check_encoding = NOT_SET; dcfg->check_unicode_encoding = NOT_SET; dcfg->upload_dir = NOT_SET_P; dcfg->upload_keep_files = NOT_SET; dcfg->upload_approve_script = NOT_SET_P; dcfg->normalize_cookies = NOT_SET; dcfg->check_cookie_format = NOT_SET; dcfg->cookie_format = NOT_SET; dcfg->charset_id = NOT_SET; dcfg->multibyte_replacement_byte = NOT_SET; dcfg->inheritance_mandatory = NOT_SET; dcfg->auditlog_type = NOT_SET; dcfg->auditlog_storage_dir = NOT_SET_P; dcfg->auditlog_parts = NOT_SET_P; dcfg->actions_restricted = 0; return dcfg; } static void sec_merge_dir_config_inheritance(pool *p, sec_dir_config *parent, sec_dir_config *child, sec_dir_config *new) { /* initialise the signature structures */ new->signatures = ap_make_array(p, 10, sizeof(signature *)); new->inherited_mandatory_signatures = ap_make_array(p, 10, sizeof(signature *)); /* the following two settings are not inherited from the * parent context */ new->filters_clear = child->filters_clear; new->inheritance_mandatory = child->inheritance_mandatory; /* build a list of mandatory signatures */ /* Note that valid values for inheritance_mandatory are -1 and 0 */ if (parent->inheritance_mandatory > 0) { signature **psignatures; int i; /* add all parent signatures to the list */ psignatures = (signature **)parent->signatures->elts; for (i = 0; i < parent->signatures->nelts; i++) { /* ignore placeholders */ if (psignatures[i]->is_inheritance_placeholder == 0) { *(signature **)ap_push_array(new->inherited_mandatory_signatures) = psignatures[i]; } } } else { signature **psignatures; int i = 0, in_chain = 0; /* add parent mandatory signatures to the list */ ap_array_cat(new->inherited_mandatory_signatures, parent->inherited_mandatory_signatures); /* loop through parent signatures and add mandatory ones to the list */ psignatures = (signature **)parent->signatures->elts; for (i = 0; i < parent->signatures->nelts; i++) { /* ignore placeholders and signatures that are really something else */ if ((psignatures[i]->is_inheritance_placeholder == 0) && ((in_chain)||((psignatures[i]->actionset != NULL)&&(psignatures[i]->actionset->mandatory != 0))) ) { *(signature **)ap_push_array(new->inherited_mandatory_signatures) = psignatures[i]; if ((psignatures[i]->actionset != NULL)&&(psignatures[i]->actionset->is_chained)) in_chain = 1; else in_chain = 0; } } } /* is inheritance enabled? */ if (new->filters_clear != 1) { /* inheritance should work here, so add all parent signatures */ ap_array_cat(new->signatures, parent->signatures); } else { signature **psignatures, **msignatures; int i, j, in_chain = 0; /* inheritance is off here, add only the signatures that are mandatory */ psignatures = (signature **)parent->signatures->elts; for (i = 0; i < parent->signatures->nelts; i++) { if (psignatures[i]->is_inheritance_placeholder == 0) { if (in_chain) { *(signature **)ap_push_array(new->signatures) = psignatures[i]; if ((psignatures[i]->actionset != NULL)&&(psignatures[i]->actionset->is_chained)) in_chain = 1; else in_chain = 0; } else { msignatures = (signature **)new->inherited_mandatory_signatures->elts; for (j = 0; j < new->inherited_mandatory_signatures->nelts; j++) { if (psignatures[i] == msignatures[j]) { *(signature **)ap_push_array(new->signatures) = psignatures[i]; if ((psignatures[i]->actionset != NULL)&&(psignatures[i]->actionset->is_chained)) in_chain = 1; else in_chain = 0; break; } } } } } } /* processing the child context now: add proper signatures to the list, process * placeholders to import and remove rules where possible */ { signature **csignatures, **nsignatures, **psignatures, **msignatures; int i, j, k; csignatures = (signature **)child->signatures->elts; for (i = 0; i < child->signatures->nelts; i++) { if (csignatures[i]->is_inheritance_placeholder != 0) { /* not a signature, this is a placeholder */ if (csignatures[i]->is_inheritance_placeholder == INHERITANCE_IMPORT) { signature *new_signature = NULL; int is_present = 0; /* import the signature from the parent context */ /* use the ID to find the signature in the parent context */ psignatures = (signature **)parent->signatures->elts; for(k = 0; k < parent->signatures->nelts; k++) { if ((psignatures[k]->actionset != NULL) && (psignatures[k]->actionset->id != NULL) && (strcasecmp(psignatures[k]->actionset->id, csignatures[i]->inheritance_id) == 0)) { new_signature = psignatures[k]; break; } } if (new_signature != NULL) { /* is the signature already present in this context */ is_present = 0; nsignatures = (signature **)new->signatures->elts; for (j = 0; j < new->signatures->nelts; j++) { if (new_signature == nsignatures[j]) { is_present = 1; break; } } /* finally, add the signature but only if it is not already present */ if (is_present == 0) { /* start with the signature we've just found, and include either * just the signature, or all signatures that are part of the * same chain */ psignatures = (signature **)parent->signatures->elts; for (j = k; j < parent->signatures->nelts; j++) { /* ignore placeholders */ if (psignatures[j]->is_inheritance_placeholder != 0) continue; *(signature **)ap_push_array(new->signatures) = psignatures[j]; if ((psignatures[j]->actionset == NULL)||(psignatures[j]->actionset->is_chained == 0)) break; } } } } else { /* remove signature */ /* find the signature in the new context */ nsignatures = (signature **)new->signatures->elts; for (j = 0; j < new->signatures->nelts; j++) { if ((nsignatures[j]->actionset != NULL) && (nsignatures[j]->actionset->id != NULL) && (strcasecmp(nsignatures[j]->actionset->id, csignatures[i]->inheritance_id) == 0)) { int is_present = 0; /* remove it only if it is not on the mandatory list */ msignatures = (signature **)new->inherited_mandatory_signatures->elts; for (k = 0; k < new->inherited_mandatory_signatures->nelts; k++) { if (msignatures[k] == nsignatures[j]) { is_present = 1; break; } } if (is_present == 0) { signature **tsignatures = NULL; int l, pos = j, in_chain = 1; /* At this point "pos" contains the position of the * signature we wish to remove. This can be either * a standalone signature, or one that starts a * chain. */ tsignatures = (signature **)new->signatures->elts; while((in_chain)&&(pos < new->signatures->nelts)) { /* determine if the signature on the current position is chained */ if ((tsignatures[pos]->actionset != NULL)&&(tsignatures[pos]->actionset->is_chained)) in_chain = 1; else in_chain = 0; /* remove the signature on the current position */ for (l = pos; l < new->signatures->nelts - 1; l++) { tsignatures[l] = tsignatures[l + 1]; } new->signatures->nelts--; } } } } } } else { /* this is a normal signature so just add it do the new context */ *(signature **)ap_push_array(new->signatures) = csignatures[i]; } } } } void *sec_merge_dir_config(struct pool *p, void *_parent, void *_child) { sec_dir_config *parent = (sec_dir_config *)_parent; sec_dir_config *child = (sec_dir_config *)_child; sec_dir_config *new = (sec_dir_config *)ap_pcalloc(p, sizeof(*new)); if (new == NULL) return NULL; #ifdef CONF_DEBUG fprintf(stdout, "sec_merge_dir_config: parent=%s (%x), child=%s (%x), new (%x)\n", parent->path, (unsigned int)parent, child->path, (unsigned int)child, (unsigned int)new); #endif /* merge child & parent into new */ memcpy(new, child, sizeof(*child)); new->filter_engine = (child->filter_engine == NOT_SET) ? parent->filter_engine : child->filter_engine; new->scan_post = (child->scan_post == NOT_SET) ? parent->scan_post : child->scan_post; new->actionset = (child->actionset == NOT_SET_P) ? parent->actionset : child->actionset; /* actions_restricted not inherited */ /* actionset_signatures not inherited */ /* take care of signature inheritance */ sec_merge_dir_config_inheritance(p, parent, child, new); new->auditlog_flag = (child->auditlog_flag == NOT_SET) ? parent->auditlog_flag : child->auditlog_flag; if (child->auditlog_fd == NOT_SET) { new->auditlog_fd = parent->auditlog_fd; new->auditlog_name = parent->auditlog_name; } else { new->auditlog_fd = child->auditlog_fd; new->auditlog_name = child->auditlog_name; } new->auditlog_type = (child->auditlog_type == NOT_SET) ? parent->auditlog_type : child->auditlog_type; new->auditlog_storage_dir = (child->auditlog_storage_dir == NOT_SET_P) ? parent->auditlog_storage_dir : child->auditlog_storage_dir; new->auditlog_parts = (child->auditlog_parts == NOT_SET_P) ? parent->auditlog_parts : child->auditlog_parts; new->filter_debug_level = (child->filter_debug_level == NOT_SET) ? parent->filter_debug_level : child->filter_debug_level; if (child->debuglog_fd == NOT_SET) { new->debuglog_fd = parent->debuglog_fd; new->debuglog_name = parent->debuglog_name; } else { new->debuglog_fd = child->debuglog_fd; new->debuglog_name = child->debuglog_name; } new->range_start = (child->range_start == NOT_SET) ? parent->range_start : child->range_start; new->range_end = (child->range_end == NOT_SET) ? parent->range_end : child->range_end; new->check_encoding = (child->check_encoding == NOT_SET) ? parent->check_encoding : child->check_encoding; new->check_unicode_encoding = (child->check_unicode_encoding == NOT_SET) ? parent->check_unicode_encoding : child->check_unicode_encoding; new->upload_dir = (child->upload_dir == NOT_SET_P) ? parent->upload_dir : child->upload_dir; new->upload_keep_files = (child->upload_keep_files == NOT_SET) ? parent->upload_keep_files : child->upload_keep_files; new->upload_approve_script = (child->upload_approve_script == NOT_SET_P) ? parent->upload_approve_script : child->upload_approve_script; new->normalize_cookies = (child->normalize_cookies == NOT_SET) ? parent->normalize_cookies : child->normalize_cookies; new->check_cookie_format = (child->check_cookie_format == NOT_SET) ? parent->check_cookie_format : child->check_cookie_format; new->cookie_format = (child->cookie_format == NOT_SET) ? parent->cookie_format : child->cookie_format; new->charset_id = (child->charset_id == NOT_SET) ? parent->charset_id : child->charset_id; new->multibyte_replacement_byte = (child->multibyte_replacement_byte == NOT_SET) ? parent->multibyte_replacement_byte : child->multibyte_replacement_byte; return new; } int detect_unicode_character(request_rec *r, char *p_read) { int unicode_len = 0; unsigned int d = 0; unsigned char c; if (p_read == NULL) return 0; c = *p_read; if (c == 0) return 0; if ((c & 0xE0) == 0xC0) { /* two byte unicode */ if (*(p_read + 1) == 0) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; else if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else { unicode_len = 2; d = ((c & 0x1F) << 6) | (*(p_read + 1) & 0x3F); } } else if ((c & 0xF0) == 0xE0) { /* three byte unicode */ if ((*(p_read + 1) == 0)||(*(p_read + 2) == 0)) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; else if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else { unicode_len = 3; d = ((c & 0x0F) << 12) | ((*(p_read + 1) & 0x3F) << 6) | (*(p_read + 2) & 0x3F); } } else if ((c & 0xF8) == 0xF0) { /* four byte unicode */ if ((*(p_read + 1) == 0)||(*(p_read + 2) == 0)||(*(p_read + 3) == 0)) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; else if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 3)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else { d = ((c & 0x07) << 18) | ((*(p_read + 1) & 0x3F) << 12) | ((*(p_read + 2) & 0x3F) < 6) | (*(p_read + 3) & 0x3F); unicode_len = 4; } } else if ((c & 0xFC) == 0xF8) { /* five byte unicode */ if ((*(p_read + 1) == 0)||(*(p_read + 2) == 0)||(*(p_read + 3) == 0)||(*(p_read + 4) == 0)) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; else if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 3)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 4)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else { d = ((c & 0x03) << 24) | ((*(p_read + 1) & 0x3F) << 18) | ((*(p_read + 2) & 0x3F) << 12) | ((*(p_read + 3) & 0x3F) << 6) | (*(p_read + 4) & 0x3F); unicode_len = 5; } } else if ((c & 0xFE) == 0xFC) { /* six byte unicode */ if ((*(p_read + 1) == 0)||(*(p_read + 2) == 0)||(*(p_read + 3) == 0)||(*(p_read + 4) == 0)||(*(p_read + 5) == 0)) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; else if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 3)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 4)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else if (((*(p_read + 5)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; else { d = ((c & 0x01) << 30) | ((*(p_read + 1) & 0x3F) << 24) | ((*(p_read + 2) & 0x3F) << 18) | ((*(p_read + 3) & 0x3F) << 12) | ((*(p_read + 4) & 0x3F) << 6) | (*(p_read + 5) & 0x3F); unicode_len = 6; } } if ((unicode_len > 1)&&((d & 0x7F) == d)) { unicode_len = UNICODE_ERROR_OVERLONG_CHARACTER; } return(unicode_len); } char *normalise_urlencoding_relaxed_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg) { unsigned char *p_read, *p_write; unsigned char c; if (error_msg == NULL) return NULL; *error_msg = NULL; if (uri == NULL) return NULL; p_read = (unsigned char *)uri; p_write = (unsigned char *)uri; while ((c = *p_read) != 0) { if (c == '%') { /* see if there are enough bytes available */ if ((*(p_read + 1) == 0)||(*(p_read + 2) == 0)) { c = 0; } else { /* here we only decode a %xx combo if it is a valid * encoding, we leave it as is otherwise */ char c1 = *(p_read + 1), c2 = *(p_read + 2); if ( (((c1 >= '0')&&(c1 <= '9')) || ((c1 >= 'a')&&(c1 <= 'f')) || ((c1 >= 'A')&&(c1 <= 'F'))) && (((c2 >= '0')&&(c2 <= '9')) || ((c2 >= 'a')&&(c2 <= 'f')) || ((c2 >= 'A')&&(c2 <= 'F'))) ) { c = x2c(++p_read); p_read++; } } } else { /* this check is performed only against the original data * and not against the decoded values (we want to * avoid false positives) */ if ((c < dcfg->range_start)||(c > dcfg->range_end)) { *error_msg = ap_psprintf(r->pool, "Invalid character detected [%i]", c); return NULL; } } /* replace null bytes with whitespace */ if (c == 0) c = 32; *p_write++ = c; p_read++; } *p_write = 0; return uri; } char *normalise_urlencoding_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg) { unsigned char *p_read, *p_write; unsigned char c; if (error_msg == NULL) return NULL; *error_msg = NULL; if (uri == NULL) return NULL; p_read = (unsigned char *)uri; p_write = (unsigned char *)uri; while ((c = *p_read) != 0) { /* decode URL decoding */ if (c == '+') c = 32; else if (c == '%') { /* see if there are enough bytes available */ if ((*(p_read + 1) == 0)||(*(p_read + 2) == 0)) { if (dcfg->check_encoding) { *error_msg = ap_psprintf(r->pool, "Invalid URL encoding detected: not enough characters"); return NULL; } else c = 0; } else { /* move onto the first hexadecimal letter */ p_read++; c = *p_read; if (dcfg->check_encoding) { if (! (((c >= '0')&&(c <= '9')) || ((c >= 'a')&&(c <= 'f')) || ((c >= 'A')&&(c <= 'F'))) ) { *error_msg = ap_psprintf(r->pool, "Invalid URL encoding detected: invalid characters used"); return NULL; } } c = *(p_read + 1); if (dcfg->check_encoding) { if (! (((c >= '0')&&(c <= '9')) || ((c >= 'a')&&(c <= 'f')) || ((c >= 'A')&&(c <= 'F'))) ) { *error_msg = ap_psprintf(r->pool, "Invalid URL encoding detected: invalid characters used"); return NULL; } } /* decode two hexadecimal letters into a single byte */ c = x2c(p_read); p_read++; } } if ((c < dcfg->range_start)||(c > dcfg->range_end)) { *error_msg = ap_psprintf(r->pool, "Invalid character detected [%i]", c); return NULL; } /* we replace null bytes with whitespace */ if (c == 0) c = 32; *p_write++ = c; p_read++; } *p_write = 0; return uri; } char *normalise_other_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg) { unsigned char *p_read, *p_write, *p_slash; unsigned char c; int count; if (error_msg == NULL) return NULL; *error_msg = NULL; if (uri == NULL) return NULL; p_read = (unsigned char *)uri; p_write = (unsigned char *)uri; p_slash = NULL; count = 0; while (*p_read != 0) { c = *p_read; if (dcfg->check_unicode_encoding) { int urc = detect_unicode_character(r, (char *)p_read); switch(urc) { case UNICODE_ERROR_CHARACTERS_MISSING : *error_msg = ap_psprintf(r->pool, "Invalid Unicode encoding: not enough bytes"); return NULL; break; case UNICODE_ERROR_INVALID_ENCODING : *error_msg = ap_psprintf(r->pool, "Invalid Unicode encoding: invalid byte value"); return NULL; break; case UNICODE_ERROR_OVERLONG_CHARACTER : *error_msg = ap_psprintf(r->pool, "Invalid Unicode encoding: overlong character"); return NULL; break; } } switch (c) { #ifdef WIN32 case '\\' : #endif case '/' : if (p_slash == NULL) { /* remove the occurencies of "./" */ if ( (count > 1) && ((*(p_write - 1) == '.') && (*(p_write - 2) == '/')) ) { count -= 2; p_write -= 2; } p_slash = p_read; *p_write++ = '/'; p_read++; count++; } else { /* the previous character was a slash, we * will ignore this one - just increment * the read pointer */ p_read++; } break; default: /* p_slash is used to detect more than one * slash character in a row */ p_slash = NULL; *p_write++ = c; p_read++; count++; break; } } *p_write = 0; return uri; } char *normalise_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg) { if (error_msg == NULL) return NULL; *error_msg = NULL; if (uri == NULL) { *error_msg = ap_psprintf(r->pool, "null given as argument"); return NULL; } if (normalise_urlencoding_inplace(r, dcfg, uri, error_msg) == NULL) { /* error_msg already populated */ return NULL; } if (normalise_other_inplace(r, dcfg, uri, error_msg) == NULL) { /* error_msg already populated */ return NULL; } return filter_multibyte_inplace(dcfg->charset_id, (char)dcfg->multibyte_replacement_byte, uri); } char *normalise_relaxed_inplace(request_rec *r, sec_dir_config *dcfg, char *uri, char **error_msg) { if (error_msg == NULL) return NULL; *error_msg = NULL; if (uri == NULL) { *error_msg = ap_psprintf(r->pool, "null given as argument"); return NULL; } if (normalise_urlencoding_relaxed_inplace(r, dcfg, uri, error_msg) == NULL) { /* error_msg already populated */ return NULL; } if (normalise_other_inplace(r, dcfg, uri, error_msg) == NULL) { /* error_msg already populated */ return NULL; } return filter_multibyte_inplace(dcfg->charset_id, (char)dcfg->multibyte_replacement_byte, uri); } char *normalise_relaxed(request_rec *r, sec_dir_config *dcfg, char *_uri, char **error_msg) { char *uri; if (error_msg == NULL) return NULL; *error_msg = NULL; if (_uri == NULL) { *error_msg = ap_psprintf(r->pool, "null given as argument"); return NULL; } uri = ap_pstrdup(r->pool, _uri); if (uri == NULL) return NULL; return normalise_relaxed_inplace(r, dcfg, uri, error_msg); } char *normalise(request_rec *r, sec_dir_config *dcfg, char *_uri, char **error_msg) { char *uri; if (_uri == NULL) return NULL; uri = ap_pstrdup(r->pool, _uri); if (uri == NULL) return NULL; return normalise_inplace(r, dcfg, uri, error_msg); } void store_msr(request_rec *r, modsec_rec *msr) { ap_table_setn(r->notes, NOTE_MSR, (char *)msr); sec_debug_log(r, 9, "Stored msr (%x) in r (%x)", msr, r); } modsec_rec *find_msr(request_rec *r) { modsec_rec *msr = NULL; request_rec *rx; msr = (modsec_rec *)ap_table_get(r->notes, NOTE_MSR); if (msr != NULL) { sec_debug_log(r, 9, "Found msr (%x) in r (%x)", msr, r); return msr; } /* If this is a subrequest then look in the main request */ if (r->main != NULL) { msr = (modsec_rec *)ap_table_get(r->main->notes, NOTE_MSR); if (msr != NULL) { sec_debug_log(r, 9, "Found msr (%x) in r->main (%x)", msr, r->main); return msr; } } /* If the request was redirected then look in the previous requests */ rx = r->prev; while(rx != NULL) { msr = (modsec_rec *)ap_table_get(rx->notes, NOTE_MSR); if (msr != NULL) { sec_debug_log(r, 9, "Found msr (%x) in r->prev (%x)", msr, rx); return msr; } rx = rx->prev; } return NULL; } #define CHUNK_CAPACITY 4096 typedef struct { const char *data; long int length; } data_chunk; int read_post_payload(modsec_rec *msr, char **p) { array_header *chunks_array = NULL; data_chunk *current_chunk = NULL; request_rec *r = msr->r; request_body *ctx = ap_pcalloc(r->pool, sizeof(request_body)); *p = NULL; if (ctx == NULL) { msr->tmp_message = ap_psprintf(r->pool, "Failed to allocate %i bytes", sizeof(*ctx)); return -1; } if (msr->should_body_exist == 0) { sec_debug_log(r, 4, "read_post_payload: this request has no body (%i)", msr->should_body_exist); return 0; } if (msr->dcfg->scan_post != 1) { sec_debug_log(r, 4, "read_post_payload: request body buffering is off here (scan post = %i)", msr->dcfg->scan_post); return 0; } /* By default, we want to write request body to a file * and delete it afterwards. */ ctx->tmp_file_mode = REQBODY_FILE_DELETE; if (r->method_number == M_POST) { char *content_type = (char *)ap_table_get(r->headers_in, "Content-Type"); if ((content_type != NULL) && (strncasecmp(content_type, "application/x-www-form-urlencoded", 33) == 0)) { /* The only case when we don't want the request body to * be written to a file is when we encounter a POST request * with application/x-www-form-urlencoded content because * this is something we can safely write to the log file. */ ctx->tmp_file_mode = REQBODY_FILE_NONE; } } if (r->method_number == M_PUT) { ctx->tmp_file_mode = REQBODY_FILE_DELETE; ctx->is_put = 1; } { unsigned long bufsize, payload_size = 0, length_sofar = 0; char *payload, *t; int i, rc; if ((rc = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR)) != OK) { msr->tmp_message = ap_psprintf(r->pool, "ap_setup_client_block failed with %i", rc); *p = NULL; return -1; } bufsize = r->remaining; if ((bufsize < 0)||(bufsize + 1 <= 0)) { msr->tmp_message = ap_psprintf(r->pool, "Invalid content length: %lu", bufsize); *p = NULL; return -1; } if (bufsize >= 1073741824) { /* 1 GB */ msr->tmp_message = ap_psprintf(r->pool, "Content-Length too long for request buffering: %lu", bufsize); *p = NULL; return -1; } chunks_array = ap_make_array(r->pool, 64, sizeof(data_chunk*)); if (chunks_array == NULL) return -1; current_chunk = (data_chunk *)ap_pcalloc(r->pool, sizeof(data_chunk)); if (current_chunk == NULL) return -1; current_chunk->data = malloc(CHUNK_CAPACITY); if (current_chunk->data == NULL) return -1; current_chunk->length = 0; *(data_chunk **)ap_push_array(chunks_array) = current_chunk; if (ctx->tmp_file_mode != REQBODY_FILE_NONE) { char *folder = NULL; if (msr->dcfg->upload_dir != NULL) folder = msr->dcfg->upload_dir; else folder = get_temp_folder(r->pool); ctx->tmp_file_name = ap_psprintf(r->pool, "%s/%s-%s-request_body-XXXXXX", folder, current_filetime(r), r->connection->remote_ip); if (ctx->tmp_file_name == NULL) { sec_debug_log(r, 1, "read_post_payload: Memory allocation failed"); goto RP_CLEAN_ERROR_RETURN; } ctx->tmp_file_fd = sec_mkstemp(ctx->tmp_file_name); if (ctx->tmp_file_fd < 0) { msr->tmp_message = ap_psprintf(r->pool, "read_post_payload: Failed to create file \"%s\" because %d(\"%s\")", log_escape(r->pool, ctx->tmp_file_name), errno, log_escape(r->pool, strerror(errno))); goto RP_CLEAN_ERROR_RETURN; } sec_debug_log(r, 2, "read_post_payload: Created file to store request body: %s", ctx->tmp_file_name); /* schedule resource cleanup for later */ ap_register_cleanup(r->pool, (void *)msr, (void (*)(void *))request_body_file_cleanup, ap_null_cleanup); } ap_hard_timeout("mod_security: receive request data", r); if (ap_should_client_block(r)) { while ((i = ap_get_client_block(r, (char *)(current_chunk->data + current_chunk->length), CHUNK_CAPACITY - current_chunk->length)) > 0) { sec_debug_log(r, 5, "read_post_payload: read %lu bytes", i); if (ctx->tmp_file_mode != REQBODY_FILE_NONE) { int j; j = write(ctx->tmp_file_fd, (char *)(current_chunk->data + current_chunk->length), i); if (j != i) { msr->_post_payload = NULL; msr->tmp_message = ap_psprintf(r->pool, "Error writing request body to file: %i", j); goto RP_CLEAN_ERROR_RETURN; } } current_chunk->length += i; length_sofar += i; if (current_chunk->length >= CHUNK_CAPACITY) { /* Get ourselves a new chunk. */ current_chunk = (data_chunk *)ap_pcalloc(r->pool, sizeof(data_chunk)); if (current_chunk == NULL) { goto RP_CLEAN_ERROR_RETURN; } current_chunk->data = malloc(CHUNK_CAPACITY); if (current_chunk->data == NULL) { goto RP_CLEAN_ERROR_RETURN; } current_chunk->length = 0; *(data_chunk **)ap_push_array(chunks_array) = current_chunk; } ap_reset_timeout(r); } } ap_kill_timeout(r); /* Allocate the buffer. */ *p = payload = t = ap_palloc(r->pool, length_sofar + 1); if ((payload == NULL)||(length_sofar + 1 <= 0)) { msr->tmp_message = ap_psprintf(r->pool, "read_post_payload: failed to allocate %li bytes", r->remaining + 1); *p = NULL; goto RP_CLEAN_ERROR_RETURN; } /* Move data from the chunks into the buffer. */ { data_chunk **chunks = NULL; payload_size = 0; chunks = (data_chunk **)chunks_array->elts; for(i = 0; i < chunks_array->nelts; i++) { if (payload_size + chunks[i]->length <= length_sofar) { memcpy(payload + payload_size, chunks[i]->data, chunks[i]->length); payload_size += chunks[i]->length; } free((void *)chunks[i]->data); chunks[i]->data = NULL; } } payload[payload_size] = 0; ctx->buffer = payload; ctx->sofar = payload; ctx->length = payload_size; ctx->remaining = payload_size; if (ctx->tmp_file_mode != REQBODY_FILE_NONE) { if (ctx->tmp_file_fd != 0) close(ctx->tmp_file_fd); } /* here we alter Apache internal structures * to point those pointers to our buffer * and to reset counters so that Apache will * think that the payload was not read at all */ r->connection->client->inptr = (unsigned char *)payload; r->connection->client->incnt = payload_size; r->read_length = 0; r->remaining = payload_size; msr->is_body_read = 1; msr->ctx_in = ctx; } return 1; RP_CLEAN_ERROR_RETURN: if (chunks_array != NULL) { data_chunk **chunks = NULL; int i; chunks = (data_chunk **)chunks_array->elts; for(i = 0; i < chunks_array->nelts; i++) { if (chunks[i]->data != NULL) { free((void *)chunks[i]->data); } } } return -1; } char *unescape_regex_hex_inplace(char *input) { char metacharacters[] = { '\\', '^', '$', '.', '[', '|', '(', ')', '?', '*', '+', '{', ']', ')', '}', '\0' }; /* null-terminated */ char *p = input; char c, *m; while(*p != 0) { /* look for the "\x" combo */ if ( (*p == '\\') && ((*(p + 1) == 'x')||(*(p + 1) == 'X')) ) { /* are there enough bytes */ if ((*(p + 2) != 0)&&(*(p + 3) != 0)) { char *end = p + 4; /* end points to the first character after the \xHH */ char *t = p; c = x2c((unsigned char *)(p + 2)); /* if the character is a regex * meta character we need to escape * it first */ m = metacharacters; while(*m != '\0') { if (*m == c) { *t++ = '\\'; break; } m++; } *t++ = c; do { c = *end++; *t++ = c; } while(c != 0); } } p++; } return input; } int parse_arguments(char *s, table *parsed_args, request_rec *r, sec_dir_config *dcfg, char **error_msg) { long int inputlength, i, j; char *my_error_msg = NULL; char *value = NULL; char *buf; int status; if (error_msg == NULL) return -1; *error_msg = NULL; if (s == NULL) return -1; inputlength = strlen(s); if (inputlength == 0) return 1; if (inputlength + 1 <= 0) return -1; buf = (char *)malloc(inputlength + 1); if (buf == NULL) { *error_msg = ap_psprintf(r->pool, "Failed to allocate %li bytes", inputlength + 1); return -1; } i = 0; j = 0; status = 0; while (i < inputlength) { if (status == 0) { /* parameter name */ while ((s[i] != '=') && (s[i] != '&') && (i < inputlength)) { buf[j] = s[i]; j++; i++; } buf[j++] = 0; } else { /* parameter value */ while ((s[i] != '&') && (i < inputlength)) { buf[j] = s[i]; j++; i++; } buf[j++] = 0; } if (status == 0) { if (normalise_inplace(r, dcfg, buf, &my_error_msg) == NULL) { free(buf); *error_msg = ap_psprintf(r->pool, "Error normalising parameter name: %s", my_error_msg); return -1; } if (s[i] == '&') { /* Empty parameter */ sec_debug_log(r, 4, "Adding parameter \"%s\" (empty)", log_escape(r->pool, buf)); ap_table_add(parsed_args, buf, ""); status = 0; /* unchanged */ j = 0; } else { status = 1; value = &buf[j]; } } else { if (normalise_inplace(r, dcfg, value, &my_error_msg) == NULL) { free(buf); *error_msg = ap_psprintf(r->pool, "Error normalising parameter value: %s", my_error_msg); return -1; } sec_debug_log(r, 4, "Adding parameter \"%s\"=\"%s\"", log_escape(r->pool, buf), log_escape(r->pool, value)); ap_table_add(parsed_args, buf, value); status = 0; j = 0; } i++; /* skip over the separator */ } /* last parameter was empty */ if (status == 1) { sec_debug_log(r, 4, "Adding parameter \"%s\" (empty)", log_escape(r->pool, buf)); ap_table_add(parsed_args, buf, ""); } free(buf); return 1; } char *remove_binary_content(request_rec *r, char *data) { long size = r->remaining; char *src, *dst, *newdata; if (data == NULL) return NULL; if ((size < 0)||(size + 1 <= 0)) return NULL; /* make a copy of the payload first */ newdata = ap_palloc(r->pool, size + 1); if (newdata == NULL) { sec_debug_log(r, 1, "remove_binary_content: failed to allocate %i bytes", size + 1); return NULL; } /* now remove zeros from the new buffer */ src = data; dst = newdata; while(size--) { if (*src != 0) *dst++ = *src++; else src++; } *dst = 0; return newdata; } int sec_initialise(modsec_rec *msr) { char *my_error_msg = NULL; request_rec *r = msr->r; int rc; const array_header *arr; table_entry *te; int i; msr->request_uri = normalise(r, msr->dcfg, msr->r->unparsed_uri, &my_error_msg); if (msr->request_uri == NULL) { msr->tmp_message = ap_psprintf(r->pool, "Error normalising REQUEST_URI: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } sec_debug_log(r, 4, "Normalised REQUEST_URI: \"%s\"", log_escape(r->pool, msr->request_uri)); sec_debug_log(r, 2, "Parsing arguments..."); /* parse and validate GET parameters when available */ if (r->args != NULL) { if (parse_arguments(r->args, msr->parsed_args, r, msr->dcfg, &my_error_msg) < 0) { msr->tmp_message = ap_psprintf(r->pool, "Invalid parameters: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } } arr = ap_table_elts(r->headers_in); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { int check_unicode_encoding = msr->dcfg->check_unicode_encoding; char *header_value = NULL; if (strncasecmp(te[i].key, "mod_security-", 13) == 0) { msr->tmp_message= ap_psprintf(r->pool, "mod_security: Internal header detected in request: %s", te[i].key); return perform_action(msr, msr->dcfg->actionset, NULL); } /* We need to turn-off the unicode encoding checks for the * Referer header because it sometimes contains the information * from another web site, and we can't be sure the information * will be a valid Unicode text. */ if (strcasecmp(te[i].key, "Referer") == 0) msr->dcfg->check_unicode_encoding = 0; if (normalise_relaxed(r, msr->dcfg, te[i].key, &my_error_msg) == NULL) { msr->dcfg->check_unicode_encoding = check_unicode_encoding; msr->tmp_message = ap_psprintf(r->pool, "Error validating header name: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } header_value = normalise_relaxed(r, msr->dcfg, te[i].val, &my_error_msg); if (header_value == NULL) { msr->dcfg->check_unicode_encoding = check_unicode_encoding; msr->tmp_message = ap_psprintf(r->pool, "Error validating header value (%s): %s", te[i].key, my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } /* cache the normalised header value for later */ ap_table_add(msr->cache_headers_in, te[i].key, header_value); msr->dcfg->check_unicode_encoding = check_unicode_encoding; } /* parse, optionally validate cookies */ if (parse_cookies(msr, &my_error_msg) < 0) { msr->tmp_message = ap_psprintf(r->pool, "Error parsing cookies: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } if (msr->dcfg->scan_post) { char *content_type = (char *)ap_table_get(r->headers_in, "Content-Type"); char *s; if (content_type != NULL) sec_debug_log(r, 3, "Content-Type is \"%s\"", log_escape(r->pool, content_type)); else sec_debug_log(r, 3, "Content-Type is not available"); s = (char *)get_env_var(r, "MODSEC_NOPOSTBUFFERING"); if (s != NULL) { msr->post_payload_dynamic_off = 1; sec_debug_log(r, 2, "read_post_payload: POST scanning turned off dynamically (MODSEC_NOPOSTBUFFERING=\"%s\")", log_escape(r->pool, s)); } else { rc = read_post_payload(msr, &msr->_post_payload); if (rc < 0) { /* the error message prepared by read_post_payload */ return perform_action(msr, msr->dcfg->actionset, NULL); } } /* go back straight away if there is no body */ if (msr->_post_payload == NULL) { return DECLINED; } if ((content_type != NULL) && (strncasecmp(content_type, "application/x-www-form-urlencoded", 33) == 0) && (msr->r->method_number == M_POST)) { int j; /* Check that the byte range is OK. */ sec_debug_log(r, 3, "Checking byte range in POST payload"); for (j = 0; j < msr->ctx_in->length; j++) { int c = ((unsigned char *)msr->ctx_in->buffer)[j]; if ((c < msr->dcfg->range_start) || (c > msr->dcfg->range_end)) { msr->tmp_message = ap_psprintf(r->pool, "Invalid character detected in POST payload [%i]", c); return perform_action(msr, msr->dcfg->actionset, NULL); } } /* parse variables before normalising the bufffer */ sec_debug_log(r, 3, "Parsing variables from POST payload"); if (parse_arguments(msr->_post_payload, msr->parsed_args, r, msr->dcfg, &my_error_msg) < 0) { msr->tmp_message = ap_psprintf(r->pool, "Error parsing POST parameters: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } msr->_post_payload = normalise(r, msr->dcfg, msr->_post_payload, &my_error_msg); if (msr->_post_payload == NULL) { msr->tmp_message = ap_psprintf(r->pool, "Error normalising POST payload: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } } else if ((content_type != NULL) && (msr->r->method_number == M_POST) && (strncasecmp(content_type, "multipart/form-data", 19) == 0) ) { multipart_data *mpd = (multipart_data *)ap_pcalloc(r->pool, sizeof(*mpd)); char *boundary = NULL; msr->mpd = mpd; msr->is_multipart = TRUE; boundary = strstr(content_type, "boundary="); if ((boundary != NULL)&&(*(boundary + 9) != 0)) { mpd->boundary = boundary + 9; if (multipart_init(mpd, msr, &my_error_msg) < 0) { msr->tmp_message = ap_psprintf(r->pool, "Failed to initialise multipart/form-data parsing: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } if (multipart_process_chunk(mpd, msr->_post_payload, r->remaining, &my_error_msg) < 0) { msr->tmp_message = ap_psprintf(r->pool, "Error processing request body: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } if (multipart_complete(mpd, &my_error_msg) < 0) { msr->tmp_message = ap_psprintf(r->pool, "Error processing request body: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } /* get parsed variables */ if (multipart_get_variables(mpd, msr->parsed_args, msr->dcfg, &my_error_msg) < 0) { msr->tmp_message = ap_psprintf(r->pool, "Error parsing multipart parameters: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } if (msr->dcfg->upload_approve_script != NULL) { /* we only accept 1 as a correct result; the function may also return * 0 for verification failed and -1 for an error (e.g. the script cannot * be executed) */ if (multipart_verify_uploaded_files(r, mpd, msr->dcfg->upload_approve_script, &my_error_msg) != 1) { msr->tmp_message = ap_psprintf(r->pool, "Error verifying files: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, NULL); } } } } else { /* remove binary content from the payload */ sec_debug_log(r, 3, "Removing null bytes from POST payload"); msr->_post_payload = remove_binary_content(r, msr->_post_payload); if (msr->_post_payload == NULL) { msr->tmp_message = ap_psprintf(r->pool, "Error while removing binary content from POST"); return perform_action(msr, msr->dcfg->actionset, NULL); } } } return DECLINED; } int sec_check_all_signatures(modsec_rec *msr) { request_rec *r = msr->r; signature **signatures; int i, mode = 0; int skip_count = 0; int rc = DECLINED; /* loop through all signatures */ signatures = (signature **)msr->dcfg->signatures->elts; for (i = 0; i < msr->dcfg->signatures->nelts; i++) { /* do not process signatures that are not proper signatures */ if (signatures[i]->is_inheritance_placeholder != 0) continue; /* check if we need to skip this rule */ if (skip_count != 0) { skip_count--; continue; } /* just clear the flag, we had to use the flag * to detect a case when the last rule in the * rule chain was marked as chained */ if (mode == 2) mode = 0; /* in mode 1, we are looking for the next filter * that is next chained, and then skip it to * execute the next filter in the chain */ if (mode == 1) { if ((signatures[i]->actionset == NULL) || ((signatures[i]->actionset != NULL)&&(signatures[i]->actionset->is_chained == 0)) ) { mode = 0; } continue; } msr->tmp_message = NULL; rc = check_single_signature(msr, signatures[i]); sec_debug_log(r, 9, "Signature check returned %i", rc); /* MODSEC_ALLOW means that an allow action was called, * we need to pass the request through */ if (rc == MODSEC_ALLOW) { sec_debug_log(r, 9, "Allow request to pass through"); return DECLINED; } /* OK means there was no filter match, we * switch to mode 1, processing will continue * with the next filter chain */ if (rc == OK) { /* we go into mode 1 (looking for the last rule * in the chain) if this rule is not it */ if ((signatures[i]->actionset != NULL)&&(signatures[i]->actionset->is_chained == 1)) { sec_debug_log(r, 9, "Chained rule and no match, find the next rule not in chain"); mode = 1; } continue; } /* any status greater than zero means there * was a filter match, so we either stop execution * or proceed to the next rule if this rule was * chained */ if (rc > 0) { if ((signatures[i]->actionset != NULL)&&(signatures[i]->actionset->is_chained == 1)) { mode = 2; sec_debug_log(r, 9, "Chained rule with match, continue in the loop"); continue; } else { rule_match: sec_debug_log(r, 9, "Rule match, returning code %i", rc); return rc; } } /* if the return status is zero this * means skip some rules, so we skip */ if (rc == MODSEC_SKIP) { if (signatures[i]->actionset == NULL) skip_count = 1; else skip_count = signatures[i]->actionset->skip_count; continue; } sec_debug_log(r, 1, "Unprocessed return code [%i]", rc); return DECLINED; } /* handle the case where there was a match on the * last filter so mode 2 check could not be done * strickly speaking this should be a configuration error */ if (mode == 2) { sec_debug_log(r, 1, "Last rule marked as chained - ignoring"); goto rule_match; } return DECLINED; } int check_single_signature(modsec_rec *msr, signature *sig) { char *my_error_msg; int rc = _check_single_signature(msr, sig, &my_error_msg); if (rc == DECLINED) { msr->tmp_message = ap_psprintf(msr->r->pool, "Error processing signature: %s", my_error_msg); return perform_action(msr, msr->dcfg->actionset, sig); } return rc; } int _check_single_signature(modsec_rec *msr, signature *sig, char **error_msg) { int j, rs; if (error_msg == NULL) return HTTP_INTERNAL_SERVER_ERROR; *error_msg = NULL; /* the idea behind non-selective filters is to apply them over * raw data, typically the complete first line of the request * and the complete POST payload */ if (sig->is_selective == 0) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at REQUEST_URI", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, msr->request_uri, VAR_REQUEST_URI, NULL); if (rs != OK) return rs; if (msr->is_body_read) { if (msr->mpd != NULL) { /* multipart/form-data request */ if (msr->_fake_post_payload == NULL) { msr->_fake_post_payload = construct_fake_urlencoded(msr, msr->parsed_args); if (msr->_fake_post_payload == NULL) { *error_msg = ap_psprintf(msr->r->pool, "Failed during fake POST payload construction"); return DECLINED; } } sec_debug_log(msr->r, 2, "Checking signature \"%s\" at POST_PAYLOAD (faked)", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, msr->_fake_post_payload, VAR_POST_PAYLOAD, NULL); if (rs != OK) return rs; } else { if (msr->_post_payload != NULL) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at POST_PAYLOAD", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, msr->_post_payload, VAR_POST_PAYLOAD, NULL); if (rs != OK) return rs; } } } } else { variable **variables; const char *v; /* this is a selective signature, this means that we need to * check only one part of the request and leave the rest alone */ /* selective signatures can be negative and non-negative; * non-negative signatures consist of a list of variables * that represent parts of the request that need to be * checked; negative signatures apply only to request * arguments, when you want to exclude an argument from * a check */ if (sig->is_negative == 0) { /* loop through signature variables and * check them */ variables = (variable **)sig->variables->elts; for (j = 0; j < sig->variables->nelts; j++) { if (variables[j]->type == VAR_ARGS) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at QUERY_STRING", log_escape(msr->r->pool, sig->pattern)); v = get_variable(msr, variables[j], VAR_QUERY_STRING); rs = check_sig_against_string(msr, sig, v, VAR_QUERY_STRING, NULL); if (rs != OK) return rs; if (msr->is_body_read) { if (msr->mpd != NULL) { /* multipart/form-data request */ if (msr->_fake_post_payload == NULL) { msr->_fake_post_payload = construct_fake_urlencoded(msr, msr->parsed_args); if (msr->_fake_post_payload == NULL) { *error_msg = ap_psprintf(msr->r->pool, "Failed during fake POST payload construction"); return DECLINED; } } sec_debug_log(msr->r, 2, "Checking signature \"%s\" at POST_PAYLOAD (faked)", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, msr->_fake_post_payload, VAR_POST_PAYLOAD, NULL); if (rs != OK) return rs; } else { if (msr->_post_payload != NULL) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at POST_PAYLOAD", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, msr->_post_payload, VAR_POST_PAYLOAD, NULL); if (rs != OK) return rs; } } } } else if (variables[j]->type == VAR_POST_PAYLOAD) { /* Ignore requests without bodies */ if (msr->should_body_exist) { /* Note it can happen that a body is available but * _post_payload is NULL. */ if (msr->is_body_read == 0) { /* Only complain if body is not available by configuration mistake */ if (msr->post_payload_dynamic_off == 0) { sec_debug_log(msr->r, 1, "Filtering against POST payload requested but payload is not available"); } return OK; } else { if (msr->mpd == NULL) { if (msr->_post_payload != NULL) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at POST_PAYLOAD", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, msr->_post_payload, VAR_POST_PAYLOAD, NULL); if (rs != OK) return rs; } } else { /* multipart/form-data request */ if (msr->_fake_post_payload == NULL) { msr->_fake_post_payload = construct_fake_urlencoded(msr, msr->parsed_args); if (msr->_fake_post_payload == NULL) { *error_msg = ap_psprintf(msr->r->pool, "Failed during fake POST payload construction"); return DECLINED; } } sec_debug_log(msr->r, 2, "Checking signature \"%s\" at POST_PAYLOAD (faked)", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, msr->_fake_post_payload, VAR_POST_PAYLOAD, NULL); if (rs != OK) return rs; } } } } else if (variables[j]->type == VAR_ARGS_NAMES) { table_entry *te; array_header *arr; int k; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at ARGS_NAMES", log_escape(msr->r->pool, sig->pattern)); arr = ap_table_elts(msr->parsed_args); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { rs = check_sig_against_string(msr, sig, te[k].key, VAR_ARGS_NAMES, NULL); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_ARGS_VALUES) { table_entry *te; array_header *arr; int k; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at ARGS_VALUES", log_escape(msr->r->pool, sig->pattern)); arr = ap_table_elts(msr->parsed_args); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { sec_debug_log(msr->r, 4, "Checking signature \"%s\" at ARGS_VALUES(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, te[k].key)); rs = check_sig_against_string(msr, sig, te[k].val, VAR_ARGS_VALUES, te[k].key); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_COOKIES_NAMES) { table_entry *te; array_header *arr; int k; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at COOKIES_NAMES", log_escape(msr->r->pool, sig->pattern)); arr = ap_table_elts(msr->parsed_cookies); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { /*sec_debug_log(msr->r, 5, "Cookie \"%s\"=\"%s\"", log_escape(msr->r->pool, te[k].key), log_escape(msr->r->pool, te[k].val));*/ rs = check_sig_against_string(msr, sig, te[k].key, VAR_COOKIES_NAMES, NULL); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_COOKIES_VALUES) { table_entry *te; array_header *arr; int k; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at COOKIES_VALUES", log_escape(msr->r->pool, sig->pattern)); arr = ap_table_elts(msr->parsed_cookies); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { sec_debug_log(msr->r, 4, "Checking signature \"%s\" at COOKIES_VALUES(%s)", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, te[k].key)); rs = check_sig_against_string(msr, sig, te[k].val, VAR_COOKIES_VALUES, te[k].key); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_HEADERS) { table_entry *te; const array_header *arr; int k; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at HEADERS", log_escape(msr->r->pool, sig->pattern)); arr = ap_table_elts(msr->cache_headers_in); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { char *header_value = ap_psprintf(msr->r->pool, "%s: %s", te[k].key, te[k].val); rs = check_sig_against_string(msr, sig, header_value, VAR_HEADERS, NULL); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_HEADERS_NAMES) { table_entry *te; const array_header *arr; int k; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at HEADERS_NAMES", log_escape(msr->r->pool, sig->pattern)); arr = ap_table_elts(msr->cache_headers_in); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { rs = check_sig_against_string(msr, sig, te[k].key, VAR_HEADERS_NAMES, NULL); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_HEADERS_VALUES) { table_entry *te; const array_header *arr; int k; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at HEADERS_VALUES", log_escape(msr->r->pool, sig->pattern)); arr = ap_table_elts(msr->cache_headers_in); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { sec_debug_log(msr->r, 4, "Checking signature \"%s\" at HEADERS_VALUES(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, te[k].key)); rs = check_sig_against_string(msr, sig, te[k].val, VAR_HEADERS_VALUES, te[k].key); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_FILES_NAMES) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at FILES_NAMES", log_escape(msr->r->pool, sig->pattern)); if (msr->mpd != NULL) { rs = multipart_check_files_names(msr, sig, variables[j]); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_FILES_SIZES) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at FILES_SIZES", log_escape(msr->r->pool, sig->pattern)); if (msr->mpd != NULL) { rs = multipart_check_files_sizes(msr, sig, variables[j]); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_COOKIE) { table_entry *te; array_header *arr; int k, count = 0; count = 0; arr = ap_table_elts(msr->parsed_cookies); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { if (strcasecmp(te[k].key, variables[j]->name) == 0) { count++; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at COOKIE(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, variables[j]->name)); rs = check_sig_against_string(msr, sig, te[k].val, VAR_COOKIE, te[k].key); if (rs != OK) return rs; } } if (count == 0) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at COOKIE(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, variables[j]->name)); rs = check_sig_against_string(msr, sig, "", VAR_COOKIE, variables[j]->name); if (rs != OK) return rs; } } else if (variables[j]->type == VAR_ARG) { table_entry *te; array_header *arr; int k, count = 0; count = 0; arr = ap_table_elts(msr->parsed_args); te = (table_entry *)arr->elts; for (k = 0; k < arr->nelts; k++) { if (strcasecmp(te[k].key, variables[j]->name) == 0) { count++; sec_debug_log(msr->r, 2, "Checking signature \"%s\" at ARG(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, variables[j]->name)); rs = check_sig_against_string(msr, sig, te[k].val, VAR_ARG, te[k].key); if (rs != OK) return rs; } } if (count == 0) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at ARG(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, variables[j]->name)); rs = check_sig_against_string(msr, sig, "", VAR_ARG, variables[j]->name); if (rs != OK) return rs; } } else { /* simple variable, get the value and check it */ char *where = NULL; if (variables[j]->name == NULL) where = ap_psprintf(msr->r->pool, "%s", all_variables[variables[j]->type]); else where = ap_psprintf(msr->r->pool, "%s(%s)", all_variables[variables[j]->type], variables[j]->name); v = get_variable(msr, variables[j], variables[j]->type); if (v != NULL) { sec_debug_log(msr->r, 2, "Checking signature \"%s\" at %s", log_escape(msr->r->pool, sig->pattern), where); rs = check_sig_against_string(msr, sig, (char *)v, variables[j]->type, variables[j]->name); if (rs != OK) return rs; } else { sec_debug_log(msr->r, 1, "Variable not found \"%s\"", log_escape(msr->r->pool, where)); } } } } else { table *our_parsed_args; char *fake_body = NULL; our_parsed_args = ap_copy_table(msr->r->pool, msr->parsed_args); /* Find the unwanted variable names in the signature * data and remove them from the variable list. */ variables = (variable **)sig->variables->elts; for (j = 0; j < sig->variables->nelts; j++) { if ((variables[j]->type == VAR_ARG) && (variables[j]->action == VAR_ACTION_ALLOW)) { ap_table_unset(our_parsed_args, variables[j]->name); } } fake_body = construct_fake_urlencoded(msr, our_parsed_args); if (fake_body == NULL) { *error_msg = ap_psprintf(msr->r->pool, "Failed with construct_fake_urlencoded"); return DECLINED; } /* make the check against the compiled string */ sec_debug_log(msr->r, 2, "Checking signature \"%s\" at ARGS_SELECTIVE", log_escape(msr->r->pool, sig->pattern)); rs = check_sig_against_string(msr, sig, (char *)fake_body, VAR_ARGS_SELECTIVE, NULL); if (rs != OK) return rs; } } return OK; } int sec_exec_child(void *_ed, child_info *pinfo) { char **env = NULL; exec_data *ed = (exec_data *)_ed; request_rec *r = ed->r; ap_add_cgi_vars(r); ap_add_common_vars(r); /* TODO add mod_security specific environment variables */ /* Hack to make PHP happy; PHP is often compiled to be * run from the command line and from the web server. It * analyses environment variables to determine how it is * supposed to behave. To make things worse, if it determines * that it is being run as a CGI script, there are certain * security checks it performs. In most cases, it simply * refuses to run when started. The following two lines * are a workaround around those security checks. */ ap_table_add(r->subprocess_env, "PATH_TRANSLATED", ed->command); ap_table_add(r->subprocess_env, "REDIRECT_STATUS", "302"); env = (char **)ap_create_environment(r->pool, r->subprocess_env); if (env == NULL) { sec_debug_log(r, 1, "sec_exec_child: Failed to create environment"); return DECLINED; } ap_error_log2stderr(r->server); #if !(defined(WIN32) || defined(DISABLE_SUEXEC)) { char *command = ap_pstrdup(r->pool, ed->command); char *p; /* Suexec will complain if the name of the * script starts with a slash. To work around * that we chdir to the folder, and then execute * the script giving a relative filename. We have * already forked so we can do that. */ p = strrchr(command, '/'); if (p != NULL) { r->filename = p + 1; *p = 0; chdir(command); } else { r->filename = ed->command; } } #else r->filename = ed->command; #endif r->args = ed->args; ap_cleanup_for_exec(); my_call_exec(r, pinfo, r->filename, env); #ifdef WIN32 return(0); #else ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, NULL, "mod_security: exec failed: %s", ed->command); exit(0); return(0); #endif } int check_sig_against_string(modsec_rec *msr, signature *_sig, const char *s, int var_type, char *var_name) { request_rec *r = msr->r; int regex_result; int rc = OK; if (_sig->regex == NULL) { msr->tmp_message = ap_psprintf(r->pool, "Compiled regex for pattern \"%s\" is null!", log_escape(r->pool, _sig->pattern)); return perform_action(msr, msr->dcfg->actionset, _sig); } if (s == NULL) { msr->tmp_message = ap_psprintf(r->pool, "check_sig_against_sig: Internal Error: received null for argument"); return perform_action(msr, msr->dcfg->actionset, _sig);; } sec_debug_log(r, 9, "Checking against \"%s\"", log_escape(msr->r->pool, (char *)s)); regex_result = my_regexec(_sig->regex, s); /* regexec returns 0 on success or REG_NOMATCH on failure */ if ( ((regex_result == 0)&&(_sig->is_allow == 0)) || ((regex_result != 0)&&(_sig->is_allow == 1)) ) { if (var_name == NULL) msr->tmp_message = ap_psprintf(msr->r->pool, "Pattern match \"%s\" at %s", log_escape(r->pool, _sig->pattern), all_variables[var_type]); else msr->tmp_message = ap_psprintf(msr->r->pool, "Pattern match \"%s\" at %s(\"%s\")", log_escape(r->pool, _sig->pattern), all_variables[var_type], log_escape(r->pool, var_name)); rc = perform_action(msr, msr->dcfg->actionset, _sig); } return rc; } char *process_action(char *name, char *value, actionset_t *actionset, pool *_pool) { if ((value != NULL)&&(strlen(value) == 0)) value = NULL; if (strcmp(name, "log") == 0) { actionset->log = 1; } else if (strcmp(name, "nolog") == 0) { actionset->log = 0; } else if (strcmp(name, "auditlog") == 0) { actionset->auditlog = 1; } else if (strcmp(name, "noauditlog") == 0) { actionset->auditlog = 0; } else if (strcmp(name, "status") == 0) { actionset->action = ACTION_DENY; if (value != NULL) { actionset->status = atoi(value); } else { return ap_psprintf(_pool, "Action \"status\" requires a parameter"); } } else if ((strcmp(name, "chain") == 0)||(strcmp(name, "chained") == 0)) { actionset->is_chained = 1; } else if ((strcmp(name, "skipnext") == 0)||(strcmp(name, "skip") == 0)) { actionset->action = ACTION_SKIP; actionset->skip_count = 1; if (value != NULL) { actionset->skip_count = atoi(value); if (actionset->skip_count <= 0) return ap_psprintf(_pool, "Invalid value for action '%s': %s", name, value); } } else if (strcmp(name, "deny") == 0) { actionset->action = ACTION_DENY; } else if (strcmp(name, "allow") == 0) { actionset->action = ACTION_ALLOW; } else if (strcmp(name, "pass") == 0) { actionset->action = ACTION_NONE; } else if (strcmp(name, "exec") == 0) { actionset->exec = 1; if (value != NULL) { actionset->exec_string = ap_pstrdup(_pool, value); } else { return ap_psprintf(_pool, "Action \"exec\" requires a parameter"); } } else if (strcmp(name, "redirect") == 0) { actionset->action = ACTION_REDIRECT; if (value != NULL) { actionset->redirect_url = ap_pstrdup(_pool, value); } else { return ap_psprintf(_pool, "Action \"redirect\" requires a parameter"); } } else if (strcmp(name, "proxy") == 0) { actionset->action = ACTION_PROXY; if (value != NULL) { actionset->proxy_url = ap_pstrdup(_pool, value); } else { return ap_psprintf(_pool, "Action \"proxy\" requires a parameter"); } } else if (strcmp(name, "mandatory") == 0) { actionset->mandatory = 1; } else if (strcmp(name, "msg") == 0) { if (value != NULL) { actionset->msg = ap_pstrdup(_pool, value); } else { return ap_psprintf(_pool, "Action \"msg\" requires a parameter"); } } else if (strcmp(name, "id") == 0) { if (value != NULL) { actionset->id = ap_pstrdup(_pool, value); } else { return ap_psprintf(_pool, "Action \"id\" requires a parameter"); } } else if (strcmp(name, "rev") == 0) { if (value != NULL) { actionset->rev = ap_pstrdup(_pool, value); } else { return ap_psprintf(_pool, "Action \"rev\" requires a parameter"); } } else if (strcmp(name, "severity") == 0) { if (value != NULL) { actionset->severity = parse_severity(value); if (actionset->severity == -1) { return ap_psprintf(_pool, "Invalid value for severity: %s", value); } } else { return ap_psprintf(_pool, "Action \"severity\" requires a parameter"); } } else if (strcmp(name, "pause") == 0) { if (value != NULL) { actionset->pause = atoi(value); if (actionset->pause <= 0) { return ap_psprintf(_pool, "Invalid value for action 'pause': %s", value); } } else { return ap_psprintf(_pool, "Action \"pause\" requires a parameter"); } } else if (strcmp(name, "setenv") == 0) { if (value != NULL) { char *peq = NULL; actionset->env_name = ap_pstrdup(_pool, value); peq = strstr(actionset->env_name, "="); if (peq != NULL) { actionset->env_value = peq + 1; *peq = 0; /* terminate env_name */ } else { /* missing value, assume "1" */ actionset->env_value = "1"; } } else { return ap_psprintf(_pool, "Action \"setenv\" requires a parameter"); } } else if (strcmp(name, "setnote") == 0) { if (value != NULL) { char *peq = NULL; actionset->note_name = ap_pstrdup(_pool, value); peq = strstr(actionset->note_name, "="); if (peq != NULL) { actionset->note_value = peq + 1; *peq = 0; /* terminate note_name */ } else { /* missing value, assume "1" */ actionset->note_value = "1"; } } } else if (strcmp(name, "logparts") == 0) { if (value != NULL) { if (value[0] == '+') { actionset->logparts = RELATIVE_VALUE_POSITIVE; actionset->logparts_value = ap_pstrdup(_pool, value + 1); } else if (value[0] == '-') { actionset->logparts = RELATIVE_VALUE_NEGATIVE; actionset->logparts_value = ap_pstrdup(_pool, value + 1); } else { actionset->logparts = ABSOLUTE_VALUE; actionset->logparts_value = ap_pstrdup(_pool, value); } if (is_valid_parts_specification(actionset->logparts_value) != 1) { return ap_psprintf(_pool, "Invalid parts specification: %s", actionset->logparts_value); } } else { return ap_psprintf(_pool, "Action \"logparts\" requires a parameter"); } } else { return ap_psprintf(_pool, "Unknown action: %s", name); } return NULL; } char *parse_actionset(char *p2, actionset_t *actionset, pool *_pool) { char *p, *t = ap_pstrdup(_pool, p2); char *name, *name_end, *value, *rc; p = t; while(*p != '\0') { name = NULL; value = NULL; /* ignore whitespace */ while(isspace(*p)) p++; if (*p == '\0') return NULL; /* we are at the beginning of a name */ name = p; while((*p != '\0')&&(*p != ':')&&(*p != ',')&&(!isspace(*p))) p++; name_end = p; while(isspace(*p)) p++; if (*p == '\0') { return process_action(name, value, actionset, _pool);; } if (*p == ',') { *name_end = '\0'; rc = process_action(name, value, actionset, _pool); if (rc != NULL) return rc; p++; continue; } if (*p != ':') { return ap_psprintf(_pool, "Invalid action list, colon or comma expected at position %i: %s", (int)(p - t), p2); } *name_end = '\0'; /* ignore whitespace after colon */ p++; while(isspace(*p)) p++; /* we'll allow empty values */ if (*p == '\0') { return process_action(name, value, actionset, _pool); } if (*p == ',') { rc = process_action(name, value, actionset, _pool); if (rc != NULL) return rc; continue; } if (*p == '\'') { /* quoted value */ char *d = NULL; p++; /* go over the openning quote */ value = d = p; for(;;) { if (*p == '\0') { return ap_psprintf(_pool, "Invalid action list, missing closing quote: %s", p2); } else if (*p == '\\') { if ( (*(p + 1) == '\0') || ((*(p + 1) != '\'')&&(*(p + 1) != '\\')) ) { return ap_psprintf(_pool, "Invalid quoting in the action list"); } p++; *d++ = *p++; } else if (*p == '\'') { *d = '\0'; p++; break; } else { *d++ = *p++; } } } else { /* non-quoted value */ value = p; while((*p != '\0')&&(*p != ',')&&(!isspace(*p))) p++; if (*p != '\0') { *p++ = '\0'; /* write over the comma or a space */ } } rc = process_action(name, value, actionset, _pool); if (rc != NULL) return rc; while(isspace(*p)||(*p == ',')) p++; } /* Chained rules must always try to deny * access in order for chaining to work * properly */ if (actionset->is_chained) { actionset->action = ACTION_DENY; actionset->status = HTTP_FORBIDDEN; } return NULL; } static const char *cmd_filter_engine(cmd_parms *cmd, sec_dir_config* dcfg, char *p1) { if (strcasecmp(p1, "On") == 0) dcfg->filter_engine = FILTERING_ON; else if (strcasecmp(p1, "Off") == 0) dcfg->filter_engine = FILTERING_OFF; else if (strcasecmp(p1, "DynamicOnly") == 0) dcfg->filter_engine = FILTERING_DYNAMIC_ONLY; else return ap_psprintf(cmd->pool, "Unrecognized parameter value for SecFilterEngine: %s", p1); return NULL; } static const char *cmd_filter_inheritance(cmd_parms *cmd, sec_dir_config* dcfg, int flag) { if (flag) dcfg->filters_clear = 0; else dcfg->filters_clear = 1; return NULL; } static const char *cmd_server_response_token(cmd_parms *cmd, sec_dir_config *dcfg, int flag) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(cmd->server->module_config, &security_module); if (cmd->server->is_virtual) { return "SecServerResponseToken not allowed in VirtualHost"; } scfg->server_response_token = flag; return NULL; } static const char *cmd_audit_engine(cmd_parms *cmd, sec_dir_config * dcfg, char *p1) { if (strcasecmp(p1, "On") == 0) dcfg->auditlog_flag = AUDITLOG_ON; else if (strcasecmp(p1, "Off") == 0) dcfg->auditlog_flag = AUDITLOG_OFF; else if (strcasecmp(p1, "RelevantOnly") == 0) dcfg->auditlog_flag = AUDITLOG_RELEVANT_ONLY; else if (strcasecmp(p1, "DynamicOrRelevant") == 0) dcfg->auditlog_flag = AUDITLOG_DYNAMIC_OR_RELEVANT; else return ap_psprintf(cmd->pool, "Unrecognized parameter value for SecAuditEngine: %s", p1); return NULL; } static const char *cmd_audit_log(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { dcfg->auditlog_name = p1; if (dcfg->auditlog_name[0] == '|') { char *pipe_name = ap_server_root_relative(cmd->pool, dcfg->auditlog_name + 1); piped_log *pipe_log; pipe_log = ap_open_piped_log(cmd->pool, pipe_name); if (pipe_log == NULL) { return ap_psprintf(cmd->pool, "mod_security: Failed to open audit log pipe: %s", pipe_name); } dcfg->auditlog_fd = ap_piped_log_write_fd(pipe_log); } else { char *file_name = ap_server_root_relative(cmd->pool, dcfg->auditlog_name); #ifdef WIN32 dcfg->auditlog_fd = open(dcfg->auditlog_name, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); #else dcfg->auditlog_fd = ap_popenf(cmd->pool, file_name, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); #endif if (dcfg->auditlog_fd < 0) { return ap_psprintf(cmd->pool, "mod_security: Failed to open the audit log file: %s", file_name); } } return NULL; } static const char *cmd_audit_log_type(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { if (strcasecmp(p1, "Serial") == 0) dcfg->auditlog_type = AUDITLOG_SERIAL; else if (strcasecmp(p1, "Concurrent") == 0) dcfg->auditlog_type = AUDITLOG_CONCURRENT; else return (const char *)ap_psprintf(cmd->pool, "Unrecognised parameter value for SecAuditLogType: %s", p1); return NULL; } static const char *cmd_audit_log_storage_dir(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { dcfg->auditlog_storage_dir = ap_server_root_relative(cmd->pool, p1); return NULL; } static const char *cmd_audit_log_parts(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { if (is_valid_parts_specification((char *)p1) != 1) { return ap_psprintf(cmd->pool, "Invalid parts specification for SecAuditLogParts: %s", p1); } dcfg->auditlog_parts = (char *)p1; return NULL; } static const char *cmd_upload_dir(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { if (strcasecmp(p1, "none") == 0) dcfg->upload_dir = NULL; else dcfg->upload_dir = ap_server_root_relative(cmd->pool, p1); /* TODO does the directory exist? */ return NULL; } static const char *cmd_upload_approve_script(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { if (strcasecmp(p1, "none") == 0) dcfg->upload_approve_script = NULL; else dcfg->upload_approve_script = p1; /* TODO does the script exist? */ return NULL; } static const char *cmd_upload_keep_files(cmd_parms * cmd, sec_dir_config * dcfg, char *p1) { if (strcasecmp(p1, "on") == 0) dcfg->upload_keep_files = KEEP_FILES_ON; else if (strcasecmp(p1, "off") == 0) dcfg->upload_keep_files = KEEP_FILES_OFF; else if (strcasecmp(p1, "relevantonly") == 0) dcfg->upload_keep_files = KEEP_FILES_RELEVANT_ONLY; else { return ap_psprintf(cmd->pool, "Unknown option: %s", p1); } return NULL; } static const char *cmd_scan_post(cmd_parms * cmd, sec_dir_config * dcfg, int flag) { dcfg->scan_post = flag; return NULL; } static const char *cmd_filter_check_url_encoding(cmd_parms * cmd, sec_dir_config *dcfg, int flag) { dcfg->check_encoding = flag; return NULL; } static const char *cmd_filter_check_unicode_encoding(cmd_parms * cmd, sec_dir_config *dcfg, int flag) { dcfg->check_unicode_encoding = flag; return NULL; } static const char *cmd_filter_force_byte_range(cmd_parms *cmd, sec_dir_config *dcfg, char *p1, char *p2) { dcfg->range_start = atoi(p1); dcfg->range_end = atoi(p2); if ((dcfg->range_start < 0)||(dcfg->range_end > 255)||(dcfg->range_start >= dcfg->range_end)) { return (const char *)ap_psprintf(cmd->pool, "Invalid range"); } return NULL; } static const char *cmd_default_action(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { char *rc = NULL; dcfg->actionset = (actionset_t *)ap_pcalloc(cmd->pool, sizeof(actionset_t)); init_default_actionset(dcfg->actionset); rc = parse_actionset(p1, dcfg->actionset, cmd->pool); if (rc != NULL) return rc; if ((dcfg->actionset->id != NULL)||(dcfg->actionset->rev != NULL) ||(dcfg->actionset->is_chained)||(dcfg->actionset->action == ACTION_SKIP) ) { return "Actions id, rev, chained, and skip are not allowed in SecFilterDefaultAction"; } return NULL; } static const char *cmd_signature_action(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { char *rc; dcfg->actionset_signatures = (actionset_t *)ap_pcalloc(cmd->pool, sizeof(actionset_t)); init_default_actionset(dcfg->actionset_signatures); rc = parse_actionset((char *)p1, dcfg->actionset_signatures, cmd->pool); if (rc != NULL) return rc; if ((dcfg->actionset_signatures->id != NULL)||(dcfg->actionset_signatures->rev != NULL) ||(dcfg->actionset_signatures->is_chained)||(dcfg->actionset_signatures->action == ACTION_SKIP) ) { return "Actions id, rev, chained, and skip are not allowed in SecFilterSignatureAction"; } return NULL; } static char *create_per_rule_actionset(cmd_parms *cmd, sec_dir_config *dcfg, signature *sig, char *config, actionset_t *actionset) { char *rc = NULL; init_empty_actionset(actionset); if (config == NULL) { parse_actionset("", actionset, cmd->pool); return NULL; } rc = parse_actionset((char *)config, actionset, cmd->pool); if (rc != NULL) return rc; /* the id and msg actions can only be used on a rule that is * starting a chain, or on a standalone rule */ if ((actionset->mandatory)||(actionset->id != NULL) ||(actionset->rev != NULL)||(actionset->severity != NOT_SET) ) { signature **signatures = NULL, *previous_signature = NULL; int i; /* go back, ignoring placeholders, and look for the rule before this one */ signatures = (signature **)dcfg->signatures->elts; for (i = dcfg->signatures->nelts - 1; i >= 0; i--) { if (signatures[i]->is_inheritance_placeholder != 0) continue; previous_signature = signatures[i]; break; } if ((previous_signature != NULL) && (previous_signature->actionset != NULL) && (previous_signature->actionset->is_chained)) { if (actionset->mandatory) return "Action \"mandatory\" cannot be used on a chained rule that did not start the chain"; if (actionset->id != NULL) return "Action \"id\" cannot be used on a chained rule that did not start the chain"; if (actionset->rev != NULL) return "Action \"rev\" cannot be used on a chained rule that did not start the chain"; if (actionset->severity != NOT_SET) return "Action \"severity\" cannot be used on a chained rule that did not start the chain"; } } return NULL; } static const char *cmd_filter(cmd_parms *cmd, sec_dir_config *dcfg, char *_p1, char *p2) { char *p1 = unescape_regex_hex_inplace(ap_pstrdup(cmd->pool, _p1)); signature *sig; sig = ap_pcalloc(cmd->pool, sizeof(signature)); if (sig == NULL) return FATAL_ERROR; sig->actions_restricted = dcfg->actions_restricted; sig->actionset = NULL; /* p1 is the regular expression string */ if (p1[0] == '!') { sig->is_allow = 1; sig->pattern = _p1; sig->regex = my_pregcomp(cmd->pool, p1 + 1); } else { sig->pattern = _p1; sig->regex = my_pregcomp(cmd->pool, p1); } if (sig->regex == NULL) { return ap_psprintf(cmd->pool, "Invalid regular expression: %s", sig->pattern); } if (p2 != NULL) { actionset_t *signature_actionset = NULL; char *error_message = NULL; signature_actionset = (actionset_t *)ap_pcalloc(cmd->pool, sizeof(actionset_t)); if (dcfg->actionset_signatures != NOT_SET_P) { error_message = create_per_rule_actionset(cmd, dcfg, sig, (char *)p2, signature_actionset); if (error_message != NULL) return error_message; sig->actionset = merge_actionsets(cmd->pool, dcfg->actionset_signatures, signature_actionset, dcfg->actions_restricted); if (sig->actionset == NULL) return "Failed to merge actionsets"; } else { actionset_t temporary_actionset; init_default_actionset(&temporary_actionset); error_message = create_per_rule_actionset(cmd, dcfg, sig, (char *)p2, signature_actionset); if (error_message != NULL) return error_message; sig->actionset = merge_actionsets(cmd->pool, &temporary_actionset, signature_actionset, dcfg->actions_restricted); if (sig->actionset == NULL) return "Failed to merge actionsets"; } if ((sig->actionset->action == ACTION_SKIP)&&(sig->actionset->is_chained)) { return "Not possible to use \"skip\" with a chained rule"; } } else { if (dcfg->actionset_signatures != NOT_SET_P) { sig->actionset = (actionset_t *)ap_pcalloc(cmd->pool, sizeof(actionset_t)); memcpy(sig->actionset, dcfg->actionset_signatures, sizeof(actionset_t)); } } /* find a pointer to the first rule in the chain */ if (dcfg->signatures->nelts != 0) { signature **psignatures = (signature **)dcfg->signatures->elts; signature *prevsig = psignatures[dcfg->signatures->nelts - 1]; if ((prevsig->actionset != NULL)&&(prevsig->actionset->is_chained)) { if (prevsig->first_sig_in_chain != NULL) sig->first_sig_in_chain = prevsig->first_sig_in_chain; else sig->first_sig_in_chain = prevsig; } } /* add the signature to the list of all signatures */ *(signature **)ap_push_array(dcfg->signatures) = sig; return NULL; } static const char *cmd_filter_debug_log(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { dcfg->debuglog_name = ap_server_root_relative(cmd->pool, p1); #ifdef WIN32 dcfg->debuglog_fd = open(dcfg->debuglog_name, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); #else dcfg->debuglog_fd = ap_popenf(cmd->pool, dcfg->debuglog_name, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); #endif if (dcfg->debuglog_fd < 0) { return ap_psprintf(cmd->pool, "mod_security: Failed to open the debug log file: %s", dcfg->debuglog_name); } return NULL; } static const char *cmd_filter_debug_level(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { dcfg->filter_debug_level = atoi(p1); return NULL; } static const char *cmd_filter_selective(cmd_parms *cmd, sec_dir_config *dcfg, char *p1, char *_p2, char *p3) { char *p2 = unescape_regex_hex_inplace(ap_pstrdup(cmd->pool, _p2)); char *p, *t; signature *sig; char *saveptr; /* initialise the structure first */ sig = ap_pcalloc(cmd->pool, sizeof(signature)); if (sig == NULL) return FATAL_ERROR; sig->actions_restricted = dcfg->actions_restricted; sig->actionset = NULL; sig->is_allow = 0; sig->is_selective = 1; sig->is_negative = 0; sig->requires_parsed_args = 0; sig->variables = ap_make_array(cmd->pool, 10, sizeof (variable *)); sig->type = SIGNATURE_NORMAL; if (p2[0] == '!') { sig->is_allow = 1; sig->pattern = _p2; sig->regex = my_pregcomp(cmd->pool, p2 + 1); } else { sig->pattern = _p2; sig->regex = my_pregcomp(cmd->pool, p2); } if (sig->regex == NULL) { return ap_psprintf(cmd->pool, "Invalid regular expression: %s", sig->pattern); } /* split parameter 1 apart and extract variable names */ p = strdup(p1); t = strtok_r(p, "|", &saveptr); while (t != NULL) { char *x = t; /* add the token to the list */ variable *v = (variable *)ap_pcalloc(cmd->pool, sizeof(variable)); if (v == NULL) return FATAL_ERROR; v->type = VAR_UNKNOWN; v->name = NULL; /* when ! is the first character in the variable * name that means that the restrictions need to be * relaxed for that variable (within the filter scope) */ if (t[0] == '!') { v->action = VAR_ACTION_ALLOW; sig->is_negative = 1; sig->requires_parsed_args = 1; x++; } else { v->action = VAR_ACTION_DENY; } /* arguments */ if (strncmp(x, "ARG_", 4) == 0) { v->type = VAR_ARG; v->name = ap_pstrdup(cmd->pool, x + 4); sig->requires_parsed_args = 1; } /* HTTP headers */ else if (strncmp(x, "HTTP_", 5) == 0) { char *px; v->type = VAR_HEADER; v->name = ap_pstrdup(cmd->pool, x + 5); /* replace all "_" with "-" */ px = v->name; while (*px != 0) { if (*px == '_') *px = '-'; px++; } } /* HTTP headers again, but a different name */ else if (strncmp(x, "HEADER_", 7) == 0) { char *px; v->type = VAR_HEADER; v->name = ap_pstrdup(cmd->pool, x + 7); /* replace all "_" with "-" */ px = v->name; while (*px != 0) { if (*px == '_') *px = '-'; px++; } } /* custom file name */ else if (strncmp(x, "FILE_NAME_", 10) == 0) { v->type = VAR_FILE_NAME; v->name = ap_pstrdup(cmd->pool, x + 10); } /* custom file size */ else if (strncmp(x, "FILE_SIZE_", 10) == 0) { v->type = VAR_FILE_SIZE; v->name = ap_pstrdup(cmd->pool, x + 10); } /* COOKIES */ else if (strncmp(x, "COOKIE_", 7) == 0) { v->type = VAR_COOKIE; v->name = ap_pstrdup(cmd->pool, x + 7); } /* environment variables */ else if (strncmp(x, "ENV_", 4) == 0) { v->type = VAR_ENV; v->name = ap_pstrdup(cmd->pool, x + 4); } /* all arguments */ else if (strcmp(x, "ARGS") == 0) { v->type = VAR_ARGS; v->name = ap_pstrdup(cmd->pool, x); } /* just the post payload */ else if (strcmp(x, "POST_PAYLOAD") == 0) { v->type = VAR_POST_PAYLOAD; v->name = ap_pstrdup(cmd->pool, x); } /* everything else */ else { int i = 0; while (all_variables[i] != NULL) { if (strcmp(all_variables[i], x) == 0) { v->type = i; /* v->name = ap_pstrdup(cmd->pool, x); */ break; } i++; } } if ((v->type == VAR_OUTPUT)||(v->type == VAR_OUTPUT_STATUS)) { return ap_psprintf(cmd->pool, "Variables OUTPUT and OUTPUT_STATUS are not supported in the Apache 1.x version"); } if (v->type == VAR_UNKNOWN) { v->name = ap_pstrdup(cmd->pool, "UKNOWN"); return ap_psprintf(cmd->pool, "Unknown variable name: %s", x); } if ((v->type == VAR_ARGS_NAMES)||(v->type == VAR_ARGS_VALUES)) sig->requires_parsed_args = 1; *(variable **)ap_push_array(sig->variables) = v; /* and proceed to the next token */ t = strtok_r(NULL, "|", &saveptr); } free(p); if (p3 != NULL) { actionset_t *signature_actionset = NULL; char *error_message = NULL; signature_actionset = (actionset_t *)ap_pcalloc(cmd->pool, sizeof(actionset_t)); if (dcfg->actionset_signatures != NOT_SET_P) { error_message = create_per_rule_actionset(cmd, dcfg, sig, (char *)p3, signature_actionset); if (error_message != NULL) return error_message; sig->actionset = merge_actionsets(cmd->pool, dcfg->actionset_signatures, signature_actionset, dcfg->actions_restricted); if (sig->actionset == NULL) return "Failed to merge actionsets"; } else { actionset_t temporary_actionset; init_default_actionset(&temporary_actionset); error_message = create_per_rule_actionset(cmd, dcfg, sig, (char *)p3, signature_actionset); if (error_message != NULL) return error_message; sig->actionset = merge_actionsets(cmd->pool, &temporary_actionset, signature_actionset, dcfg->actions_restricted); if (sig->actionset == NULL) return "Failed to merge actionsets"; } if ((sig->actionset->action == ACTION_SKIP)&&(sig->actionset->is_chained)) { return "Not possible to use \"skip\" with a chained rule"; } } else { if (dcfg->actionset_signatures != NOT_SET_P) { sig->actionset = (actionset_t *)ap_pcalloc(cmd->pool, sizeof(actionset_t)); memcpy(sig->actionset, dcfg->actionset_signatures, sizeof(actionset_t)); } } /* find a pointer to the first rule in the chain */ if (dcfg->signatures->nelts != 0) { signature **psignatures = (signature **)dcfg->signatures->elts; signature *prevsig = psignatures[dcfg->signatures->nelts - 1]; if ((prevsig->actionset != NULL)&&(prevsig->actionset->is_chained)) { if (prevsig->first_sig_in_chain != NULL) sig->first_sig_in_chain = prevsig->first_sig_in_chain; else sig->first_sig_in_chain = prevsig; } } /* add the signature to the list of all signatures */ *(signature **)ap_push_array(dcfg->signatures) = sig; return NULL; } static const char *cmd_chroot_dir(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(cmd->server->module_config, &security_module); char cwd[1025] = ""; if (cmd->server->is_virtual) { return "SecChrootDir not allowed in VirtualHost"; } scfg->chroot_dir = p1; if (getcwd(cwd, 1024) == NULL) { return "SecChrootDir: failed to get the current working directory"; } if (chdir(scfg->chroot_dir) < 0) { return ap_psprintf(cmd->pool, "SecChrootDir: failed to chdir to \"%s\", errno=%d(%s)", scfg->chroot_dir, errno, strerror(errno)); } if (chdir(cwd) < 0) { return ap_psprintf(cmd->pool, "SecChrootDir: failed to chdir to \"%s\", errno=%d(%s)", cwd, errno, strerror(errno)); } return NULL; } static const char *cmd_chroot_lock(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(cmd->server->module_config, &security_module); if (cmd->server->is_virtual) { return "SecChrootLock not allowed in VirtualHost"; } scfg->chroot_lock = ap_server_root_relative(cmd->pool, p1); if (scfg->chroot_lock == NULL) { return "SecChrootLock: allocation failed"; } return NULL; } static const char *cmd_server_signature(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(cmd->server->module_config, &security_module); if (cmd->server->is_virtual) { return "SecServerSignature not allowed in VirtualHost"; } scfg->server_signature = p1; return NULL; } static const char *cmd_normalize_cookies(cmd_parms * cmd, sec_dir_config * dcfg, int flag) { dcfg->normalize_cookies = flag; return NULL; } static const char *cmd_check_cookie_format(cmd_parms * cmd, sec_dir_config * dcfg, int flag) { dcfg->check_cookie_format = flag; return NULL; } static const char *cmd_cookie_format(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { if (strcmp(p1, "0") == 0) dcfg->cookie_format = COOKIES_V0; else if (strcmp(p1, "1") == 0) dcfg->cookie_format = COOKIES_V1; else { return ap_psprintf(cmd->pool, "Unknown cookie format: %s", p1); } return NULL; } static const char *cmd_charset(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { dcfg->charset_id = convert_charset_to_id(p1); if (dcfg->charset_id == -1) { return ap_psprintf(cmd->pool, "Unknown charset: %s", p1); } return NULL; } static const char *cmd_filter_import(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { signature *sig; /* initialise the structure first */ sig = ap_pcalloc(cmd->pool, sizeof(signature)); if (sig == NULL) return FATAL_ERROR; sig->is_inheritance_placeholder = INHERITANCE_IMPORT; sig->inheritance_id = p1; /* add the signature to the list of all signatures */ *(signature **)ap_push_array(dcfg->signatures) = sig; return NULL; } static const char *cmd_filter_remove(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { signature *sig; /* initialise the structure first */ sig = ap_pcalloc(cmd->pool, sizeof(signature)); if (sig == NULL) return FATAL_ERROR; sig->is_inheritance_placeholder = INHERITANCE_REMOVE; sig->inheritance_id = p1; /* add the signature to the list of all signatures */ *(signature **)ap_push_array(dcfg->signatures) = sig; return NULL; } static const char *cmd_filter_inheritance_mandatory(cmd_parms *cmd, sec_dir_config *dcfg, int flag) { dcfg->inheritance_mandatory = flag; return NULL; } static const char *cmd_filter_actions_restricted(cmd_parms *cmd, sec_dir_config *dcfg, int flag) { dcfg->actions_restricted = flag; return NULL; } static const char *cmd_guardian_log(cmd_parms *cmd, sec_dir_config *dcfg, char *p1, char *p2) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(cmd->server->module_config, &security_module); if (cmd->server->is_virtual) { return "SecGuardianLog not allowed in VirtualHost"; } if (p2 != NULL) { if (strncmp(p2, "env=", 4) != 0) return "Error in condition clause"; if ( (p2[4] == '\0') || ((p2[4] == '!')&&(p2[5] == '\0')) ) return "Missing variable name"; scfg->guardian_log_condition = ap_pstrdup(cmd->pool, p2 + 4); } scfg->guardian_log_name = p1; if (scfg->guardian_log_name[0] == '|') { char *pipe_name = ap_server_root_relative(cmd->pool, scfg->guardian_log_name + 1); piped_log *pipe_log; pipe_log = ap_open_piped_log(cmd->pool, pipe_name); if (pipe_log == NULL) { return ap_psprintf(cmd->pool, "mod_security: Failed to open guardian pipe: %s", pipe_name); } scfg->guardian_log_fd = ap_piped_log_write_fd(pipe_log); } else { char *file_name = ap_server_root_relative(cmd->pool, scfg->guardian_log_name); #ifdef WIN32 scfg->guardian_log_fd = open(file_name, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); #else scfg->guardian_log_fd = ap_popenf(cmd->pool, file_name, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); #endif if (scfg->guardian_log_fd < 0) { return ap_psprintf(cmd->pool, "mod_security: Failed to open guardian log file: %s", file_name); } } return NULL; } static const char *cmd_audit_log_relevant_status(cmd_parms *cmd, sec_dir_config *dcfg, char *p1) { dcfg->auditlog_relevant_regex = my_pregcomp(cmd->pool, p1); if (dcfg->auditlog_relevant_regex == NULL) { return ap_psprintf(cmd->pool, "Invalid regular expression: %s", p1); } return NULL; } static const command_rec sec_cmds[] = { { "SecFilter", cmd_filter, NULL, CMD_SCOPE_ANY, TAKE12, "The filtering expression" }, { "SecFilterDebugLog", cmd_filter_debug_log, NULL, CMD_SCOPE_ANY, TAKE1, "The filename of the filter debugging log file" }, { "SecFilterDebugLevel", cmd_filter_debug_level, NULL, CMD_SCOPE_ANY, TAKE1, "The level of the debugging log file verbosity" }, { "SecFilterSelective", cmd_filter_selective, NULL, CMD_SCOPE_ANY, TAKE23, "The variable representing areas where filtering is wanted, the filtering regular expression and optional action to take on match" }, { "SecFilterEngine", cmd_filter_engine, NULL, CMD_SCOPE_ANY, TAKE1, "On, Off, or DynamicOnly to determine when will request be filtered" }, { "SecServerResponseToken", cmd_server_response_token, NULL, RSRC_CONF, FLAG, "On or Off to set whether the mod_security token will appear in the server signature" }, { "SecFilterScanPOST", cmd_scan_post, NULL, CMD_SCOPE_ANY, FLAG, "On or Off to set whether a request body will be processed" }, { "SecFilterDefaultAction", cmd_default_action, NULL, CMD_SCOPE_ANY, TAKE1, "The default action to take on rule match" }, { "SecFilterSignatureAction", cmd_signature_action, NULL, CMD_SCOPE_ANY, TAKE1, "Base action template for signatures that follow this directive" }, { "SecFilterInheritance", cmd_filter_inheritance, NULL, CMD_SCOPE_ANY, FLAG, "On or Off to set whether rules from the parent context will be inherited" }, { "SecAuditEngine", cmd_audit_engine, NULL, CMD_SCOPE_ANY, TAKE1, "On, Off, RelevantOnly or DynamicOrRelevent to determine the level of audit logging" }, { "SecAuditLog", cmd_audit_log, NULL, CMD_SCOPE_ANY, TAKE1, "The filename of the audit log file" }, { "SecUploadDir", cmd_upload_dir, NULL, CMD_SCOPE_ANY, TAKE1, "The path to the directory where uploaded files should be stored" }, { "SecUploadKeepFiles", cmd_upload_keep_files, NULL, CMD_SCOPE_ANY, TAKE1, "On or Off to choose whether to keep the uploaded files or not" }, { "SecUploadApproveScript", cmd_upload_approve_script, NULL, CMD_SCOPE_ANY, TAKE1, "The path to the script that will be called to approve every uploaded file" }, { "SecFilterCheckURLEncoding", cmd_filter_check_url_encoding, NULL, CMD_SCOPE_ANY, FLAG, "On or Off to set whether URL encoding validation will be performed" }, { "SecFilterCheckUnicodeEncoding", cmd_filter_check_unicode_encoding, NULL, CMD_SCOPE_ANY, FLAG, "On or Off to set whether Unicode encoding validation will be performed" }, { "SecFilterForceByteRange", cmd_filter_force_byte_range, NULL, CMD_SCOPE_ANY, TAKE2, "The first and the last byte value of the range that will be accepted" }, { "SecChrootDir", cmd_chroot_dir, NULL, RSRC_CONF, TAKE1, "The path of the directory to which server will be chrooted" }, { "SecChrootLock", cmd_chroot_lock, NULL, RSRC_CONF, TAKE1, "The filename of the lock file used during the chroot process, defaults to \"logs/modsec_chroot.lock\"" }, { "SecServerSignature", cmd_server_signature, NULL, RSRC_CONF, TAKE1, "The new signature of the server" }, { "SecFilterNormalizeCookies", cmd_normalize_cookies, NULL, CMD_SCOPE_ANY, FLAG, "On or Off to determine whether cookie values will be normalized for testing, defaults to On" }, { "SecFilterCheckCookieFormat", cmd_check_cookie_format, NULL, CMD_SCOPE_ANY, FLAG, "On or Off to determine whether cookie format will be checked. Defaults to On" }, { "SecFilterCookieFormat", cmd_cookie_format, NULL, CMD_SCOPE_ANY, TAKE1, "version of the Cookie specification to use for parsing. Possible values are 0 and 1." }, { "SecCharset", cmd_charset, NULL, CMD_SCOPE_ANY, TAKE1, "Configures the charset" }, { "SecFilterImport", cmd_filter_import, NULL, CMD_SCOPE_ANY, ITERATE, "imports a rule from the parent configuration context." }, { "SecFilterRemove", cmd_filter_remove, NULL, CMD_SCOPE_ANY, ITERATE, "removes a rule that was inherited from the parent configuration context." }, { "SecFilterInheritanceMandatory", cmd_filter_inheritance_mandatory, NULL, CMD_SCOPE_ANY, FLAG, "when this directive is set to On then the rules in the parent context cannot be removed from a child context." }, { "SecGuardianLog", cmd_guardian_log, NULL, RSRC_CONF, TAKE12, "The filename of the filter debugging log file" }, { "SecAuditLogType", cmd_audit_log_type, NULL, CMD_SCOPE_ANY, TAKE1, "whether to use the old audit log format (Serial) or new (Concurrent)" }, { "SecAuditLogStorageDir", cmd_audit_log_storage_dir, NULL, CMD_SCOPE_ANY, TAKE1, "path to the audit log storage area; absolute, or relative to the root of the server" }, { "SecAuditLogParts", cmd_audit_log_parts, NULL, CMD_SCOPE_ANY, TAKE1, "list of audit log parts that go into the log." }, { "SecAuditLogRelevantStatus", cmd_audit_log_relevant_status, NULL, CMD_SCOPE_ANY, TAKE1, "regular expression that will be used to determine if the response status is relevant for audit logging" }, { "SecFilterActionsRestricted", cmd_filter_actions_restricted, NULL, CMD_SCOPE_ANY, FLAG, "whether to allow rules to override SecFiltersDefaultAction configuration" }, { NULL } }; int is_response_status_relevant(request_rec *r, sec_dir_config *dcfg, int status) { char *status_as_string; int regex_result; if (dcfg == NULL) return 0; if ((dcfg->auditlog_relevant_regex == NULL)||(dcfg->auditlog_relevant_regex == NOT_SET_P)) return 0; status_as_string = ap_psprintf(r->pool, "%i", status); if (status_as_string == NULL) return 0; regex_result = my_regexec(dcfg->auditlog_relevant_regex, status_as_string); if (regex_result == REG_NOMATCH) { sec_debug_log(r, 4, "Audit log: Status %i considered not relevant", status); return 0; } sec_debug_log(r, 3, "Audit log: Status %i considered relevant", status); return 1; } /** * This function is the main entry point for logging. */ static int sec_logger(request_rec *r) { const array_header *arr = NULL; request_rec *origr = NULL; modsec_rec *msr = NULL; sec_debug_log(r, 4, "Logging phase starting"); /* -- Initialise logging -- */ /* Find the first (origr) and the last (r) request */ origr = r; while(origr->prev) { origr = origr->prev; } while(r->next) { r = r->next; } /* At this point r is the last request in the * chain. However, we now need to detect a case when * a bad ErrorDocument was used and back out of it. That's * how Apache does it internally. Except while Apache knows * what is happening we will have to rely on the missing * headers in the final request to detect this condition. */ arr = ap_table_elts(r->headers_out); while ((arr->nelts == 0)&&(r->prev != NULL)) { r = r->prev; arr = ap_table_elts(r->headers_out); } /* Find the main context */ msr = find_msr(r); /* msr may be null in cases where Apache encountered an * invalid request. Such requests will not go through all * processing stages, but will go through the logging phase. */ if (msr == NULL) { msr = sec_create_context(origr); } if (msr->dcfg == NULL) return DECLINED; /* -- Guardian -- */ sec_guardian_logger(r, origr, msr); /* -- Audit logging starts here -- */ /* Do not log anything if we were specifically asked not to */ if (msr->explicit_auditlog == 0) { sec_debug_log(r, 4, "Audit log: Not logging because asked not to"); return DECLINED; } /* We are processing the switch statement only if we * are not aware about any explicit instructions about * audit logging. The explicit instructions always * override the configuration settings. */ if (msr->explicit_auditlog == NOT_SET) { if ((origr->handler != NULL)||(r->handler != NULL)) msr->is_dynamic = 1; else msr->is_dynamic = 0; if ( (is_response_status_relevant(r, msr->dcfg, origr->status)) || (is_response_status_relevant(r, msr->dcfg, r->status)) ) { msr->is_relevant++; } switch(msr->dcfg->auditlog_flag) { case AUDITLOG_OFF : sec_debug_log(r, 2, "Audit log: Set to Off - skipping"); return DECLINED; break; case AUDITLOG_DYNAMIC_OR_RELEVANT : if ((msr->is_dynamic == 0)&&(msr->is_relevant == 0)) { sec_debug_log(r, 2, "Audit log: Set to DynamicOrRelevant - ignoring a non-dynamic and non-relevant request"); return DECLINED; } break; case AUDITLOG_RELEVANT_ONLY : if (msr->is_relevant == 0) { sec_debug_log(r, 2, "Audit log: Set to RelevantOnly - ignoring a non-relevant request"); return DECLINED; } break; case AUDITLOG_ON : /* All right, do nothing */ break; default : sec_debug_log(r, 1, "Audit log: Internal Error - unknown setting detected (%i)", msr->dcfg->auditlog_flag); return DECLINED; break; } } sec_auditlog_init(msr); /* return immediatelly if we don't have a file to write to */ if (msr->dcfg->auditlog_fd == -1) { sec_debug_log(r, 1, "Audit log enabled, but filename not specified, uri=\"%s\"", log_escape(r->pool, r->uri)); return DECLINED; } if (msr->dcfg->auditlog_type == AUDITLOG_CONCURRENT) { sec_audit_logger_concurrent(r, origr, msr->dcfg, msr); } else { sec_audit_logger_serial(r, origr, msr->dcfg, msr); } /* -- Done logging -- */ /* It doesn't matter what we return, Apache will * execute all handlers registered with the logging * hook. */ return DECLINED; } /** * Construct a log line in the vcombinedus format (see below). */ static char *construct_log_vcombinedus(request_rec *r, request_rec *origr) { char *local_user, *remote_user; char *referer, *user_agent, *uniqueid; char *sessionid = "-"; /* not used yet */ /* remote log name */ if (r->connection->remote_logname == NULL) remote_user = "-"; else remote_user = r->connection->remote_logname; /* authenticated user */ if (r->connection->user == NULL) local_user = "-"; else local_user = r->connection->user; /* unique id */ uniqueid = (char *)get_env_var(r, "UNIQUE_ID"); if (uniqueid == NULL) uniqueid = "-"; /* referer */ referer = (char *)ap_table_get(r->headers_in, "Referer"); if (referer == NULL) referer = "-"; /* user agent */ user_agent = (char *)ap_table_get(r->headers_in, "User-Agent"); if (user_agent == NULL) user_agent = "-"; return ap_psprintf(r->pool, "%s %s %s %s [%s] \"%s\" %i %li \"%s\" \"%s\" %s \"%s\"", ap_get_server_name(r), r->connection->remote_ip, log_escape(r->pool, remote_user), log_escape(r->pool, local_user), current_logtime(r), ((origr->the_request == NULL) ? "" : log_escape(r->pool, origr->the_request)), origr->status, r->bytes_sent, log_escape(r->pool, referer), log_escape(r->pool, user_agent), log_escape(r->pool, uniqueid), sessionid); } static char *construct_log_vcombinedus_limited(request_rec *r, request_rec *origr, int _limit, int *was_limited) { char *local_user, *remote_user; char *referer, *user_agent, *uniqueid; char *sessionid = "-"; /* not used yet */ char *the_request, *bytes_sent; int limit = _limit; /* remote log name */ if (r->connection->remote_logname == NULL) remote_user = "-"; else remote_user = log_escape_nq(r->pool, r->connection->remote_logname); /* authenticated user */ if (r->connection->user == NULL) local_user = "-"; else local_user = log_escape_nq(r->pool, r->connection->user); /* unique id */ uniqueid = (char *)get_env_var(r, "UNIQUE_ID"); if (uniqueid == NULL) uniqueid = "-"; else uniqueid = log_escape(r->pool, uniqueid); /* referer */ referer = (char *)ap_table_get(r->headers_in, "Referer"); if (referer == NULL) referer = "-"; else referer = log_escape(r->pool, referer); /* user agent */ user_agent = (char *)ap_table_get(r->headers_in, "User-Agent"); if (user_agent == NULL) user_agent = "-"; else user_agent = log_escape(r->pool, user_agent); the_request = (origr->the_request == NULL) ? "" : log_escape(r->pool, origr->the_request); bytes_sent = ap_psprintf(r->pool, "%li", r->bytes_sent); /* first take away the size of the * information we must log */ limit -= 22; /* spaces and double quotes */ limit -= strlen(ap_get_server_name(r)); /* server name or IP */ limit -= strlen(r->connection->remote_ip); /* remote IP */ limit -= 28; /* current_logtime */ limit -= 3; /* status */ limit -= strlen(bytes_sent); /* bytes sent */ limit -= strlen(uniqueid); /* unique id */ limit -= strlen(sessionid); /* session id */ if (limit <= 0) { sec_debug_log(r, 1, "GuardianLog: Atomic PIPE write buffer too small: %i", PIPE_BUF); return NULL; } /* we hope to be able to squeeze in everything */ if (limit < (int)(strlen(remote_user) + strlen(local_user) + strlen(referer) + strlen(user_agent) + strlen(the_request))) { /* Boo hoo, there is not enough space available. */ *was_limited = 1; /* see if we can reduce the size of something */ if (strlen(remote_user) > 32) { sec_debug_log(r, 9, "GuardianLog: Reduced remote_user to 32"); remote_user[32] = '\0'; } limit -= strlen(remote_user); if (strlen(local_user) > 32) { sec_debug_log(r, 9, "GuardianLog: Reduced local_user to 32"); local_user[32] = '\0'; } limit -= strlen(local_user); if (strlen(referer) > 64) { sec_debug_log(r, 9, "GuardianLog: Reduced referer to 64"); referer[64] = '\0'; } limit -= strlen(referer); if (strlen(user_agent) > 64) { sec_debug_log(r, 9, "GuardianLog: Reduced user_agent to 64"); user_agent[64] = '\0'; } limit -= strlen(user_agent); if (limit <= 0) { sec_debug_log(r, 1, "GuardianLog: Atomic PIPE write buffer too small: %i", PIPE_BUF); return NULL; } /* use what's left for the request line */ if ((int)strlen(the_request) > limit) { the_request[limit] = '\0'; sec_debug_log(r, 9, "GuardianLog: Reduced the_request to %i bytes", limit); } } else { /* Yay! We have enough space! */ *was_limited = 0; } return ap_psprintf(r->pool, "%s %s %s %s [%s] \"%s\" %i %s \"%s\" \"%s\" %s \"%s\"", ap_get_server_name(r), r->connection->remote_ip, remote_user, local_user, current_logtime(r), the_request, origr->status, bytes_sent, referer, user_agent, uniqueid, sessionid ); } /** * This the guardian logger, which is used to interface to the external * script for web server protection - httpd_guardian. */ static void sec_guardian_logger(request_rec *r, request_rec *origr, modsec_rec *msr) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(r->server->module_config, &security_module); char *str1, *str2, *text; char *modsec_message = "-"; int modsec_rating = 0; /* not used yet */ int duration = (time(NULL) - r->request_time); int limit, was_limited; sec_debug_log(r, 4, "sec_guardian_logger: Starting"); /* bail out if we do not have where to write */ if ((scfg->guardian_log_name == NULL)||(scfg->guardian_log_fd == -1)) return; /* process the condition, if we have one */ if (scfg->guardian_log_condition != NULL) { if (*scfg->guardian_log_condition == '!') { if (ap_table_get(r->subprocess_env, scfg->guardian_log_condition + 1) != NULL) { /* TODO log message */ return; } } else { if (ap_table_get(r->subprocess_env, scfg->guardian_log_condition) == NULL) { /* TODO log message */ return; } } } /* * Log format is as follows: * * %V %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i" %{UNIQUE_ID}e * "SESSION_ID" %T %D "MODSEC_MESSAGE" MODSEC_RATING * * The fields SESSION_ID, MODSEC_MESSAGE, and MODSEC_RATING are not used at the moment. */ str2 = ap_psprintf(r->pool, "%i %i \"%s\" %i", 0, duration, log_escape(r->pool, modsec_message), modsec_rating); if (str2 == NULL) return; /* If we are logging to a pipe we need to observe and * obey the pipe atomic write limit - PIPE_BUF. */ if (scfg->guardian_log_name[0] == '|') { /* According to "Advanced Programming in the UNIX Environment * PIPE_BUF is 4096 on Linux 2.4.22, 8192 on Mac OSX 10.3, 9216 * on Solaris 9, and 16384 on FreeBSD 5.2.1. It also cannot be * less than 512 on any POSIX system. In our case, even 512 bytes * should be more than enough to avoid seeing the "atomic pipe * write buffer too small" message. */ was_limited = 0; limit = PIPE_BUF - strlen(str2) - 5; if (limit <= 0) { sec_debug_log(r, 1, "GuardianLog: Atomic PIPE write buffer too small: %i", PIPE_BUF); return; } str1 = construct_log_vcombinedus_limited(r, origr, limit, &was_limited); if (str1 == NULL) return; } else { str1 = construct_log_vcombinedus(r, origr); if (str1 == NULL) return; } if (was_limited == 0) text = ap_psprintf(r->pool, "%s %s\n", str1, str2); else text = ap_psprintf(r->pool, "%s %s L\n", str1, str2); if (text == NULL) return; write(scfg->guardian_log_fd, text, strlen(text)); } const char *get_response_protocol(request_rec *r) { int proto_num = r->proto_num; if (r->assbackwards) { return NULL; } if (proto_num > HTTP_VERSION(1,0) && ap_table_get(r->subprocess_env, "downgrade-1.0")) { proto_num = HTTP_VERSION(1,0); } if (proto_num == HTTP_VERSION(1,0) && ap_table_get(r->subprocess_env, "force-response-1.0")) { return "HTTP/1.0"; } return SERVER_PROTOCOL; } /** * New-style (concurrent) audit logger. */ void sec_audit_logger_concurrent(request_rec *r, request_rec *origr, sec_dir_config *dcfg, modsec_rec *msr) { const array_header *arr = NULL; table_entry *te = NULL; char *str1 = NULL, *str2 = NULL, *text = NULL; unsigned int nbytes; unsigned char md5hash[MD5_DIGESTSIZE]; int i, reconstructed_request_body_flag = 0; int was_limited = 0; sec_debug_log(r, 4, "sec_audit_logger_concurrent: Starting"); /* No fd means no logging for this request */ if ((msr->new_auditlog_fd == -1)||(msr->new_auditlog_fd == 0)) return; /* AUDITLOG_PART_REQUEST_BODY */ if (strchr(msr->dcfg->auditlog_parts, AUDITLOG_PART_REQUEST_BODY) != NULL) { char *text = NULL; if ((msr->is_body_read != 0)&&(msr->ctx_in != NULL)) { text = ap_psprintf(r->pool, "\n--%s-C--\n", msr->new_auditlog_boundary); sec_auditlog_write(msr, text, strlen(text)); sec_auditlog_write(msr, msr->ctx_in->buffer, msr->ctx_in->length); } } /* AUDITLOG_PART_A_RESPONSE_HEADERS */ if (strchr(msr->dcfg->auditlog_parts, AUDITLOG_PART_A_RESPONSE_HEADERS) != NULL) { text = ap_psprintf(r->pool, "\n--%s-F--\n", msr->new_auditlog_boundary); sec_auditlog_write(msr, text, strlen(text)); /* There are no response headers (or the status line) in HTTP 0.9 */ if (!r->assbackwards) { const char *status_line = (r->status_line != NULL) ? r->status_line : status_lines[my_index_of_response(r->status)]; const char *protocol = get_response_protocol(origr); if (status_line != NULL) { text = ap_psprintf(r->pool, "%s %s\n", protocol, status_line); } else { text = ap_psprintf(r->pool, "%s %i\n", protocol, r->status); } sec_auditlog_write(msr, text, strlen(text)); /* Output headers */ /* No need to merge err_headers_out - Apache has already done that */ arr = ap_table_elts(r->headers_out); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { /*text = ap_psprintf(r->pool, "%s: %s\n", te[i].key, te[i].val);*/ text = ap_psprintf(r->pool, "%s: %s\n", log_escape_header_name(msr->r->pool, te[i].key), log_escape_nq(msr->r->pool, te[i].val)); sec_auditlog_write(msr, text, strlen(text)); } } } /* AUDITLOG_PART_TRAILER */ if (strchr(msr->dcfg->auditlog_parts, AUDITLOG_PART_TRAILER) != NULL) { unsigned int now, duration; char *t = NULL; text = ap_psprintf(r->pool, "\n--%s-H--\n", msr->new_auditlog_boundary); sec_auditlog_write(msr, text, strlen(text)); /* Messages */ for(i = 0; i < msr->messages->nelts; i++) { text = ap_psprintf(r->pool, "Message: %s\n", ((char **)msr->messages->elts)[i]); sec_auditlog_write(msr, text, strlen(text)); } /* Action */ t = (char *)ap_table_get(origr->headers_in, NOTE_ACTION); if (t != NULL) { text = ap_psprintf(r->pool, "Action: Intercepted (%s)\n", t); sec_auditlog_write(msr, text, strlen(text)); } /* Apache-Handler */ if (origr->handler != NULL) { text = ap_psprintf(r->pool, "Apache-Handler: %s\n", origr->handler); sec_auditlog_write(msr, text, strlen(text)); } /* Processing times */ now = time(NULL); duration = now - msr->r->request_time; if (duration > 0) { text = ap_psprintf(r->pool, "Stopwatch: %lu000000 %lu000000 (- - -)\n", (unsigned long)msr->r->request_time, (unsigned long)duration); } else { text = ap_psprintf(r->pool, "Stopwatch: %lu000000 0 (- - -)\n", (unsigned long)msr->r->request_time); } sec_auditlog_write(msr, text, strlen(text)); /* Tell whomever is reading the response body was reconstructed */ if (reconstructed_request_body_flag) { text = ap_psprintf(r->pool, "Request-Body-Transformed: Reconstructed-URLEncoded\n"); sec_auditlog_write(msr, text, strlen(text)); } /* Producer */ text = ap_psprintf(r->pool, "Producer: %s\n", MODULE_NAME_FULL); sec_auditlog_write(msr, text, strlen(text)); /* Server */ if (real_server_signature != NULL) { text = ap_psprintf(r->pool, "Server: %s\n", real_server_signature); sec_auditlog_write(msr, text, strlen(text)); } } /* AUDITLOG_PART_ENDMARKER */ text = ap_psprintf(r->pool, "\n--%s-Z--\n", msr->new_auditlog_boundary); sec_auditlog_write(msr, text, strlen(text)); close(msr->new_auditlog_fd); /* Write an entry to the index file */ ap_MD5Final(md5hash, &msr->new_auditlog_md5ctx); str2 = ap_psprintf(r->pool, "%s %i %i md5:%s", msr->new_auditlog_filename, 0, msr->new_auditlog_size, bytes2hex(r->pool, md5hash, 16)); if (str2 == NULL) return; /* If we are logging to a pipe we need to observe and * obey the pipe atomic write limit - PIPE_BUF. For * more details see the discussion in sec_guardian_logger, * above. */ if (dcfg->auditlog_name[0] == '|') { int limit; was_limited = 0; limit = PIPE_BUF - strlen(str2) - 5; if (limit <= 0) { sec_debug_log(r, 1, "Audit Log: Atomic PIPE write buffer too small: %i", PIPE_BUF); return; } str1 = construct_log_vcombinedus_limited(r, origr, limit, &was_limited); if (str1 == NULL) return; } else { str1 = construct_log_vcombinedus(r, origr); if (str1 == NULL) return; } if (was_limited == 0) text = ap_psprintf(r->pool, "%s %s\n", str1, str2); else text = ap_psprintf(r->pool, "%s %s L\n", str1, str2); if (text == NULL) return; nbytes = strlen(text); write(dcfg->auditlog_fd, text, nbytes); } /** * Old-style (serial) audit logger. */ static int sec_audit_logger_serial(request_rec *r, request_rec *origr, sec_dir_config *dcfg, modsec_rec *msr) { char *the_request = origr->the_request; const char *status_line = NULL; const char *protocol = NULL; const char *error_notes = NULL; unsigned int o1size = 0, o2size = 0; char *o1 = NULL, *o2 = NULL; const array_header *arr; table_entry *te; unsigned int nbytes = 0; char *vcombinedus, *t; int i = 0; sec_debug_log(r, 2, "sec_audit_logger_serial: Starting"); /* Return silently if we don't have a request line. This * means we will not be logging request timeouts. */ if (the_request == NULL) { sec_debug_log(r, 4, "sec_audit_logger_serial: skipping, the_request is null"); return DECLINED; } vcombinedus = construct_log_vcombinedus(r, origr); if (vcombinedus == NULL) return DECLINED; status_line = (r->status_line != NULL) ? r->status_line : status_lines[my_index_of_response(r->status)]; protocol = get_response_protocol(r); /* see if there is an error message stored in notes */ error_notes = (const char *)ap_table_get(r->notes, "error-notes"); /* before allocating the first buffer, determine the size * of data; start with a reasonable number for the data we * ourselves produce add the overhead, add the_request, * and input headers */ o1size = 1024; /* allow some space for the overhead */ o1size += strlen(vcombinedus); o1size += strlen(msr->new_auditlog_boundary); o1size += strlen(the_request) * 4; /* It can grow after it is escaped */ arr = ap_table_elts(r->headers_in); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { o1size += strlen(te[i].key); o1size += strlen(te[i].val); o1size += 5; } if (error_notes != NULL) o1size += strlen(error_notes) * 4; o1 = ap_palloc(r->pool, o1size + 1); if ((o1 == NULL)||(o1size + 1 == 0)) { sec_debug_log(r, 1, "sec_audit_logger: Could not allocate output buffer #1 [asked for %lu]", o1size + 1); return DECLINED; } strcpy(o1, "=="); strncat(o1, msr->new_auditlog_boundary, o1size - strlen(o1)); strncat(o1, "==============================\n", o1size - strlen(o1)); t = ap_psprintf(r->pool, "Request: %s\n", vcombinedus); strncat(o1, t, o1size - strlen(o1)); if (r->handler != NULL) { t = ap_psprintf(r->pool, "Handler: %s\n", log_escape_nq(r->pool, (char *)r->handler)); strncat(o1, t, o1size - strlen(o1)); } if (error_notes != NULL) { t = ap_psprintf(r->pool, "Error: %s\n", log_escape_nq(r->pool, (char *)error_notes)); strncat(o1, t, o1size - strlen(o1)); } strncat(o1, "----------------------------------------\n", o1size - strlen(o1)); /* the request line */ t = ap_psprintf(r->pool, "%s\n", the_request); strncat(o1, t, o1size - strlen(o1)); /* input headers */ arr = ap_table_elts(r->headers_in); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { t = ap_psprintf(r->pool, "%s: %s\n", te[i].key, te[i].val); strncat(o1, t, o1size - strlen(o1)); } strncat(o1, "\n", o1size - strlen(o1)); /* determine the size of the second buffer */ o2size = 1024; o2size += strlen(msr->new_auditlog_boundary); if (status_line != NULL) o2size += strlen(status_line); else o2size += 10; arr = ap_table_elts(r->headers_out); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { o2size += strlen(te[i].key); o2size += strlen(te[i].val); o2size += 5; } o2 = ap_palloc(r->pool, o2size + 1); if ((o2 == NULL)||(o2size + 1 == 0)) { sec_debug_log(r, 1, "sec_audit_logger: Could not allocate output buffer #2 [asked for %lu]", o2size + 1); return DECLINED; } *o2 = '\0'; /* We don't log the headers when HTTP 0.9 is used */ if (!r->assbackwards) { if (status_line != NULL) { t = ap_psprintf(r->pool, "%s %s\n", protocol, status_line); } else { t = ap_psprintf(r->pool, "%s %i\n", protocol, r->status); } strncat(o2, t, o2size - strlen(o2)); /* output headers */ arr = ap_table_elts(r->headers_out); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { t = ap_psprintf(r->pool, "%s: %s\n", te[i].key, te[i].val); strncat(o2, t, o2size - strlen(o2)); } /* we do not need to be concerned with err_headers_out * at this point because they were already merged with * headers_out by now */ } /* The footer */ strncat(o2, "--", o2size - strlen(o2)); strncat(o2, msr->new_auditlog_boundary, o2size - strlen(o2)); strncat(o2, "--\n\n", o2size - strlen(o2)); /* Write to the file */ fd_lock(r, dcfg->auditlog_fd); nbytes = strlen(o1); write(dcfg->auditlog_fd, o1, nbytes); { int body_action = 0; /* not available by default */ char *message = NULL, *filename = NULL; sec_debug_log(r, 9, "sec_audit_logger_serial: is_relevant=%i, should_body_exist=%i, is_body_read=%i", msr->is_relevant, msr->should_body_exist, msr->is_body_read); /* determine what we need to do here */ if (msr->should_body_exist == 1) { if ((msr->is_body_read == 0)||(msr->ctx_in == NULL)) { body_action = 0; /* no payload (i.e. we did not read it) */ } else { sec_debug_log(r, 9, "sec_audit_logger_serial: is_put=%i", msr->ctx_in->is_put); if ( (msr->ctx_in->is_put) || ((msr->ctx_in->is_multipart)&&(multipart_contains_files(msr->mpd))) ) { msr->ctx_in->tmp_file_mode = REQBODY_FILE_LEAVE; body_action = 2; /* reference external files in the audit log */ /* Use only the base filename. If files * get moved around, absolute references * won't work. */ filename = strrchr(msr->ctx_in->tmp_file_name, '/'); if (filename == NULL) filename = msr->ctx_in->tmp_file_name; else filename = filename + 1; } else { body_action = 1; /* write from memory directly into a file */ } } } else { body_action = 3; /* do nothing - no request body */ } /* now generate the message */ message = NULL; switch(body_action) { case 0 : message = "[POST payload not available]"; nbytes = strlen(message); break; case 1 : message = msr->ctx_in->buffer; nbytes = msr->ctx_in->length; break; case 2 : message = ap_psprintf(r->pool, "[@file:%s]", filename); nbytes = strlen(message); break; case 3 : default : /* do nothing */ break; } /* write the message */ if (message != NULL) { char *o3 = ap_psprintf(r->pool, "%lu\n", (unsigned long)nbytes); write(dcfg->auditlog_fd, o3, strlen(o3)); write(dcfg->auditlog_fd, message, nbytes); message = "\n\n"; write(dcfg->auditlog_fd, message, 2); } } /* write the second part of the log */ nbytes = strlen(o2); write(dcfg->auditlog_fd, o2, nbytes); fd_unlock(r, dcfg->auditlog_fd); return OK; } /** * Write the supplied data to the audit log (if the FD is ready), update * the size counters, update the hash context. */ int sec_auditlog_write(modsec_rec *msr, char *data, unsigned int len) { unsigned int bytes_written, nbytes = len; int rc = 0; if ((msr->new_auditlog_fd == -1)||(msr->new_auditlog_fd == 0)||(data == NULL)) return -1; bytes_written = write(msr->new_auditlog_fd, data, nbytes); if (bytes_written < 0) rc = errno; /* Note the following will only take into account the actual * amount of bytes we've written. */ msr->new_auditlog_size += bytes_written; ap_MD5Update(&msr->new_auditlog_md5ctx, (unsigned char *)data, bytes_written); return rc; } /** * Initialise new-style audit logging. */ void sec_auditlog_init(modsec_rec *msr) { request_rec *r = msr->r; char *uniqueid, *entry_filename, *entry_basename, *text; const array_header *arr = NULL; table_entry *te = NULL; int i; sec_debug_log(r, 4, "sec_auditlog_init: Starting"); /* the boundary is used by both audit log types */ msr->new_auditlog_boundary = create_auditlog_boundary(msr->r); /* Return silently if we don't have a request line. This * means we will not be logging request timeouts. */ if (msr->r->the_request == NULL) { sec_debug_log(r, 4, "Audit log initialisation: skipping, the_request is null"); return; } if (msr->dcfg->auditlog_fd == -1) { sec_debug_log(r, 4, "Audit log initialisation: skipping, auditlog_fd is null"); return; } if (msr->dcfg->auditlog_type == AUDITLOG_SERIAL) return; ap_MD5Init(&msr->new_auditlog_md5ctx); uniqueid = (char *)get_env_var(msr->r, "UNIQUE_ID"); if (uniqueid == NULL) { sec_debug_log(r, 1, "Audit log: Concurrent audit logging requested, but UNIQUE_ID not found. Please activate mod_unique_id first."); return; } msr->new_auditlog_filename = construct_auditlog_filename(r, uniqueid); if (msr->new_auditlog_filename == NULL) return; if (msr->dcfg->auditlog_storage_dir == NULL) entry_filename = get_file_basename(r->pool, msr->dcfg->auditlog_name); else entry_filename = msr->dcfg->auditlog_storage_dir; if (entry_filename == NULL) return; entry_filename = ap_psprintf(msr->r->pool, "%s%s", entry_filename, msr->new_auditlog_filename); if (entry_filename == NULL) return; entry_basename = get_file_basename(r->pool, entry_filename); if (entry_basename == NULL) return; /* TODO OPTIMISE Surely it would be more efficient to check the folders for * the audit log repository base path in the configuration phase, to reduce * the work we do on every request. Also, since our path depends on time, * we could cache the time we last checked and don't check if we know * the folder is there. */ if (sec_dir_make_recursive(entry_basename, CREATEMODE_DIR, r) < 0) { sec_debug_log(msr->r, 1, "Audit log: Failed to create subdirectories: %s (%s)", entry_basename, strerror(errno)); } msr->new_auditlog_fd = ap_popenf(r->pool, entry_filename, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE); if (msr->new_auditlog_fd < 0) { sec_debug_log(msr->r, 1, "Audit log: Failed to create file: %s (%s)", entry_filename, strerror(errno)); return; } /* AUDITLOG_PART_HEADER */ text = ap_psprintf(r->pool, "--%s-A--\n", msr->new_auditlog_boundary); sec_auditlog_write(msr, text, strlen(text)); /* Format: time transaction_id remote_addr remote_port local_addr local_port */ text = ap_psprintf(r->pool, "[%s] %s %s %i %s %i", current_logtime(r), uniqueid, r->connection->remote_ip, r->connection->remote_addr.sin_port, r->connection->local_ip, r->connection->local_addr.sin_port); sec_auditlog_write(msr, text, strlen(text)); /* AUDITLOG_PART_REQUEST_HEADERS */ if (strchr(msr->dcfg->auditlog_parts, AUDITLOG_PART_REQUEST_HEADERS) != NULL) { text = ap_psprintf(r->pool, "\n--%s-B--\n", msr->new_auditlog_boundary); sec_auditlog_write(msr, text, strlen(text)); if (r->the_request != NULL) { sec_auditlog_write(msr, r->the_request, strlen(r->the_request)); sec_auditlog_write(msr, "\n", 1); } /* TODO Request headers can change after the body has been read * when chunked encoding is used on input. */ arr = ap_table_elts(r->headers_in); te = (table_entry *)arr->elts; for (i = 0; i < arr->nelts; i++) { if (strncasecmp(te[i].key, "mod_security-", 13) != 0) { text = ap_psprintf(r->pool, "%s: %s\n", te[i].key, te[i].val); sec_auditlog_write(msr, text, strlen(text)); } } } } /* Create a per-request modsecurity context. We * need this context even if we are not going * to do anything. */ modsec_rec *sec_create_context(request_rec *r) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(r->server->module_config, &security_module); sec_dir_config *dcfg = (sec_dir_config *)ap_get_module_config(r->per_dir_config, &security_module); modsec_rec *msr = NULL; char *content_length = NULL; msr = (modsec_rec *)ap_pcalloc(r->pool, sizeof(*msr)); msr->r = r; msr->scfg = scfg; /* We will make a copy of the per-dir configuration. * WARNING this is a shallow copy, meaning most pointers * will remain pointing to the common locations. This * is OK for now as we are not changing any of the * data we aren't copying. */ msr->dcfg = ap_pcalloc(r->pool, sizeof(sec_dir_config)); memcpy(msr->dcfg, dcfg, sizeof(sec_dir_config)); /* Make a deeper copy of the actionset. Still * not a 100% deep copy but deep enough for what * we change on the per-request basis. */ if ((dcfg->actionset != NULL)&&(dcfg->actionset != NOT_SET_P)) { msr->dcfg->actionset = ap_pcalloc(r->pool, sizeof(actionset_t)); memcpy(msr->dcfg->actionset, dcfg->actionset, sizeof(actionset_t)); } sec_set_dir_defaults(msr->dcfg); msr->request_uri = NULL; msr->_post_payload = NULL; msr->parsed_args = ap_make_table(r->pool, 10); msr->parsed_cookies = ap_make_table(r->pool, 10); msr->is_relevant = 0; msr->is_dynamic = NOT_SET; msr->explicit_auditlog = NOT_SET; msr->messages = ap_make_array(r->pool, 10, sizeof(char *)); msr->cache_request_uri = NULL; msr->cache_path_info = NULL; msr->cache_the_request = NULL; msr->cache_query_string = NULL; msr->cache_request_basename = NULL; msr->cache_script_basename = NULL; msr->cache_headers_in = ap_make_table(r->pool, 10); content_length = (char *)ap_table_get(r->headers_in, "Content-Length"); if (content_length == NULL) { /* there's no C-L, could be chunked? */ char *transfer_encoding = (char *)ap_table_get(r->headers_in, "Transfer-Encoding"); if ((transfer_encoding != NULL)&&(strstr(transfer_encoding, "chunked") != NULL)) { msr->should_body_exist = 1; } else { /* no C-L, no chunked */ msr->should_body_exist = 0; } } else { /* C-L found */ msr->should_body_exist = 1; } /* Store context for others to find */ store_msr(r, msr); return msr; } #ifdef ENABLE_EARLY_HOOK static int sec_check_access_early(request_rec *r) { sec_debug_log(r, 2, "Early processing activated"); return sec_check_access(r); } #endif int sec_check_access(request_rec *r) { char *env_modsec_enable = NULL; int real_action, real_status; modsec_rec *msr; int rc, filter_engine; sec_debug_log(r, 2, "Detection phase starting (request %x): \"%s\"", r, ((r->the_request == NULL) ? "" : log_escape(r->pool, r->the_request))); /* We only continue if this is a main request */ if (!ap_is_initial_req(r)) { sec_debug_log(r, 2, "sec_check_access: Filtering off, not an initial request"); return DECLINED; } #ifdef ENABLE_EARLY_HOOK msr = find_msr(r); if (msr != NULL) { sec_debug_log(r, 4, "sec_check_access: Ignoring request that was already processed"); return DECLINED; } #endif msr = sec_create_context(r); if (msr->dcfg == NULL) { sec_debug_log(r, 2, "sec_check_access: Filtering off, dcfg is NULL"); return DECLINED; } filter_engine = msr->dcfg->filter_engine; env_modsec_enable = (char *)get_env_var(r, "MODSEC_ENABLE"); if (env_modsec_enable != NULL) { sec_debug_log(r, 4, "sec_check_access: Detected MODSEC_ENABLE: %s", env_modsec_enable); if (strcasecmp(env_modsec_enable, "Off") == 0) filter_engine = FILTERING_OFF; else if (strcasecmp(env_modsec_enable, "On") == 0) filter_engine = FILTERING_ON; else if (strcasecmp(env_modsec_enable, "DynamicOnly") == 0) filter_engine = FILTERING_DYNAMIC_ONLY; else { sec_debug_log(r, 1, "Ignoring invalid MODSEC_ENABLE: %s", env_modsec_enable); } } /* refuse to work if filtering is off */ if (filter_engine == FILTERING_OFF) { sec_debug_log(r, 2, "sec_check_access: Filtering off, not enabled here"); return DECLINED; } if (r->handler != NULL) msr->is_dynamic = 1; else { if (filter_engine == FILTERING_DYNAMIC_ONLY) { if (S_ISDIR(r->finfo.st_mode)) { msr->is_dynamic = 1; sec_debug_log(r, 2, "sec_check_access: Unable to determine if directory request is dynamic - assuming it is."); } else { msr->is_dynamic = 0; sec_debug_log(r, 2, "sec_check_access: Filtering off, disabled for non-dynamic requests (and this is one)"); return DECLINED; } } } /* this variable should be used in the subsequent phases to * determine whether to run or not without having to duplicate * the complex logic */ msr->is_enabled = 1; /* Initialise mod_security structures for this request. * As of 1.8.6 non-fatal default actions are not allowed * during the initialization phase. */ real_action = msr->dcfg->actionset->action; real_status = msr->dcfg->actionset->status; if (msr->dcfg->actionset->action == ACTION_NONE) { msr->dcfg->actionset->action = ACTION_DENY; } if (msr->dcfg->actionset->status == 0) { msr->dcfg->actionset->status = HTTP_FORBIDDEN; } rc = sec_initialise(msr); msr->dcfg->actionset->action = real_action; msr->dcfg->actionset->status = real_status; /* Process rules only if there were no errors * in the initialization stage. */ if (rc == DECLINED) { rc = sec_check_all_signatures(msr); } /* make a note for the logger */ if (rc != DECLINED) { char *note = ap_psprintf(r->pool, "%i", rc); ap_table_setn(r->headers_in, NOTE_ACTION, note); ap_table_setn(r->subprocess_env, NOTE_ACTED, "1"); } else { ap_table_unset(r->headers_in, NOTE_ACTION); } /* Export the request body as a note */ if (msr->is_body_read) { char *post_payload = msr->_post_payload; if (msr->mpd != NULL) { if (msr->_fake_post_payload != NULL) post_payload = msr->_fake_post_payload; else post_payload = construct_fake_urlencoded(msr, msr->parsed_args); } if (post_payload != NULL) ap_table_setn(r->notes, NOTE_BODY, post_payload); } return rc; } char *log_escape(pool *p, char *text) { return _log_escape(p, text, 1, 0); } char *log_escape_nq(pool *p, char *text) { return _log_escape(p, text, 0, 0); } char *log_escape_header_name(pool *p, char *text) { return _log_escape(p, text, 0, 1); } char *_log_escape(pool *p, char *text, int escape_quotes, int escape_colon) { const unsigned char *s = NULL; unsigned char *d = NULL; char *ret = NULL; if (text == NULL) return NULL; ret = ap_palloc(p, strlen(text) * 4 + 1); if (ret == NULL) return NULL; s = (const unsigned char *)text; d = (unsigned char *)ret; while(*s != 0) { switch(*s) { case ':' : if (escape_colon) { *d++ = '\\'; *d++ = ':'; } else { *d++ = *s; } break; case '"' : if (escape_quotes) { *d++ = '\\'; *d++ = '"'; } else { *d++ = *s; } break; case '\b' : *d++ = '\\'; *d++ = 'b'; break; case '\n' : *d++ = '\\'; *d++ = 'n'; break; case '\r' : *d++ = '\\'; *d++ = 'r'; break; case '\t' : *d++ = '\\'; *d++ = 't'; break; case '\v' : *d++ = '\\'; *d++ = 'v'; break; case '\\' : *d++ = '\\'; *d++ = '\\'; break; default : if ((*s <= 0x1f)||(*s >= 0x7f)) { *d++ = '\\'; *d++ = 'x'; c2x(*s, d); d += 2; } else { *d++ = *s; } break; } s++; } *d = 0; return ret; } void sec_debug_log(request_rec *r, int level, const char *text, ...) { sec_dir_config *dcfg = (sec_dir_config *)ap_get_module_config(r->per_dir_config, &security_module); va_list ap; char str1[1024] = ""; char str2[1256] = ""; int debuglog_fd = -1; int filter_debug_level = 0; if (dcfg != NULL) { if ((dcfg->debuglog_fd != -1)&&(dcfg->debuglog_fd != 0)) debuglog_fd = dcfg->debuglog_fd; if (dcfg->filter_debug_level != NOT_SET) filter_debug_level = dcfg->filter_debug_level; } /* Return immediately if we don't have where to write * or if the log level of the message is higher than * wanted in the log. */ if ((level != 1)&&( (debuglog_fd == -1) || (level > filter_debug_level) )) return; va_start(ap, text); ap_vsnprintf(str1, sizeof(str1), text, ap); ap_snprintf(str2, sizeof(str2), "[%s] [%s/sid#%lx][rid#%lx][%s][%i] %s\n", current_logtime(r), ap_get_server_name(r), (unsigned long)(r->server), (unsigned long)r, ((r->uri == NULL) ? "" : log_escape_nq(r->pool, r->uri)), level, str1); if ((debuglog_fd != -1)&&(level <= filter_debug_level)) { write(debuglog_fd, str2, strlen(str2)); } if (level == 1) { char *unique_id = (char *)get_env_var(r, "UNIQUE_ID"); char *hostname = (char *)r->hostname; if (unique_id != NULL) unique_id = ap_psprintf(r->pool, " [unique_id \"%s\"]", log_escape(r->pool, unique_id)); else unique_id = ""; if (hostname != NULL) hostname = ap_psprintf(r->pool, " [hostname \"%s\"]", log_escape(r->pool, hostname)); else hostname = ""; ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r->server, "[client %s] mod_security: %s%s [uri \"%s\"]%s", r->connection->remote_ip, str1, hostname, log_escape(r->pool, r->unparsed_uri), unique_id); } va_end(ap); return; } char *current_logtime(request_rec *r) { int timz; struct tm *t; char tstr[100]; char sign; t = ap_get_gmtoff(&timz); sign = (timz < 0 ? '-' : '+'); if (timz < 0) { timz = -timz; } strftime(tstr, 80, "%d/%b/%Y:%H:%M:%S ", t); ap_snprintf(tstr + strlen(tstr), 80 - strlen(tstr), "%c%.2d%.2d", sign, timz / 60, timz % 60); return ap_pstrdup(r->pool, tstr); } char *current_filetime(request_rec *r) { int timz; struct tm *t; char tstr[100]; t = ap_get_gmtoff(&timz); strftime(tstr, 80, "%Y%m%d-%H%M%S", t); return ap_pstrdup(r->pool, tstr); } /** * Constructs a filename that will be used to store an * audit log entry. */ char *construct_auditlog_filename(request_rec *r, char *uniqueid) { int timz; struct tm *t; char tstr[300]; t = ap_get_gmtoff(&timz); strftime(tstr, 299, "/%Y%m%d/%Y%m%d-%H%M/%Y%m%d-%H%M%S", t); return ap_psprintf(r->pool, "%s-%s", tstr, uniqueid); } void sec_init(server_rec *s, pool *p) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(s->module_config, &security_module); extern int ap_standalone; int first_time = 0; /* Is this the first time where are here? */ if (ap_standalone && (ap_restart_time == 0)) first_time = 1; if ((scfg->server_response_token)&&(!first_time)) { ap_log_error(APLOG_MARK, APLOG_WARNING | APLOG_NOERRNO, s, "mod_security: SecServerResponseToken directive is deprecated"); } /* Although this may seem not to make any sense we are * in fact allocating sufficient space in the signature * so we can change it later by using brute force. */ real_server_signature = ap_pstrdup(p, ap_get_server_version()); if (scfg->server_signature != NULL) { ap_add_version_component(scfg->server_signature); change_server_signature(s, scfg); } #if !(defined(WIN32)||defined(NETWARE)) if (scfg->chroot_dir != NULL) { if (first_time == 0) { ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, s, "mod_security: chroot checkpoint #2 (pid=%i ppid=%i)", getpid(), getppid()); if (chdir(scfg->chroot_dir) < 0) { ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, s, "mod_security: chroot failed, unable to chdir to %s, errno=%d(%s)", scfg->chroot_dir, errno, strerror(errno)); exit(1); } if (chroot(scfg->chroot_dir) < 0) { ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, s, "mod_security: chroot failed, path=%s, errno=%d(%s)", scfg->chroot_dir, errno, strerror(errno)); exit(1); } if (chdir("/") < 0) { ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, s, "mod_security: chroot failed, unable to chdir to /, errno=%d(%s)", errno, strerror(errno)); exit(1); } ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, s, "mod_security: chroot successful, path=%s", scfg->chroot_dir); scfg->chroot_completed = 1; } else { ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, s, "mod_security: chroot checkpoint #1 (pid=%i ppid=%i)", getpid(), getppid()); } } #endif if (first_time) { if (scfg->server_signature != NULL) { ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, s, "mod_security/%s configured - %s", MODULE_RELEASE, real_server_signature); } else { ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, s, "mod_security/%s configured", MODULE_RELEASE); } } } void sec_child_init(server_rec *s, pool *p) { sec_srv_config *scfg = (sec_srv_config *)ap_get_module_config(s->module_config, &security_module); #if !(defined(WIN32)||defined(NETWARE)) /* Refuse to work if chroot was requested but * not performed. Unfortunately, we can't perform * this check earlier, or at a better location. */ if ((scfg->chroot_dir != NULL)&&(scfg->chroot_completed == 0)) { ap_log_error(APLOG_MARK, APLOG_EMERG | APLOG_NOERRNO, s, "mod_security: Internal chroot facility mailfunctioned! Exiting."); /* This is ugly but better than running a server * without a chroot when a chroot was configured. * We sleep a little (one sec) to prevent children * from dying too fast. */ sec_sleep(1000); exit(1); } #endif change_server_signature(s, scfg); /* initialise each child process with a different seed */ srand(time(NULL) * getpid()); } #ifdef WIN32 #define USE_LOCKING #endif #ifndef USE_FCNTL #ifndef USE_FLOCK #ifndef USE_LOCKING #ifndef NETWARE #define USE_FCNTL #endif #endif #endif #endif #ifdef USE_FCNTL static struct flock lock_it; static struct flock unlock_it; #endif int fd_lock(request_rec *r, int fd) { int rc = 0; #ifdef USE_FCNTL lock_it.l_whence = SEEK_SET; lock_it.l_start = 0; lock_it.l_len = 0; lock_it.l_type = F_WRLCK; lock_it.l_pid = 0; while ( ((rc = fcntl(fd, F_SETLKW, &lock_it)) < 0) && (errno == EINTR) ) { continue; } #endif #ifdef USE_FLOCK while ( ((rc = flock(fd, LOCK_EX)) < 0) && (errno == EINTR) ) { continue; } #endif #ifdef USE_LOCKING lseek(fd, 0, SEEK_SET); rc = _locking(fd, _LK_LOCK, 1); lseek(fd, 0, SEEK_END); #endif #ifdef NETWARE if ((locking_sem != 0) && (TimedWaitOnLocalSemaphore(locking_sem, 10000) != 0)) rc = -1; else rc = 1; #endif if (rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, "mod_security: Locking failed %i [%s]", rc, strerror(errno)); return -1; } return 1; } int fd_unlock(request_rec *r, int fd) { int rc = 0; #ifdef USE_FCNTL unlock_it.l_whence = SEEK_SET; unlock_it.l_start = 0; unlock_it.l_len = 0; unlock_it.l_type = F_UNLCK; unlock_it.l_pid = 0; while( ((rc = fcntl(fd, F_SETLKW, &unlock_it)) < 0) && (errno == EINTR) ) { continue; } #endif #ifdef USE_FLOCK while ( ((rc = flock(fd, LOCK_UN)) < 0) && (errno == EINTR) ) { continue; } #endif #ifdef USE_LOCKING lseek(fd, 0, SEEK_SET); rc = _locking(fd, _LK_UNLCK, 1); lseek(fd, 0, SEEK_END); #endif #ifdef NETWARE if (locking_sem) SignalLocalSemaphore(locking_sem); rc = 1; #endif if (rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, "mod_security: Unlocking failed %i", rc); return -1; } return 1; } char *get_temp_folder(pool *p) { char *filename = NULL; #ifdef WIN32 filename = ap_pcalloc(p, 256); if (filename == NULL) return ""; if (GetTempPath(255, filename) != 0) return filename; #endif filename = getenv("TMPDIR"); if (filename != NULL) return filename; filename = getenv("TEMP"); if (filename != NULL) return filename; filename = getenv("TMP"); if (filename != NULL) return filename; #if defined NETWARE return("sys:/tmp/"); #elif defined WIN32 return(""); #else return("/tmp/"); #endif } #ifdef WIN32 char *get_file_basename(pool *p, char *filename) { char *b, *c, *d; if (filename == NULL) return NULL; b = ap_pstrdup(p, filename); if (b == NULL) return NULL; c = strrchr(b, '/'); if (c != NULL) { d = strrchr(c, '\\'); if (d != NULL) *d = '\0'; else *c = '\0'; } else { d = strrchr(b, '\\'); if (d != NULL) *d = '\0'; } return b; } #else char *get_file_basename(pool *p, char *filename) { char *b, *c; if (filename == NULL) return NULL; b = ap_pstrdup(p, filename); if (b == NULL) return NULL; c = strrchr(b, '/'); if (c != NULL) *c = '\0'; return b; } #endif char *construct_put_filename(modsec_rec *msr) { char c, *put_file_name = NULL; char *tmp_dir = NULL, *t = NULL; put_file_name = ap_pstrdup(msr->r->pool, msr->r->uri); t = strstr(put_file_name, "?"); if (t != NULL) *t = 0; t = strrchr(put_file_name, '/'); if (t != NULL) put_file_name = t + 1; /* allow letters, digits and dots, nuke the rest */ t = put_file_name; while((c = *t) != 0) { if (!( isalnum(c)||(c == '.') )) *t = '_'; t++; } if (msr->dcfg->upload_dir != NULL) tmp_dir = msr->dcfg->upload_dir; else tmp_dir = get_temp_folder(msr->r->pool); return ap_psprintf(msr->r->pool, "%s/%s-%s-%s", tmp_dir, current_filetime(msr->r), msr->r->connection->remote_ip, put_file_name); } static int request_body_file_cleanup(modsec_rec *msr) { char *put_filename = NULL; if (msr == NULL) return -1; sec_debug_log(msr->r, 4, "request_body_file_cleanup: Started"); /* only continue if there is a temporary file */ if ((msr->ctx_in == NULL)||(msr->ctx_in->tmp_file_name == NULL)) return -1; if (msr->ctx_in->is_put) { put_filename = construct_put_filename(msr); } /* The new-style audit log makes no use of the temporary * request body; in such cases we don't have to copy it, just rename it. * But this function does not need to think about it because mode will * be REQBODY_FILE_DELETE. */ if (msr->ctx_in->tmp_file_mode == REQBODY_FILE_LEAVE) { /* if it's a PUT request and the files need to be kept * then we need to copy this file into another one */ if ((msr->ctx_in->is_put)&&(msr->dcfg->upload_keep_files)) { sec_debug_log(msr->r, 4, "request_body_file_cleanup: Copying request body file %s to %s", msr->ctx_in->tmp_file_name, put_filename); if (sec_copy_file(msr->ctx_in->tmp_file_name, put_filename) < 0) { sec_debug_log(msr->r, 1, "request_body_file_cleanup: Failed to copy %s to %s", msr->ctx_in->tmp_file_name, put_filename); } } return 1; } else { /* if it's a PUT request and the files need to be kept * then we should rename this file and return */ if ((msr->ctx_in->is_put)&&(msr->dcfg->upload_keep_files)) { sec_debug_log(msr->r, 4, "request_body_file_cleanup: Renaming request body file %s to %s", msr->ctx_in->tmp_file_name, put_filename); if (rename(msr->ctx_in->tmp_file_name, put_filename) != 0) { sec_debug_log(msr->r, 1, "request_body_file_cleanup: Failed to rename %s to %s", msr->ctx_in->tmp_file_name, put_filename); } return 1; } } /* Poor file, nobody wants you */ if (unlink(msr->ctx_in->tmp_file_name) < 0) { sec_debug_log(msr->r, 1, "request_body_file_cleanup: Failed to delete file \"%s\" because %d(\"%s\")", log_escape(msr->r->pool, msr->ctx_in->tmp_file_name), errno, log_escape(msr->r->pool, strerror(errno))); } else { sec_debug_log(msr->r, 2, "request_body_file_cleanup: Deleted file \"%s\"", log_escape(msr->r->pool, msr->ctx_in->tmp_file_name)); } return 1; } int multipart_cleanup(multipart_data *mpd) { sec_debug_log(mpd->r, 9, "multipart_cleanup: Started"); /* loop through the list of parts * and delete the temporary files, but only if * file storage was not requested, or if storage * of relevant files was requested and this isn't * such a request */ if ( (mpd->dcfg->upload_keep_files == KEEP_FILES_OFF) || ((mpd->dcfg->upload_keep_files == KEEP_FILES_RELEVANT_ONLY) && (mpd->msr->is_relevant <= 0)) ) { multipart_part **parts; int i; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if (parts[i]->type == MULTIPART_FILE) { if (parts[i]->tmp_file_name != NULL) { sec_debug_log(mpd->r, 9, "multipart_cleanup: deleting temporary file (part) \"%s\"", log_escape(mpd->r->pool, parts[i]->tmp_file_name)); if (unlink(parts[i]->tmp_file_name) < 0) { sec_debug_log(mpd->r, 1, "Multipart: Failed to delete file (part) \"%s\" because %d(\"%s\")", log_escape(mpd->r->pool, parts[i]->tmp_file_name), errno, log_escape(mpd->r->pool, strerror(errno))); } else { sec_debug_log(mpd->r, 4, "Multipart: Deleted file (part) \"%s\"", log_escape(mpd->r->pool, parts[i]->tmp_file_name)); } } } } } else { /* delete empty files */ multipart_part **parts; int i; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if ((parts[i]->type == MULTIPART_FILE)&&(parts[i]->tmp_file_size == 0)) { if (parts[i]->tmp_file_name != NULL) { sec_debug_log(mpd->r, 4, "Multipart: deleting temporary file (part) \"%s\"", log_escape(mpd->r->pool, parts[i]->tmp_file_name)); if (unlink(parts[i]->tmp_file_name) < 0) { sec_debug_log(mpd->r, 1, "Multipart: Failed to delete empty file (part) \"%s\" because %d(%s)", log_escape(mpd->r->pool, parts[i]->tmp_file_name), errno, strerror(errno)); } else { sec_debug_log(mpd->r, 4, "Multipart: Deleted empty file (part) \"%s\"", log_escape(mpd->r->pool, parts[i]->tmp_file_name)); } } } } } return 1; } int multipart_init(multipart_data *mpd, modsec_rec *msr, char **error_msg) { request_rec *r = msr->r; if (error_msg == NULL) return -1; *error_msg = NULL; mpd->dcfg = msr->dcfg; mpd->p = r->pool; mpd->msr = msr; mpd->r = msr->r; mpd->parts = ap_make_array(mpd->p, 10, sizeof(multipart_part *)); mpd->bufleft = MULTIPART_BUF_SIZE; mpd->bufptr = mpd->buf; mpd->buf_contains_line = 1; mpd->mpp = NULL; /* schedule resource cleanup for later */ ap_register_cleanup(r->pool, (void *)mpd, (void (*)(void *))multipart_cleanup, ap_null_cleanup); return 1; } int multipart_process_chunk(multipart_data *mpd, char *buf, int size, char **error_msg) { char *inptr = buf; unsigned int inleft = size; if (error_msg == NULL) return -1; *error_msg = NULL; if (size == 0) return 1; if (mpd->seen_data == 0) mpd->seen_data = 1; if (mpd->is_complete) { sec_debug_log(mpd->r, 4, "Multipart: ignoring data after last boundary (received %i bytes)", size); return 1; } if (mpd->bufleft == 0) { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: internal error in process_chunk: no more space in the buffer"); return -1; } /* here we loop through all the data available, byte by byte */ while(inleft > 0) { char c = *inptr; int process_buffer = 0; if ((c == 0x0d)&&(mpd->bufleft == 1)) { /* we don't want to take 0x0d as the last byte in the buffer */ process_buffer = 1; } else { inptr++; inleft = inleft - 1; *(mpd->bufptr) = c; mpd->bufptr++; mpd->bufleft--; } /* until we either reach the end of the line * or the end of our internal buffer */ if ((c == 0x0a)||(mpd->bufleft == 0)||(process_buffer)) { *(mpd->bufptr) = 0; /* boundary preconditions: length of the line greater than * the length of the boundary + the first two characters * are dashes "-" */ if ( mpd->buf_contains_line && (strlen(mpd->buf) > strlen(mpd->boundary) + 2) && (((*(mpd->buf) == '-'))&&(*(mpd->buf + 1) == '-')) && (strncmp(mpd->buf + 2, mpd->boundary, strlen(mpd->boundary)) == 0) ) { char *boundary_end = mpd->buf + 2 + strlen(mpd->boundary); if ( (*boundary_end == '\r') &&(*(boundary_end + 1) == '\n') &&(*(boundary_end + 2) == '\0') ) { /* simple boundary */ if (multipart_process_boundary(mpd, 0, error_msg) < 0) return -1; } else if ( (*boundary_end == '-') &&(*(boundary_end + 1) == '-') &&(*(boundary_end + 2) == '\r') &&(*(boundary_end + 3) == '\n') &&(*(boundary_end + 4) == '\0') ) { /* final boundary */ mpd->is_complete = 1; if (multipart_process_boundary(mpd, 1, error_msg) < 0) return -1; } else { /* error */ *error_msg = ap_psprintf(mpd->r->pool, "Multipart: invalid boundary encountered: %s", log_escape_nq(mpd->r->pool, mpd->buf)); return -1; } } else { if (mpd->mpp == NULL) { sec_debug_log(mpd->r, 4, "Multipart: ignoring data before first boundary"); } else { if (mpd->mpp_state == 0) { if ((mpd->bufleft == 0)||(process_buffer)) { /* part header lines must be shorter than * MULTIPART_BUF_SIZE bytes */ *error_msg = ap_psprintf(mpd->r->pool, "Multipart: part header line over %i bytes long", MULTIPART_BUF_SIZE); return -1; } if (multipart_process_part_header(mpd, error_msg) < 0) return -1; } else { if (multipart_process_part_data(mpd, error_msg) < 0) return -1; } } } /* reset the pointer to the beginning of the buffer * and continue to accept input data */ mpd->bufptr = mpd->buf; mpd->bufleft = MULTIPART_BUF_SIZE; mpd->buf_contains_line = (c == 0x0a) ? 1 : 0; } if ((mpd->is_complete)&&(inleft != 0)) { sec_debug_log(mpd->r, 4, "Multipart: ignoring data after last boundary (%i bytes left)", inleft); return 1; } } return 1; } char *multipart_construct_filename(multipart_data *mpd) { char c, *p, *q = mpd->mpp->filename; char *filename; /* find the last backward slash and consider the * filename to be only what's right from it */ p = strrchr(q, '\\'); if (p != NULL) q = p + 1; /* do the same for the forward slash */ p = strrchr(q, '/'); if (p != NULL) q = p + 1; /* allow letters, digits and dots, replace * everything else with underscores */ p = filename = ap_pstrdup(mpd->p, q); while((c = *p) != 0) { if (!( isalnum(c)||(c == '.') )) *p = '_'; p++; } return filename; } int multipart_parse_content_disposition(multipart_data *mpd, char *value) { char *p = NULL, *t = NULL; /* accept only what we understand */ if (strncmp(value, "form-data", 9) != 0) { return -1; } /* see if there are any other parts to parse */ p = value + 9; while((*p == '\t')||(*p == ' ')) p++; if (*p == '\0') return 1; /* this is OK */ if (*p != ';') return -2; p++; /* parse the appended parts */ while(*p != '\0') { char *name = NULL, *value = NULL, *start = NULL; /* go over the whitespace */ while((*p == '\t')||(*p == ' ')) p++; if (*p == '\0') return -3; start = p; while((*p != '\0')&&(*p != '=')&&(*p != '\t')&&(*p != ' ')) p++; if (*p == '\0') return -4; name = ap_pstrmemdup(mpd->r->pool, start, (p - start)); while((*p == '\t')||(*p == ' ')) p++; if (*p == '\0') return -5; if (*p != '=') return -13; p++; while((*p == '\t')||(*p == ' ')) p++; if (*p == '\0') return -6; if (*p == '"') { /* quoted */ p++; if (*p == '\0') return -7; start = p; value = ap_pstrdup(mpd->r->pool, p); t = value; while(*p != '\0') { if (*p == '\\') { if (*(p + 1) == '\0') { /* improper escaping */ return -8; } /* only " and \ can be escaped */ if ((*(p + 1) == '"')||(*(p + 1) == '\\')) { p++; } else { /* improper escaping */ /* We allow for now because IE sends * improperly escaped content and there's * nothing we can do about it. * * return -9; */ } } else if (*p == '"') { *t = '\0'; break; } *t++ = *p++; } if (*p == '\0') return -10; p++; /* go over the quote at the end */ } else { /* not quoted */ start = p; while((*p != '\0')&&(is_token_char(*p))) p++; value = ap_pstrmemdup(mpd->r->pool, start, (p - start)); } /* evaluate part */ if (strcmp(name, "name") == 0) { if (mpd->mpp->name != NULL) return -14; mpd->mpp->name = value; sec_debug_log(mpd->r, 9, "multipart_parse_content_disposition: name %s", log_escape_nq(mpd->r->pool, value)); } else if (strcmp(name, "filename") == 0) { if (mpd->mpp->filename != NULL) return -15; mpd->mpp->filename = value; sec_debug_log(mpd->r, 9, "multipart_parse_content_disposition: filename %s", log_escape_nq(mpd->r->pool, value)); } else return -11; if (*p != '\0') { while((*p == '\t')||(*p == ' ')) p++; /* the next character must be a zero or a semi-colon */ if (*p == '\0') return 1; /* this is OK */ if (*p != ';') return -12; p++; /* move over the semi-colon */ } /* loop will stop when (*p == '\0') */ } return 1; } int multipart_process_part_header(multipart_data *mpd, char **error_msg) { int rc; if (error_msg == NULL) return -1; *error_msg = NULL; if ((mpd->buf[0] == '\r')&&(mpd->buf[1] == '\n')&&(mpd->buf[2] == '\0')) { char *header_value; /* empty line */ header_value = (char *)ap_table_get(mpd->mpp->headers, "Content-Disposition"); if (header_value == NULL) { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: part is missing the Content-Disposition header"); return -1; } rc = multipart_parse_content_disposition(mpd, header_value); if (rc < 0) { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: invalid Content-Disposition header (%i): %s", rc, log_escape_nq(mpd->r->pool, header_value)); return -1; } if (mpd->mpp->name == NULL) { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: part name missing"); return -1; } if (mpd->mpp->filename != NULL) { mpd->mpp->type = MULTIPART_FILE; } else { mpd->mpp->type = MULTIPART_FORMDATA; } mpd->mpp_state = 1; mpd->mpp->last_header_name = NULL; } else { /* header line */ if ((mpd->buf[0] == '\t')||(mpd->buf[0] == ' ')) { char *header_value, *new_value, *data; /* header folding, add data to the header we are building */ if (mpd->mpp->last_header_name == NULL) { /* we are not building a header at this moment */ *error_msg = ap_psprintf(mpd->r->pool, "Multipart: invalid part header (invalid folding)"); return -1; } /* locate the beginning of the data */ data = mpd->buf; while((*data == '\t')||(*data == ' ')) data++; new_value = ap_pstrdup(mpd->r->pool, data); sec_remove_lf_crlf_inplace(new_value); /* update the header value in the table */ header_value = (char *)ap_table_get(mpd->mpp->headers, mpd->mpp->last_header_name); new_value = ap_pstrcat(mpd->r->pool, header_value, " ", new_value, NULL); ap_table_set(mpd->mpp->headers, mpd->mpp->last_header_name, new_value); sec_debug_log(mpd->r, 9, "multipart_process_par_header: continued folder header \"%s\" with \"%s\"", log_escape(mpd->r->pool, mpd->mpp->last_header_name), log_escape(mpd->r->pool, data)); if (strlen(new_value) > 4096) { *error_msg = ap_psprintf(mpd->r->pool, "Multpart: invalid part header (too long)"); return -1; } } else { char *header_name, *header_value, *data; /* new header */ data = mpd->buf; while((*data != ':')&&(*data != '\0')) data++; if (*data == '\0') { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: invalid part header (missing colon): %s", log_escape_nq(mpd->r->pool, mpd->buf)); return -1; } header_name = ap_pstrmemdup(mpd->r->pool, mpd->buf, (data - mpd->buf)); /* extract the value value */ data++; while((*data == '\t')||(*data == ' ')) data++; header_value = ap_pstrdup(mpd->r->pool, data); sec_remove_lf_crlf_inplace(header_value); /* error if the name already exists */ if (ap_table_get(mpd->mpp->headers, header_name) != NULL) { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: part header already exists: %s", log_escape_nq(mpd->r->pool, header_name)); return -1; } ap_table_setn(mpd->mpp->headers, header_name, header_value); mpd->mpp->last_header_name = header_name; sec_debug_log(mpd->r, 9, "multipart_process_par_header: added part header \"%s\" \"%s\"", log_escape(mpd->r->pool, header_name), log_escape(mpd->r->pool, header_value)); } } return 1; } int multipart_process_part_data(multipart_data *mpd, char **error_msg) { char *p = mpd->buf + (MULTIPART_BUF_SIZE - mpd->bufleft) - 2; char localreserve[2]; int bytes_reserved = 0; if (error_msg == NULL) return -1; *error_msg = NULL; /* preserve the last two bytes for later */ if (MULTIPART_BUF_SIZE - mpd->bufleft >= 2) { bytes_reserved = 1; localreserve[0] = *p; localreserve[1] = *(p + 1); mpd->bufleft += 2; *p = 0; } /* add data to the part we are building */ if (mpd->mpp->type == MULTIPART_FILE) { /* only store individual files on disk if we are going * to keep them or if we need to have them approved later */ if ((mpd->dcfg->upload_approve_script != NULL)||(mpd->dcfg->upload_keep_files > 0)) { /* first create a temporary file if we don't have it */ if (mpd->mpp->tmp_file_fd == 0) { char *filename = multipart_construct_filename(mpd); /* the temp folder must be chosen in the configuration * create the filename first */ if (mpd->dcfg->upload_dir != NULL) { mpd->mpp->tmp_file_name = ap_psprintf(mpd->p, "%s/%s-%s-%s", mpd->dcfg->upload_dir, current_filetime(mpd->r), mpd->r->connection->remote_ip, filename); } else { mpd->mpp->tmp_file_name = ap_psprintf(mpd->p, "%s/%s-%s-%s", get_temp_folder(mpd->r->pool), current_filetime(mpd->r), mpd->r->connection->remote_ip, filename); } if ((mpd->mpp->tmp_file_fd = open(mpd->mpp->tmp_file_name, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE)) == -1) { /* we've failed while opening the page, so we'll try * again with a more unique filename */ mpd->mpp->tmp_file_name = ap_pstrcat(mpd->p, mpd->mpp->tmp_file_name, "_XXXXXX", NULL); mpd->mpp->tmp_file_fd = sec_mkstemp(mpd->mpp->tmp_file_name); } /* do we have an opened file? */ if (mpd->mpp->tmp_file_fd < 0) { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: Failed to create file \"%s\"", log_escape(mpd->r->pool, mpd->mpp->tmp_file_name)); return -1; } } /* write data to the file */ if (mpd->reserve[0] == 1) { if (write(mpd->mpp->tmp_file_fd, &mpd->reserve[1], 2) != 2) { sec_debug_log(mpd->r, 1, "Multipart: writing to \"%s\" failed.", log_escape(mpd->r->pool, mpd->mpp->tmp_file_name)); } mpd->mpp->tmp_file_size += 2; } if (write(mpd->mpp->tmp_file_fd, mpd->buf, MULTIPART_BUF_SIZE - mpd->bufleft) != MULTIPART_BUF_SIZE - mpd->bufleft) { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: writing to \"%s\" failed.", log_escape(mpd->r->pool, mpd->mpp->tmp_file_name)); return -1; } mpd->mpp->tmp_file_size += (MULTIPART_BUF_SIZE - mpd->bufleft); } else { /* just keep track of the file size */ if (mpd->reserve[0] == 1) mpd->mpp->tmp_file_size += 2; mpd->mpp->tmp_file_size += (MULTIPART_BUF_SIZE - mpd->bufleft); } } else if (mpd->mpp->type == MULTIPART_FORMDATA) { char *value_part = NULL; /* add this part to the list of parts */ if (mpd->reserve[0] == 1) { value_part = ap_pstrcat(mpd->p, &(mpd->reserve[1]), mpd->buf, NULL); } else { value_part = ap_pstrdup(mpd->p, mpd->buf); } *(char **)ap_push_array(mpd->mpp->value_parts) = value_part; sec_debug_log(mpd->r, 9, "Added data to variable: %s", log_escape_nq(mpd->r->pool, value_part)); } else { *error_msg = ap_psprintf(mpd->r->pool, "Multipart: unknown part type %i", mpd->mpp->type); return -1; } /* store the reserved bytes to the multipart * context so that they don't get lost */ if (bytes_reserved) { mpd->reserve[0] = 1; mpd->reserve[1] = localreserve[0]; mpd->reserve[2] = localreserve[1]; } else { mpd->reserve[0] = 0; } return 1; } int multipart_complete(multipart_data *mpd, char **error_log) { if ((mpd->seen_data != 0)&&(mpd->is_complete == 0)) { *error_log = ap_psprintf(mpd->r->pool, "Multipart: final boundary missing"); return -1; } return 1; } int multipart_process_boundary(multipart_data *mpd, int last_part, char **error_msg) { sec_debug_log(mpd->r, 9, "multipart_process_boundary, last_part = %i", last_part); /* if there was a part being built finish it */ if (mpd->mpp != NULL) { /* close the temp file */ if ((mpd->mpp->type == MULTIPART_FILE)&&(mpd->mpp->tmp_file_name != NULL)&&(mpd->mpp->tmp_file_fd != 0)) { close(mpd->mpp->tmp_file_fd); } if (mpd->mpp->type != MULTIPART_FILE) { /* now construct a single string out of the parts */ mpd->mpp->value = ap_array_pstrcat(mpd->r->pool, mpd->mpp->value_parts, 0); if (mpd->mpp->value == NULL) return -1; } /* add the part to the list of parts */ *(multipart_part **)ap_push_array(mpd->parts) = mpd->mpp; if (mpd->mpp->type == MULTIPART_FILE) sec_debug_log(mpd->r, 9, "multipart_process_boundary: added file part %x to the list: name \"%s\" file name \"%s\" size %u", mpd->mpp, log_escape(mpd->r->pool, mpd->mpp->name), log_escape(mpd->r->pool, mpd->mpp->filename), mpd->mpp->tmp_file_size); else sec_debug_log(mpd->r, 9, "multipart_process_boundary: added part %x to the list: name \"%s\"", mpd->mpp, log_escape(mpd->r->pool, mpd->mpp->name)); mpd->mpp = NULL; } if (last_part == 0) { /* start building a new part */ mpd->mpp = (multipart_part *)ap_pcalloc(mpd->p, sizeof(multipart_part)); mpd->mpp->type = MULTIPART_FORMDATA; mpd->mpp_state = 0; mpd->mpp->headers = ap_make_table(mpd->r->pool, 10); mpd->mpp->last_header_name = NULL; mpd->reserve[0] = 0; mpd->reserve[1] = 0; mpd->reserve[2] = 0; mpd->reserve[3] = 0; mpd->mpp->value_parts = ap_make_array(mpd->r->pool, 10, sizeof(char *)); } return 1; } int verify_uploaded_file(request_rec *r, char *file_path, char *approver_script, char **error_msg) { BUFF *p1, *p2, *p3; exec_data *ed = NULL; char buf[129]; int j; if (error_msg == NULL) return -1; *error_msg = NULL; ed = ap_pcalloc(r->pool, sizeof(exec_data)); ed->r = r; ed->command = approver_script; ed->args = file_path; sec_debug_log(r, 4, "verify_uploaded_file: executing \"%s\" to verify \"%s\"", log_escape(r->pool, ed->command), log_escape(r->pool, ed->args)); if (!ap_bspawn_child(r->main ? r->main->pool : r->pool, sec_exec_child, (void *)ed, kill_after_timeout, &p1, &p2, &p3)) { *error_msg = ap_psprintf(r->pool, "Couldn't spawn a child process \"%s\"", log_escape(r->pool, approver_script)); return -1; } /* read some output from the script */ j = ap_bgets(buf, 128, p2); if (j > 0) { int k; buf[j] = 0; /* we only care about the first line of the result */ for(k = 0; k < j; k++) if (buf[k] == 0x0a) buf[k] = 0; sec_debug_log(r, 4, "verify_uploaded_file: got result \"%s\"", log_escape(r->pool, buf)); /* and we also only care about the first character * on the first line; unless that character is '1' * we will not approve the file */ if (buf[0] != '1') { *error_msg = ap_psprintf(r->pool, "File \"%s\" rejected by the approver script \"%s\"", log_escape(r->pool, ed->args), log_escape(r->pool, ed->command)); return 0; } } else { *error_msg = ap_psprintf(r->pool, "Received no output from the approver script (execution failed?) \"%s\"", log_escape(r->pool, ed->command)); return 0; } /* throw the rest away */ while(ap_bgets(buf, 128, p2) > 0); while(ap_bgets(buf, 128, p3) > 0); return 1; } int multipart_verify_uploaded_files(request_rec *r, multipart_data *mpd, char *approver_script, char **error_msg) { multipart_part **parts; int i; if (error_msg == NULL) return -1; *error_msg = NULL; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if (parts[i]->type == MULTIPART_FILE) { if (verify_uploaded_file(r, parts[i]->tmp_file_name, approver_script, error_msg) != 1) return 0; } } return 1; } int multipart_get_variables(multipart_data *mpd, table *parsed_args, sec_dir_config *dcfg, char **error_msg) { multipart_part **parts; char *my_error_msg = NULL; int i; if (error_msg == NULL) return -1; *error_msg = NULL; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if (parts[i]->type == MULTIPART_FORMDATA) { char *name = NULL, *value = NULL; name = normalise_relaxed(mpd->r, dcfg, parts[i]->name, &my_error_msg); if (name == NULL) { *error_msg = ap_psprintf(mpd->r->pool, "Error normalising parameter name: %s", my_error_msg); return -1; } value = normalise_relaxed(mpd->r, dcfg, parts[i]->value, &my_error_msg); if (value == NULL) { *error_msg = ap_psprintf(mpd->r->pool, "Error normalising parameter value: %s", my_error_msg); return -1; } ap_table_add(parsed_args, name, value); } } return 1; } int multipart_contains_files(multipart_data *mpd) { multipart_part **parts; int i, file_count = 0; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if ((parts[i]->type == MULTIPART_FILE) && (parts[i]->filename != NULL) && (strlen(parts[i]->filename) != 0)) file_count++; } return file_count; } multipart_part *multipart_get_part(multipart_data *mpd, char *name) { multipart_part **parts; int i; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if (strcasecmp(parts[i]->name, name) == 0) return parts[i]; } return NULL; } int multipart_check_files_names(modsec_rec *msr, signature *sig, variable *var) { multipart_data *mpd = msr->mpd; multipart_part **parts; int i, rs; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if ((parts[i]->type == MULTIPART_FILE)&&(parts[i]->filename != NULL)) { sec_debug_log(msr->r, 4, "Checking signature \"%s\" at FILES_NAMES(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, parts[i]->name)); rs = check_sig_against_string(msr, sig, parts[i]->filename, VAR_FILES_NAMES, parts[i]->name); if (rs != OK) return rs; } } return OK; } int multipart_check_files_sizes(modsec_rec *msr, signature *sig, variable *var) { multipart_data *mpd = msr->mpd; multipart_part **parts; int i, rs; parts = (multipart_part **)mpd->parts->elts; for(i = 0; i < mpd->parts->nelts; i++) { if ((parts[i]->type == MULTIPART_FILE)&&(parts[i]->filename != NULL)) { char *size_string = ap_psprintf(msr->r->pool, "%u", parts[i]->tmp_file_size); sec_debug_log(msr->r, 4, "Checking signature \"%s\" at FILES_SIZES(\"%s\")", log_escape(msr->r->pool, sig->pattern), log_escape(msr->r->pool, parts[i]->name)); rs = check_sig_against_string(msr, sig, size_string, VAR_FILES_SIZES, parts[i]->name); if (rs != OK) return rs; } } return OK; } module MODULE_VAR_EXPORT security_module = { STANDARD_MODULE_STUFF, sec_init, /* module initializer */ sec_create_dir_config, /* create per-dir config structures */ sec_merge_dir_config, /* merge per-dir config structures */ sec_create_srv_config, /* create per-server config structures */ sec_merge_srv_config, /* merge per-server config structures */ sec_cmds, /* table of config file commands */ NULL, /* [#8] MIME-typed-dispatched handlers */ NULL, /* [#1] URI to filename translation */ NULL, /* [#4] validate user id from request */ NULL, /* [#5] check if the user is ok _here_ */ NULL, /* [#3] check access by host address */ NULL, /* [#6] determine MIME type */ sec_check_access, /* [#7] pre-run fixups */ sec_logger, /* [#9] log a transaction */ NULL, /* [#2] header parser */ sec_child_init, /* child_init */ NULL, /* child_exit */ #ifdef ENABLE_EARLY_HOOK sec_check_access_early, #else NULL /* [#0] post read-request */ #endif };