Skip to content

Commit 372aa4c

Browse files
committed
add CRS regression tests via go-ftw and libmicrohttpd
1 parent dc37de6 commit 372aa4c

4 files changed

Lines changed: 259 additions & 0 deletions

File tree

.github/workflows/make-install.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,15 @@ jobs:
4343
- name: Install
4444
run: |
4545
sudo make V=1 install
46+
- name: Run CRS tests with go-ftw
47+
if: runner.os == 'Linux'
48+
run: |
49+
sudo apt-get install -y libmicrohttpd-dev
50+
sudo ldconfig
51+
go install github.com/coreruleset/go-ftw/v2@latest # renovate: datasource=go depName=github.com/coreruleset/go-ftw/v2
52+
git clone --depth 1 https://github.com/coreruleset/coreruleset.git /tmp/coreruleset
53+
gcc -o tests/coraza_httpd tests/coraza_httpd.c $(pkg-config --cflags --libs libmicrohttpd) -lcoraza -I/usr/local/include -L/usr/local/lib
54+
printf 'include coraza.conf\ninclude /tmp/coreruleset/crs-setup.conf.example\ninclude /tmp/coreruleset/plugins/empty-before.conf\ninclude /tmp/coreruleset/rules/*.conf\ninclude /tmp/coreruleset/plugins/empty-after.conf\n' > tests/coraza_includes.conf
55+
LD_LIBRARY_PATH=/usr/local/lib tests/coraza_httpd tests/coraza_includes.conf 8080 &
56+
sleep 2
57+
go-ftw run --config tests/ftw.yml --dir /tmp/coreruleset/tests/regression/tests/ --cloud --exclude "^920"

tests/coraza.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SecRuleEngine On
2+
SecRequestBodyAccess On
3+
SecResponseBodyAccess On
4+
SecResponseBodyMimeType text/plain text/html text/xml application/json
5+
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.blocking_paranoia_level=4,setvar:tx.detection_paranoia_level=4,setvar:tx.crs_validate_utf8_encoding=1,setvar:tx.arg_name_length=100,setvar:tx.arg_length=400,setvar:tx.total_arg_length=64000,setvar:tx.max_num_args=255,setvar:tx.max_file_size=64100,setvar:tx.combined_file_sizes=65535"

