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