// SPDX-FileCopyrightText: 2019-2024, Giovanni Dante Grazioli // SPDX-FileCopyrightText: 2016-2019, Thibault 'bui' Koechlin // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include /* used to store locations during the configuration time. then, accessed by the hashtable building feature during "init" time. */ /* ** Static defined rules struct for internal rules. ** We use those to be able to call is_rule_whitelisted_n() on those ** rules at any time ;) */ // clang-format off #define InternalRule(var_name, rule_id, block, drop) \ ngx_http_rule_t var_name = { \ /*type*/ 0, /*whitelist flag*/ 0, \ /*wl_id ptr*/ NULL, /*rule_id*/ rule_id, \ /*log_msg*/ NULL, /*score*/ 0, \ /*sscores*/ NULL, \ /*block*/ block, /*allow*/ 0, \ /*drop*/ drop, /*log*/ 0, \ /*br ptrs*/ NULL \ } InternalRule(nx_int__weird_request, 1, 1, 0); InternalRule(nx_int__big_request, 2, 1, 0); InternalRule(nx_int__uncommon_hex_encoding, 10, 1, 0); InternalRule(nx_int__uncommon_content_type, 11, 1, 0); InternalRule(nx_int__uncommon_url, 12, 1, 0); InternalRule(nx_int__uncommon_post_format, 13, 1, 0); InternalRule(nx_int__uncommon_post_boundary, 14, 1, 0); InternalRule(nx_int__invalid_json, 15, 1, 0); InternalRule(nx_int__empty_post_body, 16, 1, 0); InternalRule(nx_int__libinject_sql, 17, 1, 0); InternalRule(nx_int__libinject_xss, 18, 1, 0); InternalRule(nx_int__no_rules, 19, 0, 1); InternalRule(nx_int__bad_utf8, 20, 0, 1); InternalRule(nx_int__illegal_host_header, 21, 0, 1); // clang-format on #define naxsi_error_fatal(ctx, r, ...) \ do { \ if (ctx) \ ctx->block = 1; \ if (ctx) \ ctx->drop = 1; \ ngx_log_debug(NGX_LOG_DEBUG_HTTP, \ r->connection->log, \ 0, \ "XX-******** NGINX NAXSI INTERNAL ERROR ********"); \ ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, __VA_ARGS__); \ ngx_log_debug(NGX_LOG_DEBUG_HTTP, \ r->connection->log, \ 0, \ "XX-func:%s file:%s line:%d", \ __func__, \ __FILE__, \ __LINE__); \ if (r && r->uri.data) \ ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-uri:%s", r->uri.data); \ } while (0) void ngx_http_naxsi_update_current_ctx_status(ngx_http_request_ctx_t* ctx, ngx_http_naxsi_loc_conf_t* cf, ngx_http_request_t* r); int ngx_http_process_basic_rule_buffer(ngx_str_t* str, ngx_http_rule_t* rl, ngx_int_t* match); void ngx_http_naxsi_payload_handler(ngx_http_request_t* r); int ngx_http_basestr_ruleset_n(ngx_pool_t* pool, ngx_str_t* name, ngx_str_t* value, ngx_array_t* rules, ngx_http_request_t* req, ngx_http_request_ctx_t* ctx, naxsi_match_zone_t zone); void ngx_http_naxsi_body_parse(ngx_http_request_ctx_t* ctx, ngx_http_request_t* r, ngx_http_naxsi_loc_conf_t* cf, ngx_http_naxsi_main_conf_t* main_cf); void ngx_http_naxsi_rawbody_parse(ngx_http_request_ctx_t* ctx, ngx_http_request_t* r, u_char* src, u_int len); unsigned char* ngx_utf8_check(ngx_str_t* str); #if NGX_PCRE2 /* * variables to use pcre2 */ static pcre2_match_data* ngx_pcre2_match_data; static ngx_uint_t ngx_pcre2_match_data_size; /* * helper function to use pcre2 */ ngx_int_t ngx_pcre2_exec(ngx_regex_t* re, unsigned char* str, unsigned int len, ngx_int_t tmp_idx, int* captures, ngx_uint_t size) { size_t* ov; ngx_int_t rc; ngx_uint_t n, i; /* * The pcre2_match() function might allocate memory for backtracking * frames, typical allocations are from 40k and above. So the allocator * is configured to do direct allocations from heap during matching. */ if (ngx_pcre2_match_data == NULL || size > ngx_pcre2_match_data_size) { /* * Allocate a match data if not yet allocated or smaller than * needed. */ if (ngx_pcre2_match_data) { pcre2_match_data_free(ngx_pcre2_match_data); } ngx_pcre2_match_data_size = size; ngx_pcre2_match_data = pcre2_match_data_create(size / 3, NULL); if (ngx_pcre2_match_data == NULL) { rc = PCRE2_ERROR_NOMEMORY; goto failed; } } rc = pcre2_match(re, str, len, tmp_idx, 0, ngx_pcre2_match_data, NULL); if (rc < 0) { goto failed; } n = pcre2_get_ovector_count(ngx_pcre2_match_data); ov = pcre2_get_ovector_pointer(ngx_pcre2_match_data); if (n > size / 3) { n = size / 3; } for (i = 0; i < n; i++) { captures[i * 2] = ov[i * 2]; captures[i * 2 + 1] = ov[i * 2 + 1]; } failed: return rc; } #endif /* ** in : string to inspect, associated rule ** does : apply the rule on the string, return 1 if matched, ** 0 else and -1 on error */ int ngx_http_process_basic_rule_buffer(ngx_str_t* str, ngx_http_rule_t* rl, ngx_int_t* nb_match) { ngx_int_t match, tmp_idx, len; unsigned char* ret; int captures[30]; if (!rl->br || !nb_match) return (-1); *nb_match = 0; if (rl->br->match_type == RX && rl->br->rx) { tmp_idx = 0; len = str->len; while #if (NGX_PCRE2) (tmp_idx < len && (match = ngx_pcre2_exec(rl->br->rx->regex, str->data, str->len, tmp_idx, captures, 30)) >= 0) #elif defined nginx_version && (nginx_version >= 1002002 && nginx_version != 1003000) (tmp_idx < len && (match = pcre_exec(rl->br->rx->regex->code, 0, (const char*)str->data, str->len, tmp_idx, 0, captures, 30)) >= 0) #elif defined nginx_version && (nginx_version > 1001011) (tmp_idx < len && (match = pcre_exec(rl->br->rx->regex->pcre, 0, (const char*)str->data, str->len, tmp_idx, 0, captures, 30)) >= 0) #elif defined nginx_version && (nginx_version <= 1001011) (tmp_idx < len && (match = pcre_exec( rl->br->rx->regex, 0, (const char*)str->data, str->len, tmp_idx, 0, captures, 30)) >= 0) #elif defined nginx_version #error "Inconsistent nginx version." (0) #else #error "nginx_version not defined." (0) #endif { *nb_match += match; tmp_idx = captures[1]; } if (*nb_match > 0) { if (rl->br->negative) return (0); else return (1); } else if (*nb_match == 0) { if (rl->br->negative) return (1); else return (0); } return (-1); } else if (rl->br->match_type == STR && rl->br->str) { match = 0; tmp_idx = 0; while (1) { ret = (unsigned char*)strfaststr((unsigned char*)str->data + tmp_idx, (unsigned int)str->len - tmp_idx, (unsigned char*)rl->br->str->data, (unsigned int)rl->br->str->len); if (ret) { match = 1; *nb_match = *nb_match + 1; } else break; if (nb_match && ret < (str->data + str->len)) { tmp_idx = (ret - str->data) + 1; if (tmp_idx > (int)(str->len - 1)) break; } else break; } if (match) { if (rl->br->negative) return (0); else return (1); } else { if (rl->br->negative) return (1); else return (0); } } else if (rl->br->match_type == LIBINJ_XSS) { if (libinjection_xss((const char*)str->data, str->len) == 1) return (1); } else if (rl->br->match_type == LIBINJ_SQL) { sfilter state; libinjection_sqli_init(&state, (const char*)str->data, str->len, FLAG_NONE); if (libinjection_is_sqli(&state) == 1) return (1); } return (0); } /* ** Check if a (matched) rule is whitelisted. ** This func will look for the current URI in the wlr_url_hash [hashtable] ** It will also look for varname in the wlr_body|args|headers_hash [hashtable] ** and It will also look for disabled rules. ** 1 - If the rule is disabled, it's whitelisted ** 2 - If a matching URL is found, check if the further information confirms *that the rule should be whitelisted ** ($URL:/bar|$ARGS_VAR:foo : it's not because URL matches that we should *whitelist rule) ** 3 - If a matching varname is found, check zone and rules IDs. ** [TODO] : Add mz matches with style BODY|HEADERS|... ** returns (1) if rule is whitelisted, else (0) */ int ngx_http_naxsi_is_whitelist_adapted(ngx_http_whitelist_rule_t* b, ngx_str_t* name, naxsi_match_zone_t zone, ngx_http_rule_t* r, ngx_http_request_t* req, naxsi_match_type_t type, ngx_int_t target_name) { /* if something was found, check the rule ID */ if (!b) { return (0); } /* FILE_EXT zone is just a hack, as it indeed targets BODY */ if (zone == FILE_EXT) zone = BODY; NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Possible whitelist ... check..."); /* if whitelist targets arg name, but the rules hit content*/ if (b->target_name && !target_name) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "whitelist targets name, but rule matched content."); return (0); } /* if if the whitelist target contents, but the rule hit arg name*/ if (!b->target_name && target_name) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "whitelist targets content, but rule matched name."); return (0); } if (type == NAME_ONLY) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Name match in zone %s", zone == ARGS ? "ARGS" : zone == BODY ? "BODY" : zone == HEADERS ? "HEADERS" : "UNKNOWN!!!!!"); // False Positive, there was a whitelist that matches the argument name, // But is was actually matching an existing URI name. if (b->zone == ANY && (zone == ARGS || zone == BODY || zone == HEADERS)) { return (nx_check_ids(r->rule_id, b->ids)); } else if (zone != b->zone || b->uri_only) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "bad whitelist, name match, but WL was only on URL."); return (0); } return (nx_check_ids(r->rule_id, b->ids)); } if (type == URI_ONLY || type == MIXED) { /* zone must match */ if (b->uri_only && type != URI_ONLY) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "bad whitelist, type is URI_ONLY, but not whitelist"); return (0); } if (zone != b->zone && b->zone != ANY) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "bad whitelist, URL match, but not zone"); return (0); } return (nx_check_ids(r->rule_id, b->ids)); } NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "finished wl check, failed."); return (0); } ngx_http_whitelist_rule_t* nx_find_wl_in_hash(ngx_http_request_t* req, ngx_str_t* mstr, ngx_http_naxsi_loc_conf_t* cf, naxsi_match_zone_t zone) { ngx_int_t k = 0; ngx_http_whitelist_rule_t* b = NULL; ngx_str_t scratch = { .data = NULL, .len = mstr->len }; scratch.data = ngx_pcalloc(req->pool, mstr->len + 1); if (!scratch.data) return NULL; k = ngx_hash_strlow(scratch.data, mstr->data, mstr->len); if ((zone == BODY || zone == FILE_EXT || zone == ANY) && cf->wlr_body_hash && cf->wlr_body_hash->size > 0) { b = (ngx_http_whitelist_rule_t*)ngx_hash_find( cf->wlr_body_hash, k, (u_char*)scratch.data, scratch.len); } if ((zone == HEADERS || (zone == ANY && !b)) && cf->wlr_headers_hash && cf->wlr_headers_hash->size > 0) { b = (ngx_http_whitelist_rule_t*)ngx_hash_find( cf->wlr_headers_hash, k, (u_char*)scratch.data, scratch.len); } if ((zone == URL || (zone == ANY && !b)) && cf->wlr_url_hash && cf->wlr_url_hash->size > 0) { b = (ngx_http_whitelist_rule_t*)ngx_hash_find( cf->wlr_url_hash, k, (u_char*)scratch.data, scratch.len); } if ((zone == ARGS || (zone == ANY && !b)) && cf->wlr_args_hash && cf->wlr_args_hash->size > 0) { b = (ngx_http_whitelist_rule_t*)ngx_hash_find( cf->wlr_args_hash, k, (u_char*)scratch.data, scratch.len); } NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "find wl in hash %d %p %s.", k, b, zone == ARGS ? "ARGS" : zone == HEADERS ? "HEADERS" : zone == BODY ? "BODY" : zone == URL ? "URL" : zone == FILE_EXT ? "FILE_EXT" : zone == RAW_BODY ? "RAW_BODY" : zone == ANY ? "ANY" : "UNKNOWN"); ngx_pfree(req->pool, scratch.data); return (b); } int naxsi_can_ignore_ip(const ngx_str_t* ipstr, ngx_http_naxsi_loc_conf_t* cf) { if (!cf->ignore_ips || cf->ignore_ips_ha.keys.nelts < 1) { return 0; } char ip_str[INET6_ADDRSTRLEN + 1] = { 0 }; if (!naxsi_parse_ip(ipstr, NULL, ip_str)) { return 0; } ngx_str_t scratch = { .data = (unsigned char*)ip_str, .len = strlen(ip_str) }; ngx_uint_t k = ngx_hash_key(scratch.data, scratch.len); return ngx_hash_find(cf->ignore_ips, k, (u_char*)scratch.data, scratch.len) != NULL ? 1 : 0; } int naxsi_can_ignore_cidr(const ngx_str_t* ipstr, ngx_http_naxsi_loc_conf_t* cf) { if (!cf->ignore_cidrs) { return 0; } ngx_uint_t i; ip_t ip = ip_zero; if (!naxsi_parse_ip(ipstr, &ip, NULL)) { return 0; } for (i = 0; i < cf->ignore_cidrs->nelts; i++) { cidr_t* cidr = &((cidr_t*)cf->ignore_cidrs->elts)[i]; if (naxsi_is_in_subnet(cidr, &ip)) { return 1; } } return 0; } #define custloc_array(x) ((ngx_http_custom_rule_location_t*)x) /* ** wrapper used for regex matchzones. Should be used by classic basestr* as *well. */ int ngx_http_naxsi_pcre_wrapper(ngx_regex_compile_t* rx, unsigned char* str, unsigned int len) { int match; int captures[30]; #if (NGX_PCRE2) match = ngx_pcre2_exec(rx->regex, str, len, 0, captures, 1); #elif defined nginx_version && (nginx_version >= 1002002 && nginx_version != 1003000) match = pcre_exec(rx->regex->code, 0, (const char*)str, len, 0, 0, captures, 1); #elif defined nginx_version && (nginx_version > 1001011) match = pcre_exec(rx->regex->pcre, 0, (const char*)str, len, 0, 0, captures, 1); #elif defined nginx_version && (nginx_version <= 1001011) match = pcre_exec(rx->regex, 0, (const char*)str, len, 0, 0, captures, 1); #elif defined nginx_version #error "Inconsistent nginx version." return (0); #else #error "nginx_version not defined." return (0); #endif if (match > 0) return (1); return (match); } static int ngx_http_rule_match_zone_rx(ngx_http_rule_t* p, naxsi_match_zone_t zone) { if (p->br->zone != ANY) { return (1); } else if (zone == FILE_EXT && p->br->file_ext) { // mz:$URL_X|FILE_EXT is defined as br->zone = BODY, // to allow the correct match the zone must be matched // against br->file_ext return (1); } return zone == p->br->zone; } int ngx_http_naxsi_is_rule_whitelisted_rx(ngx_http_request_t* req, ngx_http_naxsi_loc_conf_t* cf, ngx_http_rule_t* r, ngx_str_t* name, naxsi_match_zone_t zone, ngx_int_t target_name) { ngx_http_rule_t* p; ngx_uint_t i, x; int rx_match, violation; /* Look it up in regexed whitelists for matchzones */ if (!cf->rxmz_wlr || cf->rxmz_wlr->nelts < 1) { return (0); } NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "RXX - Trying to find rx for %v", name); for (i = 0; i < cf->rxmz_wlr->nelts; i++) { p = (((ngx_http_rule_t**)(cf->rxmz_wlr->elts))[i]); if (!p->br || !p->br->custom_locations || p->br->custom_locations->nelts < 1) { NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Rule pushed to RXMZ, but has no custom_location."); continue; } /* ** once we have pointer to the rule : ** - go through each custom location (ie. ARGS_VAR_X:foobar*) ** - verify that regular expressions match. If not, it means whitelist does *not apply. */ NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "%d/%d RXMZ rule has %d custom locations", i, cf->rxmz_wlr->nelts, p->br->custom_locations->nelts); if (!ngx_http_rule_match_zone_rx(p, zone)) { NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "%d/%d Not targeting same zone.", i, cf->rxmz_wlr->nelts); continue; } if (target_name != p->br->target_name) { NX_DEBUG( _debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "only one target_name"); continue; } for (x = 0, violation = 0; x < p->br->custom_locations->nelts && violation == 0; x++) { /* does custom location targets a body var ? */ if (custloc_array(p->br->custom_locations->elts)[x].body_var) { rx_match = ngx_http_naxsi_pcre_wrapper( custloc_array(p->br->custom_locations->elts)[x].target_rx, name->data, name->len); if (rx_match < 0) { violation = 1; NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "[BODY] FAIL:%d (rx:%V, str:%V)", rx_match, &(custloc_array(p->br->custom_locations->elts)[x].target_rx->pattern), name); break; } NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "[BODY] Match:%d (rx:%V, str:%V)", rx_match, &(custloc_array(p->br->custom_locations->elts)[x].target_rx->pattern), name); } if (custloc_array(p->br->custom_locations->elts)[x].args_var) { rx_match = ngx_http_naxsi_pcre_wrapper( custloc_array(p->br->custom_locations->elts)[x].target_rx, name->data, name->len); if (rx_match < 0) { violation = 1; NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "[ARGS] FAIL:%d (rx:%V, str:%V)", rx_match, &(custloc_array(p->br->custom_locations->elts)[x].target_rx->pattern), name); break; } NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "[ARGS] Match:%d (rx:%V, str:%V)", rx_match, &(custloc_array(p->br->custom_locations->elts)[x].target_rx->pattern), name); } if (custloc_array(p->br->custom_locations->elts)[x].specific_url) { /* if there is a specific url, check it regardless of zone. */ rx_match = ngx_http_naxsi_pcre_wrapper( custloc_array(p->br->custom_locations->elts)[x].target_rx, req->uri.data, req->uri.len); if (rx_match < 0) { NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "[URI] FAIL:%d (rx:%V, str:%V)", rx_match, &(custloc_array(p->br->custom_locations->elts)[x].target_rx->pattern), &(req->uri)); violation = 1; break; } NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "[URI] Match:%d (rx:%V, str:%V)", rx_match, &(custloc_array(p->br->custom_locations->elts)[x].target_rx->pattern), &(req->uri)); } } if (violation == 0) { NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "wut, rule whitelisted by rx."); if (nx_check_ids(r->rule_id, p->wlid_array) == 1) return (1); } else { NX_DEBUG(_debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "not good ----"); } } return (0); } int ngx_http_naxsi_is_rule_whitelisted_n(ngx_http_request_t* req, ngx_http_naxsi_loc_conf_t* cf, ngx_http_rule_t* r, ngx_str_t* name, naxsi_match_zone_t zone, ngx_int_t target_name) { ngx_int_t k = 0; ngx_http_whitelist_rule_t* b = NULL; unsigned int i = 0; ngx_http_rule_t** dr = NULL; ngx_str_t tmp_hashname = { 0 }; ngx_str_t nullname = ngx_null_string; /* if name is NULL, replace it by an empty string */ if (!name) name = &nullname; NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "is rule [%d] whitelisted in zone %s for item %V", r->rule_id, zone == ARGS ? "ARGS" : zone == HEADERS ? "HEADERS" : zone == BODY ? "BODY" : zone == URL ? "URL" : zone == FILE_EXT ? "FILE_EXT" : zone == RAW_BODY ? "RAW_BODY" : zone == ANY ? "ANY" : "UNKNOWN", name); NX_DEBUG(_debug_whitelist_compat && target_name, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "extra: exception happened in |NAME"); /* Check if the rule is part of disabled rules for this location */ if (cf->disabled_rules) { dr = cf->disabled_rules->elts; for (i = 0; i < cf->disabled_rules->nelts; i++) { /* Is rule disabled ? */ if (nx_check_ids(r->rule_id, dr[i]->wlid_array)) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled somewhere", r->rule_id); /* if it doesn't specify zone, skip zone-check */ if (!dr[i]->br) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "no zone, skip zone-check"); continue; } /* If rule target nothing, it's whitelisted everywhere */ if (!(dr[i]->br->args || dr[i]->br->headers || dr[i]->br->body || dr[i]->br->url)) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is fully disabled", r->rule_id); return (1); } /* if exc is in name, but rule is not specificaly disabled for name (and * targets a zone) */ if (target_name != dr[i]->br->target_name) continue; switch (zone) { case ANY: if (dr[i]->br->any) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled in ANY", r->rule_id); return (1); } break; case ARGS: if (dr[i]->br->args) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled in ARGS", r->rule_id); return (1); } break; case HEADERS: if (dr[i]->br->headers) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled in HEADERS", r->rule_id); return (1); } break; case BODY: if (dr[i]->br->body) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled in BODY", r->rule_id); return (1); } break; case RAW_BODY: if (dr[i]->br->body) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled in BODY", r->rule_id); return (1); } break; case FILE_EXT: if (dr[i]->br->file_ext) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled in FILE_EXT", r->rule_id); return (1); } break; case URL: if (dr[i]->br->url) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule %d is disabled in URL zone:%d", r->rule_id, zone); return (1); } break; default: break; } } } } NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "hashing varname [%V]", name); /* ** check for ARGS_VAR:x(|NAME) whitelists. ** (name) or (#name) */ if (name->len > 0) { NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "hashing varname [%V] (rule:%d) - 'wl:X_VAR:%V'", name, r->rule_id, name); /* try to find in hashtables */ b = nx_find_wl_in_hash(req, name, cf, zone); if (ngx_http_naxsi_is_whitelist_adapted(b, name, zone, r, req, NAME_ONLY, target_name)) { return (1); } /*prefix hash with '#', to find whitelists that would be done only on * ARGS_VAR:X|NAME */ tmp_hashname.len = name->len + 1; /* too bad we have to realloc just to add the '#' */ tmp_hashname.data = ngx_pcalloc(req->pool, tmp_hashname.len + 1); if (!tmp_hashname.data) return (0); tmp_hashname.data[0] = '#'; memcpy(tmp_hashname.data + 1, name->data, name->len); NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "hashing varname [%V] (rule:%d) - 'wl:X_VAR:%V|NAME'", name, r->rule_id, name); b = nx_find_wl_in_hash(req, &tmp_hashname, cf, zone); ngx_pfree(req->pool, tmp_hashname.data); tmp_hashname.data = NULL; if (ngx_http_naxsi_is_whitelist_adapted(b, name, zone, r, req, NAME_ONLY, target_name)) { return (1); } } /* Plain URI whitelists */ if (cf->wlr_url_hash && cf->wlr_url_hash->size > 0) { /* check the URL no matter what zone we're in */ tmp_hashname.data = ngx_pcalloc(req->pool, req->uri.len + 1); /* mimic find_wl_in_hash, we are looking in a different hashtable */ if (!tmp_hashname.data) return (0); tmp_hashname.len = req->uri.len; k = ngx_hash_strlow(tmp_hashname.data, req->uri.data, req->uri.len); NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "hashing uri [%V] (rule:%d) 'wl:$URI:%V|*' %d", &req->uri, r->rule_id, &tmp_hashname, k); b = (ngx_http_whitelist_rule_t*)ngx_hash_find( cf->wlr_url_hash, k, (u_char*)tmp_hashname.data, tmp_hashname.len); ngx_pfree(req->pool, tmp_hashname.data); tmp_hashname.data = NULL; NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "%p", b); if (ngx_http_naxsi_is_whitelist_adapted(b, name, zone, r, req, URI_ONLY, target_name)) { return (1); } } /* Lookup for $URL|URL (uri)*/ tmp_hashname.data = ngx_pcalloc(req->pool, req->uri.len + 1); if (!tmp_hashname.data) return (0); tmp_hashname.len = req->uri.len; ngx_memcpy(tmp_hashname.data, req->uri.data, req->uri.len); NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "hashing uri#1 [%V] (rule:%d) ($URL:%V|URI)", &req->uri, r->rule_id, &tmp_hashname); b = nx_find_wl_in_hash(req, &tmp_hashname, cf, zone); ngx_pfree(req->pool, tmp_hashname.data); tmp_hashname.data = NULL; if (ngx_http_naxsi_is_whitelist_adapted(b, name, zone, r, req, URI_ONLY, target_name)) { return (1); } /* Looking $URL:x|ZONE|NAME */ tmp_hashname.data = ngx_pcalloc(req->pool, req->uri.len + 2); /* should make it sound crit isn't it ?*/ if (!tmp_hashname.data) return (0); tmp_hashname.len = req->uri.len + 1; tmp_hashname.data[0] = '#'; ngx_memcpy(tmp_hashname.data + 1, req->uri.data, req->uri.len); NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "hashing uri#3 [%V] (rule:%d) ($URL:%V|ZONE|NAME)", &req->uri, r->rule_id, &tmp_hashname); b = nx_find_wl_in_hash(req, &tmp_hashname, cf, zone); ngx_pfree(req->pool, tmp_hashname.data); tmp_hashname.data = NULL; if (ngx_http_naxsi_is_whitelist_adapted(b, name, zone, r, req, URI_ONLY, target_name)) { return (1); } /* Maybe it was $URL+$VAR (uri#name) or (#uri#name) */ tmp_hashname.len = req->uri.len + 1 + name->len; /* one extra byte for target_name '#' */ tmp_hashname.data = ngx_pcalloc(req->pool, tmp_hashname.len + 2); if (!tmp_hashname.data) return (0); if (target_name) { tmp_hashname.len++; strcat((char*)tmp_hashname.data, "#"); } strncat((char*)tmp_hashname.data, (char*)req->uri.data, req->uri.len); strcat((char*)tmp_hashname.data, "#"); strncat((char*)tmp_hashname.data, (char*)name->data, name->len); NX_DEBUG(_debug_whitelist_compat, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "hashing MIX [%V] (rule:%d) ($URL:%V|$X_VAR:y) or ($URL:%V|$X_VAR:y|NAME)", &req->uri, r->rule_id, &tmp_hashname, &tmp_hashname); b = nx_find_wl_in_hash(req, &(tmp_hashname), cf, zone); ngx_pfree(req->pool, tmp_hashname.data); if (ngx_http_naxsi_is_whitelist_adapted(b, name, zone, r, req, MIXED, target_name)) { return (1); } /* ** Look it up in regexed whitelists for matchzones */ if (ngx_http_naxsi_is_rule_whitelisted_rx(req, cf, r, name, zone, target_name) == 1) { NX_DEBUG( _debug_wl_debug_rx, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Whitelisted by RX !"); return (1); } return (0); } char* replace_str(const char* s, const char* oldW, const char* newW) { char* result; int i, cnt = 0; int newWlen = strlen(newW); int oldWlen = strlen(oldW); // Counting the number of times old word // occur in the string for (i = 0; s[i] != '\0'; i++) { if (strstr(&s[i], oldW) == &s[i]) { cnt++; // Jumping to index after the old word. i += oldWlen - 1; } } // Making new string of enough length result = (char*)malloc(i + cnt * (newWlen - oldWlen) + 1); if (!result) return 0; i = 0; while (*s) { // compare the substring with the result if (strstr(s, oldW) == s) { strcpy(&result[i], newW); i += newWlen; s += oldWlen; } else { result[i++] = *s++; } } result[i] = '\0'; return result; } ngx_int_t ngx_http_output_forbidden_page(ngx_http_request_ctx_t* ctx, ngx_http_request_t* r) { ngx_str_t* denied_uri = NULL; ngx_str_t empty = ngx_string(""); ngx_http_naxsi_loc_conf_t* cf = NULL; ngx_table_elt_t* h = NULL; cf = ngx_http_get_module_loc_conf(r, ngx_http_naxsi_module); if (naxsi_log_request(ctx, r, &denied_uri) != NGX_HTTP_OK) { return (NGX_ERROR); } /* ** If we shouldn't block the request, ** but a log score was reached, stop. */ if (ctx->log && (!ctx->block && !ctx->drop)) { return (NGX_DECLINED); } /* ** If we are in learning without post_action and without drop ** stop here as well. */ if (ctx->learning && !ctx->post_action && !ctx->drop) { return (NGX_DECLINED); } /* ** add headers with original url ** and arguments, as well as ** the first fragment of log */ #define NAXSI_HEADER_ORIG_URL "x-orig_url" #define NAXSI_HEADER_ORIG_ARGS "x-orig_args" #define NAXSI_HEADER_NAXSI_SIG "x-naxsi_sig" if (r->headers_in.headers.last) { h = ngx_list_push(&(r->headers_in.headers)); if (!h) return (NGX_ERROR); h->key.len = strlen(NAXSI_HEADER_ORIG_URL); h->key.data = ngx_pcalloc(r->pool, strlen(NAXSI_HEADER_ORIG_URL) + 1); if (!h->key.data) return (NGX_ERROR); memcpy(h->key.data, NAXSI_HEADER_ORIG_URL, strlen(NAXSI_HEADER_ORIG_URL)); h->lowcase_key = ngx_pcalloc(r->pool, strlen(NAXSI_HEADER_ORIG_URL) + 1); if (!h->lowcase_key) return (NGX_ERROR); memcpy(h->lowcase_key, NAXSI_HEADER_ORIG_URL, strlen(NAXSI_HEADER_ORIG_URL)); h->value.len = denied_uri->len; h->value.data = ngx_pcalloc(r->pool, denied_uri->len + 1); if (!h->value.data) return (NGX_ERROR); memcpy(h->value.data, denied_uri->data, denied_uri->len); h = ngx_list_push(&(r->headers_in.headers)); if (!h) return (NGX_ERROR); h->key.len = strlen(NAXSI_HEADER_ORIG_ARGS); h->key.data = ngx_pcalloc(r->pool, strlen(NAXSI_HEADER_ORIG_ARGS) + 1); if (!h->key.data) return (NGX_ERROR); memcpy(h->key.data, NAXSI_HEADER_ORIG_ARGS, strlen(NAXSI_HEADER_ORIG_ARGS)); h->lowcase_key = ngx_pcalloc(r->pool, strlen(NAXSI_HEADER_ORIG_ARGS) + 1); if (!h->lowcase_key) return (NGX_ERROR); memcpy(h->lowcase_key, NAXSI_HEADER_ORIG_ARGS, strlen(NAXSI_HEADER_ORIG_ARGS)); h->value.len = r->args.len; h->value.data = ngx_pcalloc(r->pool, r->args.len + 1); if (!h->value.data) return (NGX_ERROR); memcpy(h->value.data, r->args.data, r->args.len); h = ngx_list_push(&(r->headers_in.headers)); if (!h) return (NGX_ERROR); h->key.len = strlen(NAXSI_HEADER_NAXSI_SIG); h->key.data = ngx_pcalloc(r->pool, strlen(NAXSI_HEADER_NAXSI_SIG) + 1); if (!h->key.data) return (NGX_ERROR); memcpy(h->key.data, NAXSI_HEADER_NAXSI_SIG, strlen(NAXSI_HEADER_NAXSI_SIG)); h->lowcase_key = ngx_pcalloc(r->pool, strlen(NAXSI_HEADER_NAXSI_SIG) + 1); if (!h->lowcase_key) return (NGX_ERROR); memcpy(h->lowcase_key, NAXSI_HEADER_NAXSI_SIG, strlen(NAXSI_HEADER_NAXSI_SIG)); const u_char* naxsi_sig = naxsi_request_id(r); h->value.len = strlen((const char*)naxsi_sig); h->value.data = ngx_pcalloc(r->pool, h->value.len + 1); if (!h->value.data) return (NGX_ERROR); memcpy(h->value.data, naxsi_sig, h->value.len); } if (ctx->learning && !ctx->drop) { if (ctx->post_action) { ngx_http_core_loc_conf_t* clcf; clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); clcf->post_action.data = cf->denied_url->data; clcf->post_action.len = cf->denied_url->len; } return (NGX_DECLINED); } else { ngx_http_internal_redirect(r, cf->denied_url, &empty); return (NGX_HTTP_OK); } } /* ** new rulematch, less arguments ^ */ int ngx_http_apply_rulematch_v_n(ngx_http_rule_t* r, ngx_http_request_ctx_t* ctx, ngx_http_request_t* req, ngx_str_t* name, ngx_str_t* value, naxsi_match_zone_t zone, ngx_int_t nb_match, ngx_int_t target_name) { unsigned int found = 0, i, z; ngx_http_special_score_t * sc, *rsc; ngx_http_naxsi_loc_conf_t* cf; ngx_http_matched_rule_t* mr; ngx_str_t empty = ngx_string(""); if (!name) { name = ∅ } if (!value) { value = ∅ } cf = ngx_http_get_module_loc_conf(req, ngx_http_naxsi_module); if (!cf || !ctx) return (0); if (ngx_http_naxsi_is_rule_whitelisted_n(req, cf, r, name, zone, target_name) == 1) { NX_DEBUG( _debug_whitelist_light, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "rule is whitelisted."); return (0); } NX_DEBUG(_debug_extensive_log, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Current extensive log value: %d", ctx->extensive_log); if (ctx->extensive_log) { if (target_name) { naxsi_log_extensive(ctx, req, value, name, r, zone, target_name); } else { naxsi_log_extensive(ctx, req, name, value, r, zone, target_name); } } if (nb_match == 0) nb_match = 1; if (!ctx->matched) ctx->matched = ngx_array_create(req->pool, 2, sizeof(ngx_http_matched_rule_t)); /* log stuff, cause this case sux */ if (!ctx->matched) return (0); mr = ngx_array_push(ctx->matched); if (!mr) return (0); memset(mr, 0, sizeof(ngx_http_matched_rule_t)); if (target_name) mr->target_name = 1; switch (zone) { case HEADERS: mr->headers_var = 1; break; case URL: mr->url = 1; break; case ARGS: mr->args_var = 1; break; case BODY: mr->body_var = 1; break; case FILE_EXT: mr->file_ext = 1; break; default: break; }; mr->rule = r; // the current "name" ptr will be free by caller, so make a copy mr->name = ngx_pcalloc(req->pool, sizeof(ngx_str_t)); if (mr->name) { if (name->len > 0) { mr->name->data = ngx_pcalloc(req->pool, name->len + 1); if (mr->name->data) { memcpy(mr->name->data, name->data, name->len); mr->name->len = name->len; } } else { mr->name->data = NULL; mr->name->len = 0; } } /* apply special score on rulematch */ if (r->sscores) { NX_DEBUG(_debug_whitelist, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Rule applies %d custom scores", r->sscores->nelts); if (!ctx->special_scores) // create the list ctx->special_scores = ngx_array_create(req->pool, 1, sizeof(ngx_http_special_score_t)); if (ctx->special_scores) { rsc = r->sscores->elts; for (z = 0; z < r->sscores->nelts; z++) { // search into the list for matching special score found = 0; sc = ctx->special_scores->elts; for (i = 0; i < ctx->special_scores->nelts; i++) { if (rsc[z].sc_tag && sc[i].sc_tag && sc[i].sc_tag->len == rsc[z].sc_tag->len && !ngx_strcmp(sc[i].sc_tag->data, rsc[z].sc_tag->data)) { NX_DEBUG(_debug_whitelist, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Special Score (%V) actual=%d,next=%d", rsc[z].sc_tag, sc[i].sc_score, sc[i].sc_score + (rsc[z].sc_score * nb_match)); sc[i].sc_score += (rsc[z].sc_score * nb_match); found = 1; break; } } if (!found) { NX_DEBUG(_debug_whitelist, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "Special Score (%V) next=%d", rsc[z].sc_tag, (rsc[z].sc_score * nb_match)); sc = ngx_array_push(ctx->special_scores); if (sc) { memset(sc, 0, sizeof(ngx_http_special_score_t)); sc->sc_tag = rsc[z].sc_tag; sc->sc_score = (rsc[z].sc_score * nb_match); } } } } } /* else, apply normal score */ ctx->score += (r->score * nb_match); if (r->block) ctx->block = 1; if (r->allow) ctx->allow = 1; if (r->drop) ctx->drop = 1; if (r->log) ctx->log = 1; ngx_http_naxsi_update_current_ctx_status(ctx, cf, req); return (1); } /* ** does : this functions receives an string in the form [foo=bar&bla=foo..] ** it splits the string into varname/value couples, and then pass ** this couple along with valid rules to checking func. ** WARN/TODO : Even I tried to make my code bof proof, this should be seriously *audited :) */ int ngx_http_spliturl_ruleset(ngx_pool_t* pool, ngx_str_t* nx_str, ngx_array_t* rules, ngx_array_t* main_rules, ngx_http_request_t* req, ngx_http_request_ctx_t* ctx, naxsi_match_zone_t zone) { ngx_str_t name, val; char * eq, *ev, *orig, *str; int len, full_len; int nullbytes = 0; if (naxsi_escape_nullbytes(nx_str) > 0) ngx_http_apply_rulematch_v_n(&nx_int__uncommon_hex_encoding, ctx, req, NULL, NULL, zone, 1, 0); str = (char*)nx_str->data; NX_DEBUG(_debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-check url-like [%s]", str); orig = str; full_len = strlen(orig); if (full_len > 0 && *(orig + full_len - 1) == '&') { NX_DEBUG(_debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-url unexpected last '&' [%s]", str); ngx_http_apply_rulematch_v_n(&nx_int__uncommon_url, ctx, req, NULL, NULL, zone, 1, 0); } while (str < (orig + full_len) && *str) { if (*str == '&') { NX_DEBUG(_debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-url unexpected '&' [%s]", str); ngx_http_apply_rulematch_v_n(&nx_int__uncommon_url, ctx, req, NULL, NULL, zone, 1, 0); str++; continue; } if ((ctx->block && !ctx->learning) || ctx->drop) return (0); eq = strchr(str, '='); ev = strchr(str, '&'); if (!eq /*?foobar | ?foobar& */ || (eq && ev && eq > ev)) /*?foobar&bla=test*/ { NX_DEBUG(_debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-url has no '=' or '&' before '=' [%s]", str); if (!ev) ev = str + strlen(str); /* len is now [name] */ len = ev - str; val.data = (unsigned char*)str; val.len = ev - str; name.data = (unsigned char*)NULL; name.len = 0; } else /* should be normal like ?var=bar& ..*/ { NX_DEBUG(_debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-Classic format url [%s]", str); if (!ev) /* ?bar=lol */ ev = str + strlen(str); /* len is now [name]=[content] */ len = ev - str; eq++; val.data = (unsigned char*)eq; val.len = ev - eq; name.data = (unsigned char*)str; name.len = eq - str - 1; } if (name.len) { nullbytes = naxsi_unescape(&name); if (nullbytes > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, req, &name, &val, zone, 1, 1); } } if (val.len) { nullbytes = naxsi_unescape(&val); if (nullbytes > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, req, &name, &val, zone, 1, 0); } } NX_DEBUG(_debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-extract [%V]=[%V]", &(name), &(val)); if (rules) { ngx_http_basestr_ruleset_n(pool, &name, &val, rules, req, ctx, zone); } else { NX_DEBUG( _debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-no arg rules ?"); } if (main_rules) { ngx_http_basestr_ruleset_n(pool, &name, &val, main_rules, req, ctx, zone); } else { NX_DEBUG( _debug_spliturl_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-no main rules ?"); } str += len + 1; } return (0); } /* ** check variable + name against a set of rules, checking against 'custom' *location rules too. */ void ngx_http_libinjection(ngx_pool_t* pool, ngx_str_t* name, ngx_str_t* value, ngx_http_request_ctx_t* ctx, ngx_http_request_t* req, naxsi_match_zone_t zone) { /* ** Libinjection integration : ** 1 - check if libinjection_sql is explicitly enabled ** 2 - check if libinjection_xss is explicitly enabled ** if 1 is true : perform check on both name and content, ** in case of match, apply internal rule ** increasing the LIBINJECTION_SQL score ** if 2 is true ; same as for '1' but with ** LIBINJECTION_XSS */ sfilter state; int issqli; if (ctx->libinjection_sql) { /* hardcoded call to libinjection on NAME, apply internal rule if matched. */ libinjection_sqli_init(&state, (const char*)name->data, name->len, FLAG_NONE); issqli = libinjection_is_sqli(&state); if (issqli == 1) { ngx_http_apply_rulematch_v_n(&nx_int__libinject_sql, ctx, req, name, value, zone, 1, 1); } /* hardcoded call to libinjection on CONTENT, apply internal rule if * matched. */ libinjection_sqli_init(&state, (const char*)value->data, value->len, FLAG_NONE); issqli = libinjection_is_sqli(&state); if (issqli == 1) { ngx_http_apply_rulematch_v_n(&nx_int__libinject_sql, ctx, req, name, value, zone, 1, 0); } } if (ctx->libinjection_xss) { /* first on var_name */ issqli = libinjection_xss((const char*)name->data, name->len); if (issqli == 1) { ngx_http_apply_rulematch_v_n(&nx_int__libinject_xss, ctx, req, name, value, zone, 1, 1); } /* hardcoded call to libinjection on CONTENT, apply internal rule if * matched. */ issqli = libinjection_xss((const char*)value->data, value->len); if (issqli == 1) { ngx_http_apply_rulematch_v_n(&nx_int__libinject_xss, ctx, req, name, value, zone, 1, 0); } } } int ngx_http_basestr_ruleset_n(ngx_pool_t* pool, ngx_str_t* name, ngx_str_t* value, ngx_array_t* rules, ngx_http_request_t* req, ngx_http_request_ctx_t* ctx, naxsi_match_zone_t zone) { ngx_http_rule_t* r; unsigned int i, ret, z, uri_constraint_ok = 1, rule_matched = 0; ngx_int_t nb_match = 0; ngx_http_custom_rule_location_t* location; naxsi_escape_nullbytes(value); NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX- check check [%V]=[%V] in zone %s", name, value, zone == BODY ? "BODY" : zone == HEADERS ? "HEADERS" : zone == URL ? "URL" : zone == ARGS ? "ARGS" : zone == FILE_EXT ? "FILE_EXT" : zone == RAW_BODY ? "RAW_BODY" : "UNKNOWN"); if (!rules) { ngx_log_debug( NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-no no rules has been defined for naxsi."); return (0); } r = rules->elts; NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-checking %d rules ...", rules->nelts); /* check for overlong/surrogate utf8 encoding */ if (ngx_utf8_check(name) != NULL) { if (ngx_http_apply_rulematch_v_n(&nx_int__bad_utf8, ctx, req, NULL, NULL, zone, 1, 1)) return (0); /* if not whitelisted only */ } else if (ngx_utf8_check(value) != NULL) { if (ngx_http_apply_rulematch_v_n(&nx_int__bad_utf8, ctx, req, NULL, NULL, zone, 1, 0)) return (0); /* if not whitelisted only */ } /* call to libinjection */ ngx_http_libinjection(pool, name, value, ctx, req, zone); for (i = 0; i < rules->nelts && ((!ctx->block || ctx->learning) && !ctx->drop); i++) { /*properly reset counter*/ uri_constraint_ok = 1; rule_matched = 0; NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-RULE %d : START", r[i].rule_id); /* does the rule have a custom location ? custom location means checking * only on a specific argument */ if (name && r[i].br->custom_location) { location = r[i].br->custom_locations->elts; /* ** make a first pass, just in order to check that any ** $URL / $URL_X constraints are validated before checking any other ** parameters. ** Unlike other criterias (wich are treated as 'OR') ** this one must be valid to go forward */ for (z = 0; z < r[i].br->custom_locations->nelts; z++) { if (location[z].specific_url) { /* if matchzone is a regex, ensure it matches (ie. BODY_VAR_X / * ARGS_VAR_X / ..) */ if (r[i].br->rx_mz) { if (ngx_http_naxsi_pcre_wrapper(location[z].target_rx, req->uri.data, req->uri.len) == -1) { uri_constraint_ok = 0; } } /* if it was a static string, ensure it matches (ie. BODY_VAR / * ARGS_VAR / ..) */ if (!r[i].br->rx_mz) { if (req->uri.len != location[z].target.len || strncasecmp((const char*)req->uri.data, (const char*)location[z].target.data, req->uri.len) != 0) { uri_constraint_ok = 0; } } break; } } /* ** if one of the custom location rule specifies an $URL/$URL_X ** and it was mismatched, skip the rule. */ if (uri_constraint_ok == 0) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX URI CONSTRAINT MISMATCH, SKIP"); continue; } /* for each custom location */ for (z = 0; z < r[i].br->custom_locations->nelts; z++) { rule_matched = 0; /* check if zone is correct before checking names cf. issue #120 */ if (!(zone == BODY && location[z].body_var != 0) && !(zone == HEADERS && location[z].headers_var != 0) && !(zone == ARGS && location[z].args_var != 0)) continue; /* if matchzone is a regex, ensure it matches (ie. BODY_VAR_X / * ARGS_VAR_X / ..) */ if (r[i].br->rx_mz && ngx_http_naxsi_pcre_wrapper(location[z].target_rx, name->data, name->len) == -1) continue; /* if it was a static string, ensure it matches (ie. BODY_VAR / ARGS_VAR * / ..) */ if ((!r[i].br->rx_mz) && (name->len != location[z].target.len || strncasecmp((const char*)name->data, (const char*)location[z].target.data, location[z].target.len))) continue; NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-[SPECIFIC] check one rule [%d] iteration %d * %d", r[i].rule_id, i, z); /* match rule against var content, */ ret = ngx_http_process_basic_rule_buffer(value, &(r[i]), &nb_match); if (ret == 1) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-apply rulematch [%V]=[%V] [rule=%d] (match %d times)", name, value, r[i].rule_id, nb_match); rule_matched = 1; ngx_http_apply_rulematch_v_n(&(r[i]), ctx, req, name, value, zone, nb_match, 0); } if (!r[i].br->negative) { /* match rule against var name, */ ret = ngx_http_process_basic_rule_buffer(name, &(r[i]), &nb_match); /* if our rule matched, apply effects (score etc.) */ if (ret == 1) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-apply rulematch[in name] [%V]=[%V] [rule=%d] (match " "%d times)", name, value, r[i].rule_id, nb_match); rule_matched = 1; ngx_http_apply_rulematch_v_n(&(r[i]), ctx, req, name, name, zone, nb_match, 1); } } if (rule_matched == 1) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-[SPECIFIC] Rule %d matched in custom_location, go to next rule", r[i].rule_id); break; } } } /* ** check against the rule if the current zone is matching ** the zone the rule is meant to be check against */ if ((zone == HEADERS && r[i].br->headers) || (zone == URL && r[i].br->url) || (zone == ARGS && r[i].br->args) || (zone == BODY && r[i].br->raw_body) || (zone == BODY && r[i].br->body_rule) || (zone == FILE_EXT && r[i].br->file_ext)) { /* ** If the Rule **specifically** targets name (ie. mz:BODY|NAME), only *check against name */ if (!r[i].br->target_name) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-test rulematch (value) [zone-wide]!1 [%V]=[%V] [rule =%d] " "(%d times)", name, value, r[i].rule_id, nb_match); /* check the rule against the value*/ ret = ngx_http_process_basic_rule_buffer(value, &(r[i]), &nb_match); /*if our rule matched, apply effects (score etc.)*/ if (ret == 1) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-apply rulematch (value) [%V]=[%V] [rule=%d] (%d times)", name, value, r[i].rule_id, nb_match); ngx_http_apply_rulematch_v_n(&(r[i]), ctx, req, name, value, zone, nb_match, 0); } } if (!r[i].br->negative) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-test rulematch [against-name]!1 [%V]=[%V] [rule=%d] (%d times)", name, value, r[i].rule_id, nb_match); /* check the rule against the name*/ ret = ngx_http_process_basic_rule_buffer(name, &(r[i]), &nb_match); /*if our rule matched, apply effects (score etc.)*/ if (ret == 1) { NX_DEBUG(_debug_basestr_ruleset, NGX_LOG_DEBUG_HTTP, req->connection->log, 0, "XX-apply rulematch!1 [%V]=[%V] [rule=%d] (%d times)", name, value, r[i].rule_id, nb_match); ngx_http_apply_rulematch_v_n(&(r[i]), ctx, req, name, value, zone, nb_match, 1); } } } } return (0); } /* ** does : parse body data, a.k.a POST/PUT datas. identifies content-type, ** and, if appropriate, boundary. then parse the stuff if multipart/for.. ** or rely on spliturl if application/x-w.. ** this function sucks ! I don't parse bigger-than-body-size posts that ** are partially stored in files, TODO ;) */ /* ** Parse content-disposition line. */ int nx_content_disposition_parse(unsigned char* str, unsigned char* line_end, unsigned char** fvarn_start, unsigned char** fvarn_end, unsigned char** ffilen_start, unsigned char** ffilen_end, ngx_http_request_t* r) { unsigned char *varn_start = NULL, *varn_end = NULL; unsigned char *filen_start = NULL, *filen_end = NULL; /* we have two cases : ** ---- file upload ** Content-Disposition: form-data; name="somename"; *filename="NetworkManager.conf"\r\n ** Content-Type: application/octet-stream\r\n\r\n ** ** ---- normal post var ** Content-Disposition: form-data; name="lastname"\r\n\r\n ** */ while (str < line_end) { /* rfc allow spaces and tabs inbetween */ while (str < line_end && (*str == ' ' || *str == '\t')) { str++; } if (str < line_end && *str == ';') { str++; } while (str < line_end && (*str == ' ' || *str == '\t')) { str++; } if (str >= line_end) { break; } if (!ngx_strncmp(str, "name=\"", 6)) { /* we already successfully parsed a name, reject that. */ if (varn_end || varn_start) { return (NGX_ERROR); } varn_end = varn_start = str + 6; do { varn_end = (unsigned char*)strnchr((const char*)varn_end, '"', line_end - varn_start); if (!varn_end || (varn_end && *(varn_end - 1) != '\\')) { break; } varn_end++; } while (varn_end && varn_end < line_end); if (!varn_end || !*varn_end) { return (NGX_ERROR); } str = varn_end; if (str < line_end + 1) { str++; } else { return (NGX_ERROR); } *fvarn_start = varn_start; *fvarn_end = varn_end; } else if (!ngx_strncmp(str, "filename=\"", 10)) { /* we already successfully parsed a filename, reject that. */ if (filen_end || filen_start) { return (NGX_ERROR); } filen_end = filen_start = str + 10; do { filen_end = (unsigned char*)strnchr((const char*)filen_end, '"', line_end - filen_start); if (!filen_end) { break; } if (filen_end && *(filen_end - 1) != '\\') { break; } filen_end++; } while (filen_end && filen_end < line_end); if (!filen_end) { return (NGX_ERROR); } str = filen_end; if (str < line_end + 1) { str++; } else { return (NGX_ERROR); } *ffilen_end = filen_end; *ffilen_start = filen_start; } else if (str == line_end - 1) { break; } else { /* gargabe is present ?*/ NX_DEBUG(_debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "extra data in content-disposition ? end:%p, str:%p, diff=%d", line_end, str, line_end - str); return (NGX_ERROR); } } /* tssk tssk */ if (filen_end > line_end || varn_end > line_end) { return (NGX_ERROR); } return (NGX_OK); } int nx_content_type_parse(ngx_http_request_t* r, unsigned char** boundary, unsigned int* boundary_len) { unsigned char* h; unsigned char* end; h = r->headers_in.content_type->value.data + strlen("multipart/form-data;"); end = r->headers_in.content_type->value.data + r->headers_in.content_type->value.len; /* skip potential whitespace/tabs */ while (h < end && *h && (*h == ' ' || *h == '\t')) h++; if (strncmp((const char*)h, "boundary=", 9)) return (NGX_ERROR); h += 9; *boundary_len = end - h; *boundary = h; /* RFC 1867/1341 says 70 char max, I arbitrarily set min to 3 (yes) */ if (*boundary_len > 70 || *boundary_len < 3) return (NGX_ERROR); return (NGX_OK); } void ngx_http_naxsi_multipart_parse(ngx_http_request_ctx_t* ctx, ngx_http_request_t* r, u_char* src, u_int len) { ngx_str_t final_var, final_data; u_char * boundary, *varn_start, *varn_end; u_char * filen_start, *filen_end; u_char * end, *line_end; u_int boundary_len, varn_len, varc_len, idx, nullbytes; ngx_http_naxsi_loc_conf_t* cf; ngx_http_naxsi_main_conf_t* main_cf; cf = ngx_http_get_module_loc_conf(r, ngx_http_naxsi_module); main_cf = ngx_http_get_module_main_conf(r, ngx_http_naxsi_module); /*extract boundary*/ if (nx_content_type_parse(r, (unsigned char**)&boundary, &boundary_len) != NGX_OK) { if (boundary && boundary_len > 1) { NX_DEBUG(_debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-POST boundary : (%s) : %d", boundary, boundary_len); } ngx_http_apply_rulematch_v_n(&nx_int__uncommon_post_boundary, ctx, r, NULL, NULL, BODY, 1, 0); return; } NX_DEBUG(_debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-POST boundary : (%s) : %d", boundary, boundary_len); /* fetch every line starting with boundary */ idx = 0; while (idx < len) { NX_DEBUG(_debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-POST data : (%s)", src + idx); ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "Remaining Len:%d (boundary len:%d)", len - idx, boundary_len); /* if we've reached the last boundary '--' + boundary + '--' + '\r\n'$END */ /* Authorize requests that don't have the leading \r\n */ if (idx + boundary_len + 6 == len || idx + boundary_len + 4 == len) { if (ngx_strncmp(src + idx, "--", 2) || ngx_strncmp(src + idx + 2, boundary, boundary_len) || ngx_strncmp(src + idx + boundary_len + 2, "--", 2)) { /* bad closing boundary ?*/ ngx_http_apply_rulematch_v_n( &nx_int__uncommon_post_boundary, ctx, r, NULL, NULL, BODY, 1, 0); return; } else break; } /* --boundary\r\n : New var */ if ((len - idx < 4 + boundary_len) || src[idx] != '-' || src[idx + 1] != '-' || /* and if it's really followed by a boundary */ ngx_strncmp(src + idx + 2, boundary, boundary_len) || /* and if it's not the last boundary of the buffer */ idx + boundary_len + 2 + 2 >= len || /* and if it's followed by \r\n */ src[idx + boundary_len + 2] != '\r' || src[idx + boundary_len + 3] != '\n') { /* bad boundary */ ngx_http_apply_rulematch_v_n(&nx_int__uncommon_post_boundary, ctx, r, NULL, NULL, BODY, 1, 0); return; } idx += boundary_len + 4; /* we have two cases : ** ---- file upload ** Content-Disposition: form-data; name="somename"; *filename="NetworkManager.conf"\r\n ** Content-Type: application/octet-stream\r\n\r\n ** ** ---- normal post var ** Content-Disposition: form-data; name="lastname"\r\n\r\n ** */ /* 31 = echo -n "content-disposition: form-data;" | wc -c */ if (ngx_strncasecmp(src + idx, (u_char*)"content-disposition: form-data;", 31)) { ngx_log_debug( NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "Unknown content-type: [%s]", src + idx); if (ngx_http_apply_rulematch_v_n( &nx_int__uncommon_post_format, ctx, r, NULL, NULL, BODY, 1, 0)) { naxsi_error_fatal(ctx, r, "POST data : unknown content-disposition"); } return; } idx += 31; line_end = (u_char*)ngx_strchr(src + idx, '\n'); if (!line_end) { if (ngx_http_apply_rulematch_v_n( &nx_int__uncommon_post_format, ctx, r, NULL, NULL, BODY, 1, 0)) { naxsi_error_fatal(ctx, r, "POST data : malformed boundary line"); } return; } /* Parse content-disposition, extract name / filename */ varn_start = varn_end = filen_start = filen_end = NULL; if (nx_content_disposition_parse( src + idx, line_end, &varn_start, &varn_end, &filen_start, &filen_end, r) != NGX_OK) { ngx_http_apply_rulematch_v_n(&nx_int__uncommon_post_format, ctx, r, NULL, NULL, BODY, 1, 0); return; } /* var name is mandatory */ if (!varn_start || !varn_end || varn_end <= varn_start) { if (ngx_http_apply_rulematch_v_n( &nx_int__uncommon_post_format, ctx, r, NULL, NULL, BODY, 1, 0)) { naxsi_error_fatal(ctx, r, "POST data : no 'name' in POST var"); } return; } varn_len = varn_end - varn_start; /* If there is a filename, it is followed by a "content-type" line, skip it */ if (filen_start && filen_end) { line_end = (u_char*)ngx_strchr(line_end + 1, '\n'); if (!line_end) { if (ngx_http_apply_rulematch_v_n( &nx_int__uncommon_post_format, ctx, r, NULL, NULL, BODY, 1, 0)) { naxsi_error_fatal(ctx, r, "POST data : malformed filename (no content-type ?)"); } return; } } /* ** now idx point to the end of the ** content-disposition: form-data; filename="" name="" */ idx += (u_char*)line_end - (src + idx) + 1; if (src[idx] != '\r' || src[idx + 1] != '\n') { if (ngx_http_apply_rulematch_v_n( &nx_int__uncommon_post_format, ctx, r, NULL, NULL, BODY, 1, 0)) { naxsi_error_fatal(ctx, r, "POST data : malformed content-disposition line"); } return; } idx += 2; /* seek the end of the data */ end = NULL; while (idx < len) { end = (u_char*)sstrfaststr(src + idx, len - idx, "\r\n--", strlen("\r\n--")); /* file data can contain \x0 */ while (!end) { idx += strlen((const char*)src + idx); if (idx < len - 2) { idx++; end = (u_char*)sstrfaststr(src + idx, len - idx, "\r\n--", strlen("\r\n--")); } else { break; } } if (!end || ngx_strncmp(end + 4, boundary, boundary_len)) { if (ngx_http_apply_rulematch_v_n( &nx_int__uncommon_post_format, ctx, r, NULL, NULL, BODY, 1, 0)) { naxsi_error_fatal(ctx, r, "POST data : malformed content-disposition line"); } return; } if (!ngx_strncmp(end + 4, boundary, boundary_len)) { break; } else { idx += ((u_char*)end - (src + idx)) + 1; end = NULL; } } if (!end) { naxsi_error_fatal(ctx, r, "POST data : malformed line"); return; } if (filen_start) { final_var.data = (unsigned char*)varn_start; final_var.len = varn_len; final_data.data = (unsigned char*)filen_start; final_data.len = filen_end - filen_start; nullbytes = naxsi_unescape(&final_var); if (nullbytes > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, r, &final_var, &final_data, BODY, 1, 1); } nullbytes = naxsi_unescape(&final_data); if (nullbytes > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, r, &final_var, &final_data, BODY, 1, 0); } NX_DEBUG(_debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[POST] checking filename [%V] = [%V]", &final_var, &final_data); /* here we got val name + val content !*/ if (cf->body_rules) { ngx_http_basestr_ruleset_n( r->pool, &final_var, &final_data, cf->body_rules, r, ctx, FILE_EXT); } else { NX_DEBUG(_debug_post_heavy, /* here we got val name + val content !*/ NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[POST] No local body rules"); } if (main_cf->body_rules) { ngx_http_basestr_ruleset_n( r->pool, &final_var, &final_data, main_cf->body_rules, r, ctx, FILE_EXT); } else { NX_DEBUG(_debug_post_heavy, /* here we got val name + val content !*/ NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[POST] No main body rules"); } idx += (u_char*)end - (src + idx); } else if (varn_start) { varc_len = (u_char*)end - (src + idx); final_var.data = (unsigned char*)varn_start; final_var.len = varn_len; final_data.data = src + idx; final_data.len = varc_len; nullbytes = naxsi_unescape(&final_var); if (nullbytes > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, r, &final_var, &final_data, BODY, 1, 1); } nullbytes = naxsi_unescape(&final_data); if (nullbytes > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, r, &final_var, &final_data, BODY, 1, 0); } NX_DEBUG(_debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[POST] [%V]=[%V]", &final_var, &final_data); /* here we got val name + val content !*/ if (cf->body_rules) { ngx_http_basestr_ruleset_n(r->pool, &final_var, &final_data, cf->body_rules, r, ctx, BODY); } else { NX_DEBUG(_debug_post_heavy, /* here we got val name + val content !*/ NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "No local body rules ?!"); } if (main_cf->body_rules) { ngx_http_basestr_ruleset_n( r->pool, &final_var, &final_data, main_cf->body_rules, r, ctx, BODY); } else { NX_DEBUG(_debug_post_heavy, /* here we got val name + val content !*/ NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "No main body rules ?!"); } idx += (u_char*)end - (src + idx); } else { ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "(multipart) : "); } if (!ngx_strncmp(end, "\r\n", 2)) idx += 2; } } void ngx_http_naxsi_body_parse(ngx_http_request_ctx_t* ctx, ngx_http_request_t* r, ngx_http_naxsi_loc_conf_t* cf, ngx_http_naxsi_main_conf_t* main_cf) { u_char* src; ngx_str_t tmp; ngx_chain_t* bb; u_char* full_body; u_int full_body_len; u_char* content_type_str; NX_DEBUG(_debug_body_parse, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-BODY PARSE"); if (!r->request_body->bufs) { ngx_http_apply_rulematch_v_n(&nx_int__empty_post_body, ctx, r, NULL, NULL, BODY, 1, 0); return; } if (!r->headers_in.content_type) { NX_DEBUG(_debug_body_parse, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-No content type .."); if (ngx_http_apply_rulematch_v_n( &nx_int__uncommon_content_type, ctx, r, NULL, NULL, BODY, 1, 0)) return; /* if not whitelisted only */ content_type_str = (u_char*)"application/octet-stream"; } else content_type_str = r->headers_in.content_type->value.data; if (r->request_body->temp_file) { ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "naxsi: POST REQUEST to temp_file, partially parsed."); if (ngx_http_apply_rulematch_v_n(&nx_int__big_request, ctx, r, NULL, NULL, BODY, 1, 0)) return; /* if not whitelisted only */ } NX_DEBUG(_debug_body_parse, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-VALID BODY"); /* request body in single buffer */ if (r->request_body->bufs->next == NULL) { full_body_len = (u_int)(r->request_body->bufs->buf->last - r->request_body->bufs->buf->pos); full_body = ngx_pcalloc(r->pool, (u_int)(full_body_len + 1)); if (!full_body) { naxsi_error_fatal(ctx, r, "failed alloc of %d", full_body_len + 1); return; } memcpy(full_body, r->request_body->bufs->buf->pos, full_body_len); } /* request body in chain */ else { NX_DEBUG(_debug_body_parse, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[POST] REQUEST BODY IN CHAIN !"); for (full_body_len = 0, bb = r->request_body->bufs; bb; bb = bb->next) full_body_len += (bb->buf->last - bb->buf->pos); full_body = ngx_pcalloc(r->pool, full_body_len + 1); if (!full_body) { naxsi_error_fatal(ctx, r, "failed alloc of %d", full_body_len + 1); return; } src = full_body; for (bb = r->request_body->bufs; bb; bb = bb->next) full_body = ngx_cpymem(full_body, bb->buf->pos, bb->buf->last - bb->buf->pos); full_body = src; NX_DEBUG(_debug_body_parse, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[POST] REQUEST BODY IN CHAIN [%s] (len=%d)", full_body, full_body_len); } NX_DEBUG(_debug_body_parse, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "content-len header (%d) mismatch actual len (%d) ??", r->headers_in.content_length_n, full_body_len); /* File probably got buffered. */ if (r->headers_in.content_length_n != full_body_len) { if (ngx_http_apply_rulematch_v_n(&nx_int__big_request, ctx, r, NULL, NULL, BODY, 1, 0)) { ngx_pfree(r->pool, full_body); return; /* if not whitelisted only */ } } /* x-www-form-urlencoded POSTs */ /* 33 = echo -n "application/x-www-form-urlencoded" | wc -c */ if (!ngx_strncasecmp(content_type_str, (u_char*)"application/x-www-form-urlencoded", 33)) { NX_DEBUG( _debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-application/x-www.."); tmp.len = full_body_len; tmp.data = full_body; NX_DEBUG( _debug_post_heavy, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-POST DATA [%V]", &tmp); if (ngx_http_spliturl_ruleset(r->pool, &tmp, cf->body_rules, main_cf->body_rules, r, ctx, BODY)) ngx_http_apply_rulematch_v_n(&nx_int__uncommon_url, ctx, r, NULL, NULL, BODY, 1, 0); } /* 19 = echo -n "multipart/form-data" | wc -c */ else if (!ngx_strncasecmp(content_type_str, (u_char*)"multipart/form-data", 19)) { ngx_http_naxsi_multipart_parse(ctx, r, full_body, full_body_len); } /* 16 = echo -n "application/json" | wc -c */ else if (!ngx_strncasecmp(content_type_str, (u_char*)"application/json", 16)) { ngx_http_naxsi_json_parse(ctx, r, full_body, full_body_len); } /* 24 = echo -n "application/vnd.api+json" | wc -c */ else if (!ngx_strncasecmp(content_type_str, (u_char*)"application/vnd.api+json", 24)) { ngx_http_naxsi_json_parse(ctx, r, full_body, full_body_len); } /* 24 = echo -n "application/reports+json" | wc -c */ else if (!ngx_strncasecmp(content_type_str, (u_char*)"application/reports+json", 24)) { ngx_http_naxsi_json_parse(ctx, r, full_body, full_body_len); } /* 22 = echo -n "application/csp-report" | wc -c */ else if (!ngx_strncasecmp(content_type_str, (u_char*)"application/csp-report", 22)) { ngx_http_naxsi_json_parse(ctx, r, full_body, full_body_len); } else { ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[POST] Unknown content-type"); /* ** Only attempt to process "raw" body if id:nx_int__uncommon_content_type was ** whitelisted. Else, it should be blocking and stop processing here. */ if (!ngx_http_apply_rulematch_v_n( &nx_int__uncommon_content_type, ctx, r, NULL, NULL, BODY, 1, 0)) { if ((!ctx->block || ctx->learning) && !ctx->drop) { ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "After uncommon content-type"); ngx_http_naxsi_rawbody_parse(ctx, r, full_body, full_body_len); } } } ngx_pfree(r->pool, full_body); } void ngx_http_naxsi_uri_parse(ngx_http_naxsi_main_conf_t* main_cf, ngx_http_naxsi_loc_conf_t* cf, ngx_http_request_ctx_t* ctx, ngx_http_request_t* r) { ngx_str_t tmp, name; if (!r->uri.len) return; if ((ctx->block && !ctx->learning) || ctx->drop) return; if (!main_cf->generic_rules && !cf->generic_rules) { tmp.data = NULL; tmp.len = 0; ngx_http_apply_rulematch_v_n(&nx_int__no_rules, ctx, r, &tmp, &tmp, URL, 1, 0); return; } tmp.len = r->uri.len; tmp.data = ngx_pcalloc(r->pool, r->uri.len + 1); if (!tmp.data) { naxsi_error_fatal(ctx, r, "failed alloc of %d", r->uri.len + 1); return; } memcpy(tmp.data, r->uri.data, r->uri.len); if (naxsi_escape_nullbytes(&tmp) > 0) { ngx_str_t tmp_name, tmp_val; tmp_name.data = tmp_val.data = NULL; tmp_name.len = tmp_val.len = 0; ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, r, &tmp_name, &tmp_val, URL, 1, 0); } name.data = NULL; name.len = 0; if (cf->generic_rules) ngx_http_basestr_ruleset_n(r->pool, &name, &tmp, cf->generic_rules, r, ctx, URL); if (main_cf->generic_rules) ngx_http_basestr_ruleset_n(r->pool, &name, &tmp, main_cf->generic_rules, r, ctx, URL); ngx_pfree(r->pool, tmp.data); } void ngx_http_naxsi_args_parse(ngx_http_naxsi_main_conf_t* main_cf, ngx_http_naxsi_loc_conf_t* cf, ngx_http_request_ctx_t* ctx, ngx_http_request_t* r) { ngx_str_t tmp; if ((ctx->block && !ctx->learning) || ctx->drop) return; if (!r->args.len) return; if (!cf->get_rules && !main_cf->get_rules) return; tmp.len = r->args.len; tmp.data = ngx_pcalloc(r->pool, r->args.len + 1); if (!tmp.data) { naxsi_error_fatal(ctx, r, "failed alloc"); return; } memcpy(tmp.data, r->args.data, r->args.len); if (ngx_http_spliturl_ruleset(r->pool, &tmp, cf->get_rules, main_cf->get_rules, r, ctx, ARGS)) { naxsi_error_fatal(ctx, r, "spliturl error : malformed url, possible attack"); return; } ngx_pfree(r->pool, tmp.data); } void ngx_http_naxsi_headers_parse(ngx_http_naxsi_main_conf_t* main_cf, ngx_http_naxsi_loc_conf_t* cf, ngx_http_request_ctx_t* ctx, ngx_http_request_t* r) { ngx_list_part_t* part; ngx_table_elt_t* h; unsigned int i; ngx_str_t lowcase_header; if (!cf->header_rules && !main_cf->header_rules) return; // this check may be removed, as it shouldn't be needed anymore ! if ((ctx->block && !ctx->learning) || ctx->drop) return; part = &r->headers_in.headers.part; h = part->elts; // this check may be removed, as it shouldn't be needed anymore ! for (i = 0; ((!ctx->block || ctx->learning) && !ctx->block); i++) { if (i >= part->nelts) { if (part->next == NULL) break; part = part->next; h = part->elts; i = 0; } lowcase_header.data = h[i].lowcase_key; lowcase_header.len = h[i].key.len; if (naxsi_escape_nullbytes(&lowcase_header) > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, r, &h[i].key, &h[i].value, HEADERS, 1, 1); } if (naxsi_escape_nullbytes(&h[i].value) > 0) { ngx_http_apply_rulematch_v_n( &nx_int__uncommon_hex_encoding, ctx, r, &h[i].key, &h[i].value, HEADERS, 1, 0); } if (cf->header_rules) { ngx_http_basestr_ruleset_n( r->pool, &lowcase_header, &(h[i].value), cf->header_rules, r, ctx, HEADERS); } if (main_cf->header_rules) { ngx_http_basestr_ruleset_n( r->pool, &lowcase_header, &(h[i].value), main_cf->header_rules, r, ctx, HEADERS); } } if (naxsi_is_illegal_host_name(&r->headers_in.server) > 0) { ngx_http_apply_rulematch_v_n( &nx_int__illegal_host_header, ctx, r, NULL, &r->headers_in.server, HEADERS, 1, 0); } return; } /* ** does : this is a 'main' function, all the stuff goes from here. ** to make it short, it does the following : ** - if we got header rules, apply header_rules on each. ** - apply generic_rules on url decoded URI. ** - if we got get_rules and get args, apply get_rules varname/value couple. ** - if we are in a POST/PUT request and we got body_rules, apply rules :) */ void ngx_http_naxsi_data_parse(ngx_http_request_ctx_t* ctx, ngx_http_request_t* r) { ngx_http_naxsi_loc_conf_t* cf; ngx_http_naxsi_main_conf_t* main_cf; cf = ngx_http_get_module_loc_conf(r, ngx_http_naxsi_module); main_cf = ngx_http_get_module_main_conf(r, ngx_http_naxsi_module); if (!cf || !ctx) { ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "naxsi: unable to parse data."); return; } /* process rules only if request is not already blocked or if the learning mode is enabled */ ngx_http_naxsi_headers_parse(main_cf, cf, ctx, r); /* check uri */ ngx_http_naxsi_uri_parse(main_cf, cf, ctx, r); /* check args */ ngx_http_naxsi_args_parse(main_cf, cf, ctx, r); /* check method */ if ((r->method == NGX_HTTP_PATCH || r->method == NGX_HTTP_POST || r->method == NGX_HTTP_PUT) && /* presence of body rules (PATCH/POST/PUT rules) */ (cf->body_rules || main_cf->body_rules) && /* and the presence of data to parse */ r->request_body && ((!ctx->block || ctx->learning) && !ctx->drop)) ngx_http_naxsi_body_parse(ctx, r, cf, main_cf); ngx_http_naxsi_update_current_ctx_status(ctx, cf, r); } void ngx_http_naxsi_update_current_ctx_status(ngx_http_request_ctx_t* ctx, ngx_http_naxsi_loc_conf_t* cf, ngx_http_request_t* r) { unsigned int i, z; unsigned int matched; ngx_http_check_rule_t* cr; ngx_http_special_score_t* sc; NX_DEBUG(_debug_custom_score, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-custom check rules"); if (!ctx->ignore && (cf->ignore_ips || cf->ignore_cidrs)) { ngx_str_t* ip = &r->connection->addr_text; NX_DEBUG(_debug_whitelist_ignore, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX- lookup ignore client ip: %V", ip); ctx->ignore = naxsi_can_ignore_ip(ip, cf) || naxsi_can_ignore_cidr(ip, cf); } if (cf->check_rules && ctx->special_scores) { NX_DEBUG(_debug_custom_score, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-we have custom check rules and CTX got special score :)"); cr = cf->check_rules->elts; sc = ctx->special_scores->elts; for (z = 0; z < ctx->special_scores->nelts; z++) for (i = 0; i < cf->check_rules->nelts; i++) { NX_DEBUG(_debug_custom_score, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX- rule says :(%s:%d) vs current context:(%s:%d) (flag=%d)", cr[i].sc_tag.data, cr[i].sc_score, sc[z].sc_tag->data, sc[z].sc_score, cr[i].cmp); if (!ngx_strcmp(sc[z].sc_tag->data, cr[i].sc_tag.data)) { NX_DEBUG(_debug_custom_score, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX- rule says :(%s:%d) vs current context:(%s:%d) (flag=%d)", cr[i].sc_tag.data, cr[i].sc_score, sc[z].sc_tag->data, sc[z].sc_score, cr[i].cmp); matched = 0; // huglier than your mom :) switch (cr[i].cmp) { case SUP: matched = sc[z].sc_score > cr[i].sc_score ? 1 : 0; break; case SUP_OR_EQUAL: matched = sc[z].sc_score >= cr[i].sc_score ? 1 : 0; break; case INF: matched = sc[z].sc_score < cr[i].sc_score ? 1 : 0; break; case INF_OR_EQUAL: matched = sc[z].sc_score <= cr[i].sc_score ? 1 : 0; break; } if (matched) { NX_DEBUG(_debug_custom_score, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX- custom score rule triggered .."); if (cr[i].block) { ctx->block = 1; } if (cr[i].drop) { ctx->drop = 1; } if (cr[i].allow) { ctx->allow = 1; } if (cr[i].log) { ctx->log = 1; } } } } } if (ctx->ignore) { ctx->block = 0; ctx->drop = 0; } } /* ** This function is called when the body is read. ** Will set-up flags to tell that parsing can be done, ** and then run the core phases again ** (WARNING: check backward compatibility of count-- ** with older version of nginx 0.7.x) */ void ngx_http_naxsi_payload_handler(ngx_http_request_t* r) { ngx_http_request_ctx_t* ctx; ctx = ngx_http_get_module_ctx(r, ngx_http_naxsi_module); ctx->ready = 1; r->count--; NX_DEBUG(_debug_payload_handler, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-dummy PAYLOAD HANDLER !"); if (ctx->wait_for_body) { NX_DEBUG(_debug_payload_handler, NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "XX-dummy : AFTER NGX_AGAIN"); ctx->wait_for_body = 0; ngx_http_core_run_phases(r); } }