Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,36 @@ modsecurity_use_error_log

Turns on or off ModSecurity error log functionality.

# Variables

This module exposes the following variables that can be used in `log_format` or anywhere else nginx variables are valid.

modsecurity_intervention
-------------------------
**value:** *`1` if ModSecurity triggered a disruptive intervention
(deny, redirect, etc.) on the request, `0` otherwise*

modsecurity_triggered_rules
----------------------------
**value:** *comma-separated list of matched rule IDs (e.g. `941100,949110`),
or `-` when no rule matched*

```nginx
log_format modsec '$remote_addr [$time_local] "$request" $status '
'intervention=$modsecurity_intervention '
'rules=$modsecurity_triggered_rules';

server {
listen 8080;
modsecurity on;
modsecurity_rules_file /etc/modsecurity.d/modsecurity.conf;
access_log logs/modsec-access.log modsec;
location / {
...
}
}
```

# Contributing

As an open source project we invite (and encourage) anyone from the community to contribute to our project. This may take the form of: new
Expand Down
111 changes: 110 additions & 1 deletion src/ngx_http_modsecurity_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
#endif

static ngx_int_t ngx_http_modsecurity_init(ngx_conf_t *cf);
static ngx_int_t ngx_http_modsecurity_add_variables(ngx_conf_t *cf);
static ngx_int_t ngx_http_modsecurity_intervention_variable(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data);
static ngx_int_t ngx_http_modsecurity_triggered_rules_variable(
ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data);
static void *ngx_http_modsecurity_create_main_conf(ngx_conf_t *cf);
static char *ngx_http_modsecurity_init_main_conf(ngx_conf_t *cf, void *conf);
static void *ngx_http_modsecurity_create_conf(ngx_conf_t *cf);
Expand All @@ -38,6 +43,18 @@
static void ngx_http_modsecurity_cleanup_rules(void *data);


static ngx_http_variable_t ngx_http_modsecurity_vars[] = {

{ ngx_string("modsecurity_intervention"), NULL,
ngx_http_modsecurity_intervention_variable, 0, 0, 0 },

{ ngx_string("modsecurity_triggered_rules"), NULL,
ngx_http_modsecurity_triggered_rules_variable, 0, 0, 0 },

ngx_http_null_variable
};


/*
* PCRE malloc/free workaround, based on
* https://github.com/openresty/lua-nginx-module/blob/master/src/ngx_http_lua_pcrefix.c
Expand Down Expand Up @@ -161,6 +178,8 @@
return 0;
}

ctx->intervention_triggered = 1;

mcf = ngx_http_get_module_loc_conf(r, ngx_http_modsecurity_module);
if (mcf == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
Expand Down Expand Up @@ -534,7 +553,7 @@


static ngx_http_module_t ngx_http_modsecurity_ctx = {
NULL, /* preconfiguration */
ngx_http_modsecurity_add_variables, /* preconfiguration */
ngx_http_modsecurity_init, /* postconfiguration */

ngx_http_modsecurity_create_main_conf, /* create main configuration */
Expand Down Expand Up @@ -564,6 +583,96 @@
};


static ngx_int_t
ngx_http_modsecurity_intervention_variable(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data)
{
ngx_http_modsecurity_ctx_t *ctx;
static u_char zero = '0';
static u_char one = '1';

ctx = ngx_http_modsecurity_get_module_ctx(r);
if (ctx == NULL) {
v->not_found = 1;
return NGX_OK;
}

v->data = ctx->intervention_triggered ? &one : &zero;
v->len = 1;
v->valid = 1;
v->no_cacheable = 0;
v->not_found = 0;

return NGX_OK;
}


static ngx_int_t
ngx_http_modsecurity_triggered_rules_variable(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data)
{
ngx_http_modsecurity_ctx_t *ctx;
size_t count, i, cap;

Check warning on line 615 in src/ngx_http_modsecurity_module.c

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define each identifier in a dedicated statement.

See more on https://sonarcloud.io/project/issues?id=owasp-modsecurity_ModSecurity-nginx&issues=AZ20Oowe34gj-nNeQT9e&open=AZ20Oowe34gj-nNeQT9e&pullRequest=374
u_char *buf, *p, *end;

ctx = ngx_http_modsecurity_get_module_ctx(r);
if (ctx == NULL || ctx->modsec_transaction == NULL) {
v->not_found = 1;
return NGX_OK;
}

count = msc_get_matched_rules_count(ctx->modsec_transaction);
if (count == 0) {
v->not_found = 1;
return NGX_OK;
}

/* NGX_INT64_LEN digits per id + one comma separator per id */
cap = count * (NGX_INT64_LEN + 1);
buf = ngx_pnalloc(r->pool, cap);
if (buf == NULL) {
return NGX_ERROR;
}

p = buf;
end = buf + cap;
for (i = 0; i < count; i++) {
int64_t id = msc_get_matched_rule_id(ctx->modsec_transaction, i);
if (i > 0) {
*p++ = ',';
}
p = ngx_snprintf(p, end - p, "%L", id);
}

v->data = buf;
v->len = p - buf;
v->valid = 1;
v->no_cacheable = 0;
v->not_found = 0;

return NGX_OK;
}


static ngx_int_t
ngx_http_modsecurity_add_variables(ngx_conf_t *cf)
{
ngx_http_variable_t *var, *v;

for (v = ngx_http_modsecurity_vars; v->name.len; v++) {
var = ngx_http_add_variable(cf, &v->name, v->flags);
if (var == NULL) {
return NGX_ERROR;
}

var->get_handler = v->get_handler;
var->data = v->data;
}

return NGX_OK;
}