tests/coraza_httpd.c

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* coraza_httpd - minimal HTTP server with Coraza WAF
3+
* Uses libmicrohttpd + libcoraza for standalone FTW testing
4+
*/
5+
6+
#include <stdio.h>
7+
#include <stdlib.h>
8+
#include <string.h>
9+
#include <signal.h>
10+
#include <microhttpd.h>
11+
#include <coraza/coraza.h>
12+
13+
static coraza_waf_t waf = 0;
14+
static volatile int running = 1;
15+
16+
static void handle_signal(int sig) {
17+
(void)sig;
18+
running = 0;
19+
}
20+
21+
struct connection_info {
22+
char *data;
23+
size_t data_len;
24+
};
25+
26+
static enum MHD_Result header_iterator(void *cls,
27+
enum MHD_ValueKind kind, const char *key, const char *value)
28+
{
29+
(void)kind;
30+
coraza_transaction_t tx = (coraza_transaction_t)(uintptr_t)cls;
31+
if (key && value) {
32+
coraza_add_request_header(tx, (char*)key, strlen(key), (char*)value, strlen(value));
33+
}
34+
return MHD_YES;
35+
}
36+
37+
static enum MHD_Result handle_request(void *cls,
38+
struct MHD_Connection *connection,
39+
const char *url, const char *method, const char *version,
40+
const char *upload_data, size_t *upload_data_size,
41+
void **con_cls)
42+
{
43+
(void)cls;
44+
45+
/* First call: set up connection info */
46+
if (*con_cls == NULL) {
47+
struct connection_info *ci = calloc(1, sizeof(struct connection_info));
48+
*con_cls = ci;
49+
return MHD_YES;
50+
}
51+
52+
struct connection_info *ci = *con_cls;
53+
54+
/* Accumulate POST data */
55+
if (*upload_data_size > 0) {
56+
ci->data = realloc(ci->data, ci->data_len + *upload_data_size + 1);
57+
memcpy(ci->data + ci->data_len, upload_data, *upload_data_size);
58+
ci->data_len += *upload_data_size;
59+
ci->data[ci->data_len] = '\0';
60+
*upload_data_size = 0;
61+
return MHD_YES;
62+
}
63+
64+
/* All data received, process with Coraza */
65+
coraza_transaction_t tx = coraza_new_transaction(waf);
66+
67+
/* Phase 0: connection */
68+
coraza_process_connection(tx, "127.0.0.1", 12345, "127.0.0.1", 80);
69+
70+
/* Strip HTTP/ prefix for version */
71+
const char *proto = "1.1";
72+
if (version && strncmp(version, "HTTP/", 5) == 0)
73+
proto = version + 5;
74+
coraza_process_uri(tx, (char*)url, (char*)method, (char*)proto);
75+
coraza_intervention_t *it = coraza_intervention(tx);
76+
int denied = (it && it->status >= 400);
77+
int deny_status = it ? it->status : 0;
78+
if (it) coraza_free_intervention(it);
79+
80+
/* Phase 1: request headers */
81+
if (!denied) {
82+
MHD_get_connection_values(connection, MHD_HEADER_KIND,
83+
header_iterator, (void*)(uintptr_t)tx);
84+
85+
coraza_process_request_headers(tx);
86+
it = coraza_intervention(tx);
87+
if (it && it->status >= 400) { denied = 1; deny_status = it->status; }
88+
if (it) coraza_free_intervention(it);
89+
}
90+
91+
/* Phase 2: request body */
92+
if (!denied && ci->data) {
93+
coraza_append_request_body(tx, (unsigned char*)ci->data, ci->data_len);
94+
}
95+
if (!denied) {
96+
coraza_process_request_body(tx);
97+
it = coraza_intervention(tx);
98+
if (it && it->status >= 400) { denied = 1; deny_status = it->status; }
99+
if (it) coraza_free_intervention(it);
100+
}
101+
102+
/* Phase 3: response headers */
103+
if (!denied) {
104+
coraza_add_response_header(tx, "Content-Type", 12, "text/plain", 10);
105+
coraza_process_response_headers(tx, 200, "HTTP/1.1");
106+
it = coraza_intervention(tx);
107+
if (it && it->status >= 400) { denied = 1; deny_status = it->status; }
108+
if (it) coraza_free_intervention(it);
109+
}
110+
111+
/* Phase 4: response body */
112+
const char *resp_body;
113+
size_t resp_len;
114+
if (denied) {
115+
resp_body = "Access Denied\n";
116+
resp_len = 14;
117+
} else if (strcmp(url, "/reflect") == 0 && ci->data) {
118+
resp_body = ci->data;
119+
resp_len = ci->data_len;
120+
coraza_append_response_body(tx, (unsigned char*)resp_body, resp_len);
121+
} else {
122+
resp_body = "OK\n";
123+
resp_len = 3;
124+
}
125+
126+
if (!denied) {
127+
coraza_process_response_body(tx);
128+
it = coraza_intervention(tx);
129+
if (it && it->status >= 400) { denied = 1; deny_status = it->status; }
130+
if (it) coraza_free_intervention(it);
131+
}
132+
133+
/* Phase 5: logging */
134+
coraza_process_logging(tx);
135+
it = coraza_intervention(tx);
136+
if (it) coraza_free_intervention(it);
137+
138+
/* Send response */
139+
int status = denied ? deny_status : 200;
140+
if (denied) {
141+
resp_body = "Access Denied\n";
142+
resp_len = 14;
143+
}
144+
145+
struct MHD_Response *response = MHD_create_response_from_buffer(
146+
resp_len, (void*)resp_body, MHD_RESPMEM_MUST_COPY);
147+
enum MHD_Result ret = MHD_queue_response(connection, status, response);
148+
MHD_destroy_response(response);
149+
150+
coraza_free_transaction(tx);
151+
152+
return ret;
153+
}
154+
155+
static void request_completed(void *cls, struct MHD_Connection *connection,
156+
void **con_cls, enum MHD_RequestTerminationCode toe)
157+
{
158+
(void)cls; (void)connection; (void)toe;
159+
struct connection_info *ci = *con_cls;
160+
if (ci) {
161+
free(ci->data);
162+
free(ci);
163+
}
164+
}
165+
166+
int main(int argc, char **argv) {
167+
const char *config_file = "coraza_includes.conf";
168+
int port = 8080;
169+
170+
if (argc > 1) config_file = argv[1];
171+
if (argc > 2) port = atoi(argv[2]);
172+
173+
/* Create WAF */
174+
coraza_waf_config_t config = coraza_new_waf_config();
175+
if (coraza_rules_add_file(config, (char*)config_file) < 0) {
176+
fprintf(stderr, "Failed to load rules from %s\n", config_file);
177+
coraza_free_waf_config(config);
178+
return 1;
179+
}
180+
181+
char *err = NULL;
182+
waf = coraza_new_waf(config, &err);
183+
coraza_free_waf_config(config);
184+
if (waf == 0) {
185+
fprintf(stderr, "Failed to create WAF: %s\n", err ? err : "unknown");
186+
return 1;
187+
}
188+
printf("WAF loaded, %d rules\n", coraza_rules_count(waf));
189+
190+
/* Start HTTP server */
191+
struct MHD_Daemon *daemon = MHD_start_daemon(
192+
MHD_USE_INTERNAL_POLLING_THREAD,
193+
port, NULL, NULL,
194+
&handle_request, NULL,
195+
MHD_OPTION_NOTIFY_COMPLETED, &request_completed, NULL,
196+
MHD_OPTION_CLIENT_DISCIPLINE_LVL, (int)-1,
197+
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)5,
198+
MHD_OPTION_END);
199+
200+
if (!daemon) {
201+
fprintf(stderr, "Failed to start HTTP server\n");
202+
coraza_free_waf(waf);
203+
return 1;
204+
}
205+
206+
signal(SIGINT, handle_signal);
207+
signal(SIGTERM, handle_signal);
208+
209+
printf("Coraza HTTP server listening on port %d\n", port);
210+
211+
while (running) sleep(1);
212+
213+
MHD_stop_daemon(daemon);
214+
coraza_free_waf(waf);
215+
return 0;
216+
}

