blob: 8b9d998dda56e3a565197add74aaa21faf8410ec [file] [log] [blame]
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +02001/*
2 * Modsecurity wrapper for haproxy
3 *
4 * This file contains the wrapper which sends data in ModSecurity
5 * and returns the verdict.
6 *
7 * Copyright 2016 OZON, Thierry Fournier <thierry.fournier@ozon.io>
8 *
9 * This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version
12 * 2 of the License, or (at your option) any later version.
13 *
14 */
15#include <limits.h>
16#include <stdio.h>
17#include <stdarg.h>
18
Willy Tarreau86416052020-06-04 09:20:54 +020019#include <haproxy/hlua.h>
Willy Tarreau92b4f132020-06-01 11:05:15 +020020#include <haproxy/time.h>
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +020021
22#include <types/global.h>
23#include <types/stream.h>
24
Willy Tarreauaa74c4e2020-06-04 10:19:23 +020025#include <haproxy/arg.h>
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +020026#include <proto/log.h>
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +020027#include <proto/spoe.h>
28
29#include <api.h>
30
31#include "modsec_wrapper.h"
32#include "spoa.h"
33
34static char host_name[60];
35
36/* Note: The document and the code of "apr_table_make" considers
37 * that this function doesn't fails. The Apache APR code says
38 * other thing. If the system doesn't have any more memory, a
39 * a segfault occurs :(. Be carrefull with this module.
40 */
41
42struct directory_config *modsec_config = NULL;
43static server_rec *modsec_server = NULL;
44
45struct apr_bucket_haproxy {
46 apr_bucket_refcount refcount;
47 char *buffer;
48 size_t length;
49};
50
51static void haproxy_bucket_destroy(void *data)
52{
53 struct apr_bucket_haproxy *bucket = data;
54
55 if (apr_bucket_shared_destroy(bucket))
56 apr_bucket_free(bucket);
57}
58
59static apr_status_t haproxy_bucket_read(apr_bucket *bucket, const char **str,
60 apr_size_t *len, apr_read_type_e block)
61{
62 struct apr_bucket_haproxy *data = bucket->data;
63
64 if (bucket->start) {
65 *str = NULL;
66 *len = 0;
67 return APR_SUCCESS;
68 }
69
70 *str = data->buffer;
71 *len = data->length;
72 bucket->start = 1; /* Just a flag to say that the read is started */
73
74 return APR_SUCCESS;
75}
76
77static const apr_bucket_type_t apr_bucket_type_haproxy = {
78 "HAProxy", 7, APR_BUCKET_DATA,
79 haproxy_bucket_destroy,
80 haproxy_bucket_read,
81 apr_bucket_setaside_noop,
82 apr_bucket_shared_split,
83 apr_bucket_shared_copy
84};
85
86static char *chunk_strdup(struct request_rec *req, const char *str, size_t len)
87{
88 char *out;
89
90 out = apr_pcalloc(req->pool, len + 1);
91 if (!out)
92 return NULL;
93 memcpy(out, str, len);
94 out[len] = '\0';
95 return out;
96}
97
98static char *printf_dup(struct request_rec *req, char *fmt, ...)
99{
100 char *out;
101 va_list ap;
102 int len;
103
104 va_start(ap, fmt);
105 len = vsnprintf(NULL, 0, fmt, ap);
Dragan Dosen2f1cacb2017-09-18 09:20:43 +0200106 va_end(ap);
107
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200108 if (len == -1)
109 return NULL;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200110
111 out = apr_pcalloc(req->pool, len + 1);
112 if (!out)
113 return NULL;
114
115 va_start(ap, fmt);
116 len = vsnprintf(out, len + 1, fmt, ap);
Dragan Dosen2f1cacb2017-09-18 09:20:43 +0200117 va_end(ap);
118
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200119 if (len == -1)
120 return NULL;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200121
122 return out;
123}
124
125/* This function send logs. For now, it do nothing. */
126static void modsec_log(void *obj, int level, char *str)
127{
128 LOG(&null_worker, "%s", str);
129}
130
Joseph Herlant9fe83fa2018-11-09 18:25:59 -0800131/* This function load the ModSecurity file. It returns -1 if the
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200132 * initialisation fails.
133 */
134int modsecurity_load(const char *file)
135{
136 const char *msg;
137 char cwd[128];
138
139 /* Initialises modsecurity. */
140
141 modsec_server = modsecInit();
142 if (modsec_server == NULL) {
Ilya Shipitsince7b00f2020-03-23 22:28:40 +0500143 LOG(&null_worker, "ModSecurity initialisation failed.\n");
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200144 return -1;
145 }
146
147 modsecSetLogHook(NULL, modsec_log);
148
149 gethostname(host_name, 60);
150 modsec_server->server_hostname = host_name;
151
152 modsecStartConfig();
153
154 modsec_config = modsecGetDefaultConfig();
155 if (modsec_config == NULL) {
Ilya Shipitsince7b00f2020-03-23 22:28:40 +0500156 LOG(&null_worker, "ModSecurity default configuration initialisation failed.\n");
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200157 return -1;
158 }
159
160 msg = modsecProcessConfig(modsec_config, file, getcwd(cwd, 128));
161 if (msg != NULL) {
162 LOG(&null_worker, "ModSecurity load configuration failed.\n");
163 return -1;
164 }
165
166 modsecFinalizeConfig();
167
168 modsecInitProcess();
169
170 return 1;
171}
172
173struct modsec_hdr {
174 const char *name;
175 uint64_t name_len;
176 const char *value;
177 uint64_t value_len;
178};
179
180int modsecurity_process(struct worker *worker, struct modsecurity_parameters *params)
181{
182 struct conn_rec *cr;
183 struct request_rec *req;
184 struct apr_bucket_brigade *brigade;
185 struct apr_bucket *link_bucket;
186 struct apr_bucket_haproxy *data_bucket;
187 struct apr_bucket *last_bucket;
188 int i;
189 long clength;
190 char *err;
191 int fail;
192 const char *lang;
193 char *name, *value;
194 // int body_partial;
195 struct timeval now;
196 int ret;
197 char *buf;
198 char *end;
199 const char *uniqueid;
200 uint64_t uniqueid_len;
201 const char *meth;
202 uint64_t meth_len;
203 const char *path;
204 uint64_t path_len;
205 const char *qs;
206 uint64_t qs_len;
207 const char *vers;
208 uint64_t vers_len;
209 const char *body;
210 uint64_t body_len;
211 uint64_t body_exposed_len;
212 uint64_t hdr_nb;
213 struct modsec_hdr hdrs[255];
214 struct modsec_hdr hdr;
215 int status;
216 int return_code = -1;
217
218 /* Decode uniqueid. */
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200219 uniqueid = params->uniqueid.data.u.str.area;
220 uniqueid_len = params->uniqueid.data.u.str.data;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200221
222 /* Decode method. */
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200223 meth = params->method.data.u.str.area;
224 meth_len = params->method.data.u.str.data;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200225
226 /* Decode path. */
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200227 path = params->path.data.u.str.area;
228 path_len = params->path.data.u.str.data;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200229
230 /* Decode query string. */
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200231 qs = params->query.data.u.str.area;
232 qs_len = params->query.data.u.str.data;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200233
234 /* Decode version. */
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200235 vers = params->vers.data.u.str.area;
236 vers_len = params->vers.data.u.str.data;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200237
238 /* Decode header binary block. */
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200239 buf = params->hdrs_bin.data.u.str.area;
240 end = buf + params->hdrs_bin.data.u.str.data;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200241
242 /* Decode each header. */
243 hdr_nb = 0;
244 while (1) {
245
246 /* Initialise the storage struct. It is useless
247 * because the process fail if the struct is not
248 * fully filled. This init is just does in order
249 * to prevent bug after some improvements.
250 */
251 memset(&hdr, 0, sizeof(hdr));
252
253 /* Decode header name. */
254 ret = decode_varint(&buf, end, &hdr.name_len);
255 if (ret == -1)
256 return -1;
257 hdr.name = buf;
258 buf += hdr.name_len;
259 if (buf > end)
260 return -1;
261
262 /* Decode header value. */
263 ret = decode_varint(&buf, end, &hdr.value_len);
264 if (ret == -1)
265 return -1;
266 hdr.value = buf;
267 buf += hdr.value_len;
268 if (buf > end)
269 return -1;
270
271 /* Detect the end of the headers. */
272 if (hdr.name_len == 0 && hdr.value_len == 0)
273 break;
274
275 /* Store the header. */
276 if (hdr_nb < 255) {
277 memcpy(&hdrs[hdr_nb], &hdr, sizeof(hdr));
278 hdr_nb++;
279 }
280 }
281
282 /* Decode body length. Note that the following control
283 * is just set for avoifing a gcc warning.
284 */
285 body_exposed_len = (uint64_t)params->body_length.data.u.sint;
286 if (body_exposed_len < 0)
287 return -1;
288
289 /* Decode body. */
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200290 body = params->body.data.u.str.area;
291 body_len = params->body.data.u.str.data;
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200292
293 fail = 1;
294
295 /* Init processing */
296
297 cr = modsecNewConnection();
298 req = modsecNewRequest(cr, modsec_config);
299
300 /* Load request. */
301
302 req->proxyreq = PROXYREQ_NONE;
303 req->header_only = 0; /* May modified later */
304
305 /* Copy header list. */
306
307 for (i = 0; i < hdr_nb; i++) {
308 name = chunk_strdup(req, hdrs[i].name, hdrs[i].name_len);
309 if (!name) {
310 errno = ENOMEM;
311 goto fail;
312 }
313 value = chunk_strdup(req, hdrs[i].value, hdrs[i].value_len);
314 if (!value) {
315 errno = ENOMEM;
316 goto fail;
317 }
318 apr_table_setn(req->headers_in, name, value);
319 }
320
321 /* Process special headers. */
322 req->range = apr_table_get(req->headers_in, "Range");
323 req->content_type = apr_table_get(req->headers_in, "Content-Type");
324 req->content_encoding = apr_table_get(req->headers_in, "Content-Encoding");
325 req->hostname = apr_table_get(req->headers_in, "Host");
Yann Cézardbf60f6b2019-04-25 14:30:23 +0200326 if (req->hostname != NULL) {
327 req->parsed_uri.hostname = chunk_strdup(req, req->hostname, strlen(req->hostname));
328 } else {
329 req->parsed_uri.hostname = NULL;
330 }
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200331
332 lang = apr_table_get(req->headers_in, "Content-Languages");
333 if (lang != NULL) {
334 req->content_languages = apr_array_make(req->pool, 1, sizeof(const char *));
335 *(const char **)apr_array_push(req->content_languages) = lang;
336 }
337
338 lang = apr_table_get(req->headers_in, "Content-Length");
339 if (lang) {
340 errno = 0;
341 clength = strtol(lang, &err, 10);
342 if (*err != '\0' || errno != 0 || clength < 0 || clength > INT_MAX) {
343 errno = ERANGE;
344 goto fail;
345 }
346 req->clength = clength;
347 }
348
349 /* Copy the first line of the request. */
350 req->the_request = printf_dup(req, "%.*s %.*s%s%.*s %.*s",
351 meth_len, meth,
352 path_len, path,
353 qs_len > 0 ? "?" : "",
354 qs_len, qs,
355 vers_len, vers);
356 if (!req->the_request) {
357 errno = ENOMEM;
358 goto fail;
359 }
360
361 /* Copy the method. */
362 req->method = chunk_strdup(req, meth, meth_len);
363 if (!req->method) {
364 errno = ENOMEM;
365 goto fail;
366 }
367
368 /* Set the method number. */
369 if (meth_len < 3) {
370 errno = EINVAL;
371 goto fail;
372 }
373
374 /* Detect the method */
375 switch (meth_len) {
376 case 3:
377 if (strncmp(req->method, "GET", 3) == 0)
378 req->method_number = M_GET;
379 else if (strncmp(req->method, "PUT", 3) == 0)
380 req->method_number = M_PUT;
381 else {
382 errno = EINVAL;
383 goto fail;
384 }
385 break;
386 case 4:
387 if (strncmp(req->method, "POST", 4) == 0)
388 req->method_number = M_POST;
389 else if (strncmp(req->method, "HEAD", 4) == 0) {
390 req->method_number = M_GET;
391 req->header_only = 1;
392 }
393 else if (strncmp(req->method, "COPY", 4) == 0)
394 req->method_number = M_COPY;
395 else if (strncmp(req->method, "MOVE", 4) == 0)
396 req->method_number = M_MOVE;
397 else if (strncmp(req->method, "LOCK", 4) == 0)
398 req->method_number = M_LOCK;
399 else {
400 errno = EINVAL;
401 goto fail;
402 }
403 break;
404 case 5:
405 if (strncmp(req->method, "TRACE", 5) == 0)
406 req->method_number = M_TRACE;
407 else if (strncmp(req->method, "PATCH", 5) == 0)
408 req->method_number = M_PATCH;
409 else if (strncmp(req->method, "MKCOL", 5) == 0)
410 req->method_number = M_MKCOL;
411 else if (strncmp(req->method, "MERGE", 5) == 0)
412 req->method_number = M_MERGE;
413 else if (strncmp(req->method, "LABEL", 5) == 0)
414 req->method_number = M_LABEL;
415 else {
416 errno = EINVAL;
417 goto fail;
418 }
419 break;
420 case 6:
421 if (strncmp(req->method, "DELETE", 6) == 0)
422 req->method_number = M_DELETE;
423 else if (strncmp(req->method, "REPORT", 6) == 0)
424 req->method_number = M_REPORT;
425 else if (strncmp(req->method, "UPDATE", 6) == 0)
426 req->method_number = M_UPDATE;
427 else if (strncmp(req->method, "UNLOCK", 6) == 0)
428 req->method_number = M_UNLOCK;
429 else {
430 errno = EINVAL;
431 goto fail;
432 }
433 break;
434 case 7:
435 if (strncmp(req->method, "CHECKIN", 7) == 0)
436 req->method_number = M_CHECKIN;
437 else if (strncmp(req->method, "INVALID", 7) == 0)
438 req->method_number = M_INVALID;
439 else if (strncmp(req->method, "CONNECT", 7) == 0)
440 req->method_number = M_CONNECT;
441 else if (strncmp(req->method, "OPTIONS", 7) == 0)
442 req->method_number = M_OPTIONS;
443 else {
444 errno = EINVAL;
445 goto fail;
446 }
447 break;
448 case 8:
449 if (strncmp(req->method, "PROPFIND", 8) == 0)
450 req->method_number = M_PROPFIND;
451 else if (strncmp(req->method, "CHECKOUT", 8) == 0)
452 req->method_number = M_CHECKOUT;
453 else {
454 errno = EINVAL;
455 goto fail;
456 }
457 break;
458 case 9:
459 if (strncmp(req->method, "PROPPATCH", 9) == 0)
460 req->method_number = M_PROPPATCH;
461 else {
462 errno = EINVAL;
463 goto fail;
464 }
465 break;
466 case 10:
467 if (strncmp(req->method, "MKACTIVITY", 10) == 0)
468 req->method_number = M_MKACTIVITY;
469 else if (strncmp(req->method, "UNCHECKOUT", 10) == 0)
470 req->method_number = M_UNCHECKOUT;
471 else {
472 errno = EINVAL;
473 goto fail;
474 }
475 break;
476 case 11:
477 if (strncmp(req->method, "MKWORKSPACE", 11) == 0)
478 req->method_number = M_MKWORKSPACE;
479 else {
480 errno = EINVAL;
481 goto fail;
482 }
483 break;
484 case 15:
485 if (strncmp(req->method, "VERSION_CONTROL", 15) == 0)
486 req->method_number = M_VERSION_CONTROL;
487 else {
488 errno = EINVAL;
489 goto fail;
490 }
491 break;
492 case 16:
493 if (strncmp(req->method, "BASELINE_CONTROL", 16) == 0)
494 req->method_number = M_BASELINE_CONTROL;
495 else {
496 errno = EINVAL;
497 goto fail;
498 }
499 break;
500 default:
501 errno = EINVAL;
502 goto fail;
503 }
504
505 /* Copy the protocol. */
506 req->protocol = chunk_strdup(req, vers, vers_len);
507 if (!req->protocol) {
508 errno = ENOMEM;
509 goto fail;
510 }
511
512 /* Compute the protocol number. */
513 if (vers_len >= 8)
514 req->proto_num = 1000 + !!(vers[7] == '1');
515
516 /* The request time. */
517 gettimeofday(&now, NULL);
518 req->request_time = apr_time_make(now.tv_sec, now.tv_usec / 1000);
519
520 /* No status line. */
521 req->status_line = NULL;
522 req->status = 0;
523
524 /* Copy path. */
525 req->parsed_uri.path = chunk_strdup(req, path, path_len);
526 if (!req->parsed_uri.path) {
527 errno = ENOMEM;
528 goto fail;
529 }
530
531 /* Copy args (query string). */
532 req->args = chunk_strdup(req, qs, qs_len);
533 if (!req->args) {
534 errno = ENOMEM;
535 goto fail;
536 }
537
538 /* Set parsed_uri */
539
540 req->parsed_uri.scheme = "http";
541
542 if (req->hostname && req->parsed_uri.scheme && req->parsed_uri.path) {
543 i = snprintf(NULL, 0, "%s://%s%s",
544 req->parsed_uri.scheme, req->hostname, req->parsed_uri.path);
545 req->uri = apr_pcalloc(req->pool, i + 1);
546 if (!req->uri) {
547 errno = ENOMEM;
548 goto fail;
549 }
550 i = snprintf(req->uri, i + 1, "%s://%s%s",
551 req->parsed_uri.scheme, req->hostname, req->parsed_uri.path);
552 }
553
554 req->filename = req->parsed_uri.path;
555
556 /* Set unique id */
557
558 apr_table_setn(req->subprocess_env, "UNIQUE_ID", chunk_strdup(req, uniqueid, uniqueid_len));
559
560 /*
561 *
562 * Load body.
563 *
564 */
565
566 /* Create an empty bucket brigade */
567 brigade = apr_brigade_create(req->pool, req->connection->bucket_alloc);
568 if (!brigade) {
569 errno = ENOMEM;
570 goto fail;
571 }
572
Joseph Herlant9fe83fa2018-11-09 18:25:59 -0800573 /* Stores HTTP body available data in a bucket */
Thierry FOURNIERa5ec06d2017-04-10 23:47:23 +0200574 data_bucket = apr_bucket_alloc(sizeof(*data_bucket), req->connection->bucket_alloc);
575 if (!data_bucket) {
576 errno = ENOMEM;
577 goto fail;
578 }
579 data_bucket->buffer = (char *)body;
580 data_bucket->length = body_len;
581
582 /* Create linked bucket */
583 link_bucket = apr_bucket_alloc(sizeof(*link_bucket), req->connection->bucket_alloc);
584 if (!link_bucket) {
585 errno = ENOMEM;
586 goto fail;
587 }
588 APR_BUCKET_INIT(link_bucket); /* link */
589 link_bucket->free = apr_bucket_free;
590 link_bucket->list = req->connection->bucket_alloc;
591 link_bucket = apr_bucket_shared_make(link_bucket, data_bucket, 0, body_len);
592 link_bucket->type = &apr_bucket_type_haproxy;
593
594 /* Insert the bucket at the end of the brigade. */
595 APR_BRIGADE_INSERT_TAIL(brigade, link_bucket);
596
597 /* Insert the last bucket. */
598 last_bucket = apr_bucket_eos_create(req->connection->bucket_alloc);
599 APR_BRIGADE_INSERT_TAIL(brigade, last_bucket);
600
601 /* Declares the bucket brigade in modsecurity */
602 modsecSetBodyBrigade(req, brigade);
603
604 /*
605 *
606 * Process analysis.
607 *
608 */
609
610 /* Process request headers analysis. */
611 status = modsecProcessRequestHeaders(req);
612 if (status != DECLINED && status != DONE)
613 return_code = status;
614
615 /* Process request body analysis. */
616 status = modsecProcessRequestBody(req);
617 if (status != DECLINED && status != DONE)
618 return_code = status;
619
620 /* End processing. */
621
622 fail = 0;
623 if (return_code == -1)
624 return_code = 0;
625
626fail:
627
628 modsecFinishRequest(req);
629 modsecFinishConnection(cr);
630
631 if (fail) {
632
633 /* errno == ERANGE / ENOMEM / EINVAL */
634 switch (errno) {
635 case ERANGE: LOG(worker, "Invalid range");
636 case ENOMEM: LOG(worker, "Out of memory error");
637 case EINVAL: LOG(worker, "Invalid value");
638 default: LOG(worker, "Unknown error");
639 }
640 }
641
642 return return_code;
643}