From 59629a6aff92fb6bc16309d298f242df89edb298 Mon Sep 17 00:00:00 2001 From: brectanus Date: Fri, 23 May 2008 16:30:18 +0000 Subject: [PATCH] Add/update regression tests. --- .../regression/action/00-disruptive-actions.t | 48 ++--- .../regression/config/10-request-directives.t | 189 ++++++++++++++++-- apache2/t/run-regression-tests.pl.in | 99 +++++++-- 3 files changed, 280 insertions(+), 56 deletions(-) diff --git a/apache2/t/regression/action/00-disruptive-actions.t b/apache2/t/regression/action/00-disruptive-actions.t index a44d3721..6cae4a3f 100644 --- a/apache2/t/regression/action/00-disruptive-actions.t +++ b/apache2/t/regression/action/00-disruptive-actions.t @@ -1,6 +1,6 @@ ### Pass { - type => "rule", + type => "action", comment => "pass action in phase:1", conf => qq( SecRuleEngine On @@ -18,7 +18,7 @@ ), }, { - type => "rule", + type => "action", comment => "pass action in phase:2", conf => qq( SecRuleEngine On @@ -36,7 +36,7 @@ ), }, { - type => "rule", + type => "action", comment => "pass action in phase:3", conf => qq( SecRuleEngine On @@ -54,7 +54,7 @@ ), }, { - type => "rule", + type => "action", comment => "pass action in phase:4", conf => qq( SecRuleEngine On @@ -74,7 +74,7 @@ ### Allow { - type => "rule", + type => "action", comment => "allow action in phase:1", conf => qq( SecRuleEngine On @@ -92,7 +92,7 @@ ), }, { - type => "rule", + type => "action", comment => "allow action in phase:2", conf => qq( SecRuleEngine On @@ -110,7 +110,7 @@ ), }, { - type => "rule", + type => "action", comment => "allow action in phase:3", conf => qq( SecRuleEngine On @@ -128,7 +128,7 @@ ), }, { - type => "rule", + type => "action", comment => "allow action in phase:4", conf => qq( SecRuleEngine On @@ -148,7 +148,7 @@ ### Deny { - type => "rule", + type => "action", comment => "deny action in phase:1", conf => qq( SecRuleEngine On @@ -165,7 +165,7 @@ ), }, { - type => "rule", + type => "action", comment => "deny action in phase:2", conf => qq( SecRuleEngine On @@ -182,7 +182,7 @@ ), }, { - type => "rule", + type => "action", comment => "deny action in phase:3", conf => qq( SecRuleEngine On @@ -199,7 +199,7 @@ ), }, { - type => "rule", + type => "action", comment => "deny action in phase:4", conf => qq( SecRuleEngine On @@ -218,7 +218,7 @@ ### Drop { - type => "rule", + type => "action", comment => "drop action in phase:1", conf => qq( SecRuleEngine On @@ -235,7 +235,7 @@ ), }, { - type => "rule", + type => "action", comment => "drop action in phase:2", conf => qq( SecRuleEngine On @@ -252,7 +252,7 @@ ), }, { - type => "rule", + type => "action", comment => "drop action in phase:3", conf => qq( SecRuleEngine On @@ -269,7 +269,7 @@ ), }, { - type => "rule", + type => "action", comment => "drop action in phase:4", conf => qq( SecRuleEngine On @@ -288,7 +288,7 @@ ### Redirect { - type => "rule", + type => "action", comment => "redirect action in phase:1 (get)", conf => qq( SecRuleEngine On @@ -305,7 +305,7 @@ ), }, { - type => "rule", + type => "action", comment => "redirect action in phase:2 (get)", conf => qq( SecRuleEngine On @@ -322,7 +322,7 @@ ), }, { - type => "rule", + type => "action", comment => "redirect action in phase:3 (get)", conf => qq( SecRuleEngine On @@ -339,7 +339,7 @@ ), }, { - type => "rule", + type => "action", comment => "redirect action in phase:4 (get)", conf => qq( SecRuleEngine On @@ -358,7 +358,7 @@ ### Proxy { - type => "rule", + type => "action", comment => "proxy action in phase:1 (get)", conf => qq( SecRuleEngine On @@ -375,7 +375,7 @@ ), }, { - type => "rule", + type => "action", comment => "proxy action in phase:2 (get)", conf => qq( SecRuleEngine On @@ -392,7 +392,7 @@ ), }, { - type => "rule", + type => "action", comment => "proxy action in phase:3 (get)", conf => qq( SecRuleEngine On @@ -409,7 +409,7 @@ ), }, { - type => "rule", + type => "action", comment => "proxy action in phase:4 (get)", conf => qq( SecRuleEngine On diff --git a/apache2/t/regression/config/10-request-directives.t b/apache2/t/regression/config/10-request-directives.t index ac27f371..b951676d 100644 --- a/apache2/t/regression/config/10-request-directives.t +++ b/apache2/t/regression/config/10-request-directives.t @@ -10,7 +10,7 @@ SecRule ARGS:b "@streq 2" ), match_log => { - error => [ qr/Access denied with code 403 \(phase 1\). String match "2" at ARGS:b./, 1 ], + error => [ qr/Access denied with code 403 \(phase 1\)\. String match "2" at ARGS:b\./, 1 ], }, match_response => { status => qr/^403$/, @@ -48,7 +48,7 @@ SecRule ARGS:b "@streq 2" ), match_log => { - error => [ qr/Access denied with code 403 \(phase 2\). String match "2" at ARGS:b./, 1 ], + error => [ qr/Access denied with code 403 \(phase 2\)\. String match "2" at ARGS:b\./, 1 ], }, match_response => { status => qr/^403$/, @@ -91,17 +91,12 @@ comment => "SecRequestBodyAccess (pos)", conf => qq( SecRuleEngine On - SecAuditEngine On - SecAuditLogParts ABCDEFGHIJKZ - SecDebugLog $ENV{DEBUG_LOG} - SecDebugLogLevel 9 - SecAuditLog $ENV{AUDIT_LOG} SecRequestBodyAccess On SecRule ARGS:a "\@streq 1" "phase:2,deny,chain" SecRule ARGS:b "\@streq 2" ), match_log => { - error => [ qr/Access denied with code 403 \(phase 2\). String match "2" at ARGS:b./, 1 ], + error => [ qr/Access denied with code 403 \(phase 2\)\. String match "2" at ARGS:b\./, 1 ], }, match_response => { status => qr/^403$/, @@ -119,11 +114,6 @@ comment => "SecRequestBodyAccess (neg)", conf => qq( SecRuleEngine On - SecAuditEngine On - SecAuditLogParts ABCDEFGHIJKZ - SecDebugLog $ENV{DEBUG_LOG} - SecDebugLogLevel 9 - SecAuditLog $ENV{AUDIT_LOG} SecRequestBodyAccess Off SecRule ARGS:a "\@streq 1" "phase:2,deny" SecRule ARGS:b "\@streq 2" "phase:2,deny" @@ -142,3 +132,176 @@ "a=1&b=2", ), }, + +### SecRequestBodyLimit +{ + type => "config", + comment => "SecRequestBodyLimit (equal)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 7 + ), + match_log => { + -error => [ qr/Request body is larger than the configured limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (greater)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 5 + ), + match_log => { + error => [ qr/Request body is larger than the configured limit \(5\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, + +### SecRequestBodyInMemoryLimit +{ + type => "config", + comment => "SecRequestBodyInMemoryLimit (equal)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimit 1000 + SecRequestBodyInMemoryLimit 266 + ), + match_log => { + -debug => [ qr/Input filter: Request too large to store in memory, switching to disk\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ) . encode_chunked(q(-----------------------------69343412719991675451336310646 +Content-Disposition: form-data; name="a" + +1 +-----------------------------69343412719991675451336310646 +Content-Disposition: form-data; name="b" + +2 +-----------------------------69343412719991675451336310646--), 1024), +}, +{ + type => "config", + comment => "SecRequestBodyInMemoryLimit (greater)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimit 1000 + SecRequestBodyInMemoryLimit 16 + ), + match_log => { + debug => [ qr/Input filter: Request too large to store in memory, switching to disk\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ) . encode_chunked(q(-----------------------------69343412719991675451336310646 +Content-Disposition: form-data; name="a" + +1 +-----------------------------69343412719991675451336310646 +Content-Disposition: form-data; name="b" + +2 +-----------------------------69343412719991675451336310646--), 1024), +}, + +### SecCookieFormat +{ + type => "config", + comment => "SecCookieFormat (pos)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecCookieFormat 1 + SecRule REQUEST_COOKIES_NAMES "\@streq SESSIONID" "phase:1,deny,chain" + SecRule REQUEST_COOKIES:\$SESSIONID_PATH "\@streq /" "chain" + SecRule REQUEST_COOKIES:SESSIONID "\@streq cookieval" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 1\)\. String match "cookieval" at REQUEST_COOKIES:SESSIONID\./, 1 ], + debug => [ qr(Adding request cookie: name "\$SESSIONID_PATH", value "/"), 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Cookie" => q($Version="1"; SESSIONID="cookieval"; $PATH="/"), + ], + undef, + ), +}, +{ + type => "config", + comment => "SecCookieFormat (neg)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecCookieFormat 0 + SecRule REQUEST_COOKIES_NAMES "\@streq SESSIONID" "phase:1,deny,chain" + SecRule REQUEST_COOKIES:\$SESSIONID_PATH "\@streq /" "chain" + SecRule REQUEST_COOKIES:SESSIONID "\@streq cookieval" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + -debug => [ qr(Adding request cookie: name "\$SESSIONID_PATH", value "/"), 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Cookie" => q($Version="1"; SESSIONID="cookieval"; $PATH="/"), + ], + undef, + ), +}, + diff --git a/apache2/t/run-regression-tests.pl.in b/apache2/t/run-regression-tests.pl.in index e515929d..bbc88b7e 100755 --- a/apache2/t/run-regression-tests.pl.in +++ b/apache2/t/run-regression-tests.pl.in @@ -21,19 +21,23 @@ use Data::Dumper; use IO::Socket; use LWP::UserAgent; -my @TYPES = qw(config target rule); +my @TYPES = qw(config action target rule); my $SCRIPT = basename($0); my $SCRIPT_DIR = File::Spec->rel2abs(dirname($0)); my $REG_DIR = "$SCRIPT_DIR/regression"; my $SROOT_DIR = "$REG_DIR/server_root"; my $CONF_DIR = "$SROOT_DIR/conf"; my $LOGS_DIR = "$SROOT_DIR/logs"; +my $PID_FILE = "$LOGS_DIR/httpd.pid"; my $PASSED = 0; my $TOTAL = 0; my %C = (); my %LOG = (); +my $UA_NAME = "ModSecurity Regression Tests/1.2.3"; my $UA = LWP::UserAgent->new; -$UA->agent("ModSecurity Regression Tests/1.2.3"); +$UA->agent($UA_NAME); + +$SIG{TERM} = $SIG{INT} = \&handle_interrupt; my %opt; getopts('A:E:D:C:T:H:a:p:dh', \%opt); @@ -96,6 +100,7 @@ $opt{p} = 8088 unless (defined $opt{p}); ERROR_LOG => $opt{E}, HTTPD_CONF => $opt{C}, HTDOCS => $opt{H}, + USER_AGENT => $UA_NAME, ); unless (defined $opt{S}) { @@ -105,15 +110,17 @@ unless (defined $opt{S}) { dbg("OPTIONS: ", \%opt); -msg("Attempting to stop any already running regression tests instances..."); -httpd_stop(); +if (-e "$PID_FILE") { + msg("Shutting down previous instance: $PID_FILE"); + httpd_stop(); +} if (defined $ARGV[0]) { runfile(dirname($ARGV[0]), basename($ARGV[0]), $ARGV[1]); done(); } -for my $type (sort @TYPES) { +for my $type (@TYPES) { my $dir = "$SCRIPT_DIR/regression/$type"; my @cfg = (); @@ -196,14 +203,14 @@ sub runfile { if ($neg and defined $match) { $rc = 1; msg("response $mtype matched: $m"); - dbg("$LOG{$mtype}{buf}"); + dbg($resp); last; } elsif (!$neg and !defined $match) { $rc = 1; msg("response $mtype no match: $m"); - dbg("$LOG{$mtype}{buf}"); + dbg($resp); last; } } @@ -254,6 +261,29 @@ sub runfile { msg(sprintf("Passed: %2d; Failed: %2d", $pass, $testnum ? (1 - $pass) : ($n - $pass))); } +sub do_raw_request { + my $sock = new IO::Socket::INET( + Proto => "tcp", + PeerAddr => "localhost", + PeerPort => $opt{p}, + ) or msg("Failed to connect to localhost:$opt{p}: $@"); + return unless ($sock); + + my $r = "@_"; + $r =~ s/^[^A-Z]+//s; + $r =~ s/^[ \t]+//mg; + $r =~ s/^\x0a/\x0d\x0a/mg; + $r =~ s/([^\x0d])\x0a/$1\x0d\x0a/mg; + + print $sock "$r"; + $sock->shutdown(1); + + my @resp = <$sock>; + $sock->close(); + + return HTTP::Response->parse(join("", @resp)); +} + sub do_request { my $r = $_[0]; @@ -267,8 +297,8 @@ sub do_request { return $UA->request($r); } else { - # TODO: send a raw request via IO::Socket and - # return HTTP::Request->parse($response_string) +# dbg("REQUEST:\n", $r); + return do_raw_request($r); } return; @@ -281,10 +311,10 @@ sub match_response { msg("Warning: Empty regular expression.") if (!defined $re or $re eq ""); if ($name eq "status") { - return $@ if ($resp->code =~ m/$re/m); + return $& if ($resp->code =~ m/$re/); } elsif ($name eq "content") { - return $@ if ($resp->content =~ m/$re/m); + return $& if ($resp->content =~ m/$re/m); } return; @@ -303,7 +333,7 @@ sub match_log { do { $n += $fh->sysread($$rbuf, 1024, $n); # dbg("Match \"$re\" in \"$$rbuf\" ($n)"); - return $@ if ($$rbuf =~ m/$re/m); + return $& if ($$rbuf =~ m/$re/m); # TODO: Use select()/poll() sleep 0.1; } while (gettimeofday - $t0 < $timeout); @@ -324,7 +354,7 @@ sub dbg { my $out = join "", map { (ref $_ ne "" ? Dumper($_) : $_) } @_; - $out =~ s/^/DBG: /s; + $out =~ s/^/DBG: /m; print STDOUT "$out\n"; } @@ -336,15 +366,21 @@ sub msg { print STDOUT "$out\n"; } +sub handle_interrupt { + $SIG{TERM} = $SIG{INT} = \&handle_interrupt; + + msg("Interrupted via SIG$_[0]. Shutting down tests..."); + httpd_stop(); + + quit(1); +} + sub quit { my($ec,$msg) = @_; $ec = 0 unless (defined $_[0]); msg("$msg") if (defined $msg); - msg("Attempting to stop any regression tests instance still running..."); - httpd_stop(); - exit $ec; } @@ -362,7 +398,7 @@ sub httpd_start { $opt{a}, -d => $opt{S}, -f => $opt{C}, - (map { (-c => $_) } ("Listen $opt{p}", @_)), + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), -k => "start", ); @@ -408,7 +444,7 @@ sub httpd_stop { $opt{a}, -d => $opt{S}, -f => $opt{C}, - (map { (-c => $_) } ("Listen $opt{p}", @_)), + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), -k => "stop", ); @@ -454,7 +490,7 @@ sub httpd_reload { $opt{a}, -d => $opt{S}, -f => $opt{C}, - (map { (-c => $_) } ("Listen $opt{p}", @_)), + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), -k => "graceful", ); @@ -510,5 +546,30 @@ sub httpd_reset_logs { $LOG{audit}{fd}->blocking(0); $LOG{audit}{fd}->sysseek(0, 2); $LOG{audit}{buf} = ""; + + # Debug + if (!defined $LOG{debug}{fd}) { + $LOG{debug}{fd} = new FileHandle($opt{D}, O_RDWR|O_CREAT); + } + $LOG{debug}{fd}->blocking(0); + $LOG{debug}{fd}->sysseek(0, 2); + $LOG{debug}{buf} = ""; } +sub encode_chunked { + my($data, $size) = @_; + $size = 128 unless ($size); + my $chunked = ""; + + my $n = 0; + my $bytes = length($data); + while ($bytes >= $size) { + $chunked .= sprintf "%x\x0d\x0a%s\x0d\x0a", $size, substr($data, $n, $size); + $n += $size; + $bytes -= $size; + } + if ($bytes) { + $chunked .= sprintf "%x\x0d\x0a%s\x0d\x0a", $bytes, substr($data, $n, $bytes); + } + $chunked .= "0\x0d\x0a\x0d\x0a" +}