tests/ftw.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
testoverride:
2+
input:
3+
dest_addr: 127.0.0.1
4+
port: 8080
5+
ignore:
6+
# From https://github.com/corazawaf/coraza/blob/main/testing/coreruleset/.ftw.yml
7+
920100-4: 'Invalid uri, Coraza not reached - 404 page not found'
8+
920100-5: 'Invalid uri, Coraza not reached - 404 page not found'
9+
920100-8: 'Go/http allows a colon in the path. Test expects status 400 or 403 (Apache behaviour)'
10+
920270-4: 'Rule works, log contains 920270. Test expects status 400 (Apache behaviour)'
11+
920290-1: 'Rule works, log contains 920290. Test expects status 400 (Apache behaviour)'
12+
920430-5: 'Test has expect_error, Go/http and Envoy return 400'
13+
920430-8: 'Go/http does no allow HTTP/3.0 - 505 HTTP Version Not Supported'
14+
921250-1: 'Expected to match $Version in cookies, failing also in upstream'
15+
921250-2: 'Expected to match $Version in cookies, failing also in upstream'
16+
933120-2: 'To be investigated: match_regex value might be ModSec specific'
17+
920274-1: ''
18+
920280-3: ''
19+
920290-4: 'investigate, test related to empty host header'
20+
920430-3: 'investigate, expect_error: true'
21+
920430-9: ''
22+
920610-2: ''
23+
920620-1: ''
24+
922130-1: ''
25+
922130-2: ''
26+
922130-7: ''

0 commit comments

Comments
 (0)