static ngx_int_t
ngx_http_modsecurity_init(ngx_conf_t *cf)
{
Expand Down
172 changes: 172 additions & 0 deletions tests/modsecurity-log-vars.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/perl

# Tests for $modsecurity_intervention and $modsecurity_triggered_rules.

###############################################################################

use warnings;
use strict;

use Test::More;

BEGIN { use FindBin; chdir($FindBin::Bin); }

use lib 'lib';
use Test::Nginx;

###############################################################################

select STDERR; $| = 1;
select STDOUT; $| = 1;

my $t = Test::Nginx->new()->has(qw/http/);

$t->write_file_expand('nginx.conf', <<'EOF');

%%TEST_GLOBALS%%

daemon off;

events {
}

http {
%%TEST_GLOBALS_HTTP%%

log_format modsec '$request_uri|i=$modsecurity_intervention|r=$modsecurity_triggered_rules';
access_log %%TESTDIR%%/access.log modsec;

server {
listen 127.0.0.1:8080;
server_name localhost;

location /pass {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq never" "id:100,phase:2,log,pass"
';
}

location /match-logonly {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq hit" "id:200,phase:2,log,pass"
';
}

location /multi {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq hit" "id:301,phase:2,log,pass"
SecRule ARGS "@streq hit" "id:302,phase:2,log,pass"
SecRule ARGS "@streq hit" "id:303,phase:2,log,pass"
';
}

location /mixed-logging {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq hit" "id:701,phase:2,nolog,pass"
SecRule ARGS "@streq hit" "id:702,phase:2,noauditlog,pass"
SecRule ARGS "@streq hit" "id:703,phase:2,log,pass"
';
}

location /allow {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq skip" "id:800,phase:1,log,allow"
SecRule ARGS "@streq skip" "id:801,phase:1,log,deny,status:403"
';
}

location /block {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq go" "id:400,phase:1,log,deny,status:403"
';
}

location /redirect {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq go" "id:500,phase:1,log,status:302,redirect:http://example.com/"
';
}

location /block-phase3 {
modsecurity on;
modsecurity_rules '
SecRuleEngine On
SecRule ARGS "@streq go" "id:600,phase:3,log,deny,status:403"
';
}
}
}
EOF

$t->write_file("/block-phase3", "body");
$t->run();
$t->plan(16);

###############################################################################

# No rule matches: intervention=0, no rule list (nginx prints '-' for missing var).
http_get('/pass?arg=x');
like(log_line($t, '/pass'), qr/\|i=0\|/, 'pass: intervention=0');
like(log_line($t, '/pass'), qr/\|r=-$/, 'pass: no triggered rules');

# Rule matches but non-disruptive: intervention=0, rule id listed.
http_get('/match-logonly?arg=hit');
like(log_line($t, '/match-logonly'), qr/\|i=0\|/, 'log-only: intervention=0');
like(log_line($t, '/match-logonly'), qr/\|r=200$/, 'log-only: rule id captured');

# Multiple rules all matching: intervention=0, every id listed.
http_get('/multi?arg=hit');
like(log_line($t, '/multi'), qr/\|i=0\|/, 'multi: intervention=0');
like(log_line($t, '/multi'), qr/\|r=301,302,303$/, 'multi: all rule ids listed in order');

# Three rules with different logging actions (nolog / noauditlog / log).
http_get('/mixed-logging?arg=hit');
like(log_line($t, '/mixed-logging'), qr/\|i=0\|/, 'mixed-logging: intervention=0');
like(log_line($t, '/mixed-logging'), qr/\|r=703$/, 'mixed-logging: only the log-action rule is captured (nolog/noauditlog both clear m_saveMessage)');

# allow action: short-circuits rule evaluation but is NOT treated as an intervention.
http_get('/allow?arg=skip');
like(log_line($t, '/allow'), qr/\|i=0\|/, 'allow: intervention=0');
like(log_line($t, '/allow'), qr/\|r=800$/, 'allow: only the allow rule captured; subsequent deny short-circuited');

# Deny intervention (phase 1): intervention=1, rule id listed.
http_get('/block?arg=go');
like(log_line($t, '/block'), qr/\|i=1\|/, 'block: intervention=1');
like(log_line($t, '/block'), qr/\|r=400$/, 'block: rule id captured');

# Redirect intervention: intervention=1, rule id listed.
http_get('/redirect?arg=go');
like(log_line($t, '/redirect'), qr/\|i=1\|/, 'redirect: intervention=1');
like(log_line($t, '/redirect'), qr/\|r=500$/, 'redirect: rule id captured');

# Intervention fired from a post-access phase.
http_get('/block-phase3?arg=go');
like(log_line($t, '/block-phase3'), qr/\|i=1\|/, 'phase3 block: intervention=1');
like(log_line($t, '/block-phase3'), qr/\|r=600$/, 'phase3 block: rule id captured');

###############################################################################

sub log_line {
my ($t, $uri_prefix) = @_;
my $path = $t->testdir() . '/access.log';
open my $fh, '<', $path or return "open: $!";
my @matches = grep { /^\Q$uri_prefix\E/ } <$fh>;
close $fh;
return $matches[-1] // '';
}

###############################################################################
Loading