Files

2710 lines
90 KiB
C

// SPDX-FileCopyrightText: 2019-2024, Giovanni Dante Grazioli <wargio@libero.it>
// SPDX-FileCopyrightText: 2016-2019, Thibault 'bui' Koechlin <tko@nbs-system.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <ngx_config.h>
#include <naxsi.h>
#include <naxsi_macros.h>
#include <naxsi_net.h>
/* 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 = &empty;
}
if (!value) {
value = &empty;
}
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
** <DATA>
** ---- normal post var
** Content-Disposition: form-data; name="lastname"\r\n\r\n
** <DATA>
*/
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
** <DATA>
** ---- normal post var
** Content-Disposition: form-data; name="lastname"\r\n\r\n
** <DATA>
*/
/* 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);
}
}