/* * Modsecurity wrapper for haproxy * * This file contains the wrapper which sends data in ModSecurity * and returns the verdict. * * Copyright 2016 OZON, Thierry Fournier * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "modsec_wrapper.h" #include "spoa.h" static char host_name[60]; /* Note: The document and the code of "apr_table_make" considers * that this function doesn't fails. The Apache APR code says * other thing. If the system doesn't have any more memory, a * a segfault occurs :(. Be carrefull with this module. */ struct directory_config *modsec_config = NULL; static server_rec *modsec_server = NULL; struct apr_bucket_haproxy { apr_bucket_refcount refcount; char *buffer; size_t length; }; static void haproxy_bucket_destroy(void *data) { struct apr_bucket_haproxy *bucket = data; if (apr_bucket_shared_destroy(bucket)) apr_bucket_free(bucket); } static apr_status_t haproxy_bucket_read(apr_bucket *bucket, const char **str, apr_size_t *len, apr_read_type_e block) { struct apr_bucket_haproxy *data = bucket->data; if (bucket->start) { *str = NULL; *len = 0; return APR_SUCCESS; } *str = data->buffer; *len = data->length; bucket->start = 1; /* Just a flag to say that the read is started */ return APR_SUCCESS; } static const apr_bucket_type_t apr_bucket_type_haproxy = { "HAProxy", 7, APR_BUCKET_DATA, haproxy_bucket_destroy, haproxy_bucket_read, apr_bucket_setaside_noop, apr_bucket_shared_split, apr_bucket_shared_copy }; static char *chunk_strdup(struct request_rec *req, const char *str, size_t len) { char *out; out = apr_pcalloc(req->pool, len + 1); if (!out) return NULL; memcpy(out, str, len); out[len] = '\0'; return out; } static char *printf_dup(struct request_rec *req, char *fmt, ...) { char *out; va_list ap; int len; va_start(ap, fmt); len = vsnprintf(NULL, 0, fmt, ap); va_end(ap); if (len == -1) return NULL; out = apr_pcalloc(req->pool, len + 1); if (!out) return NULL; va_start(ap, fmt); len = vsnprintf(out, len + 1, fmt, ap); va_end(ap); if (len == -1) return NULL; return out; } /* This function send logs. For now, it do nothing. */ static void modsec_log(void *obj, int level, char *str) { LOG(&null_worker, "%s", str); } /* This fucntion load the ModSecurity file. It returns -1 if the * initialisation fails. */ int modsecurity_load(const char *file) { const char *msg; char cwd[128]; /* Initialises modsecurity. */ modsec_server = modsecInit(); if (modsec_server == NULL) { LOG(&null_worker, "ModSecurity initilisation failed.\n"); return -1; } modsecSetLogHook(NULL, modsec_log); gethostname(host_name, 60); modsec_server->server_hostname = host_name; modsecStartConfig(); modsec_config = modsecGetDefaultConfig(); if (modsec_config == NULL) { LOG(&null_worker, "ModSecurity default configuration initilisation failed.\n"); return -1; } msg = modsecProcessConfig(modsec_config, file, getcwd(cwd, 128)); if (msg != NULL) { LOG(&null_worker, "ModSecurity load configuration failed.\n"); return -1; } modsecFinalizeConfig(); modsecInitProcess(); return 1; } struct modsec_hdr { const char *name; uint64_t name_len; const char *value; uint64_t value_len; }; int modsecurity_process(struct worker *worker, struct modsecurity_parameters *params) { struct conn_rec *cr; struct request_rec *req; struct apr_bucket_brigade *brigade; struct apr_bucket *link_bucket; struct apr_bucket_haproxy *data_bucket; struct apr_bucket *last_bucket; int i; long clength; char *err; int fail; const char *lang; char *name, *value; // int body_partial; struct timeval now; int ret; char *buf; char *end; const char *uniqueid; uint64_t uniqueid_len; const char *meth; uint64_t meth_len; const char *path; uint64_t path_len; const char *qs; uint64_t qs_len; const char *vers; uint64_t vers_len; const char *body; uint64_t body_len; uint64_t body_exposed_len; uint64_t hdr_nb; struct modsec_hdr hdrs[255]; struct modsec_hdr hdr; int status; int return_code = -1; /* Decode uniqueid. */ uniqueid = params->uniqueid.data.u.str.area; uniqueid_len = params->uniqueid.data.u.str.data; /* Decode method. */ meth = params->method.data.u.str.area; meth_len = params->method.data.u.str.data; /* Decode path. */ path = params->path.data.u.str.area; path_len = params->path.data.u.str.data; /* Decode query string. */ qs = params->query.data.u.str.area; qs_len = params->query.data.u.str.data; /* Decode version. */ vers = params->vers.data.u.str.area; vers_len = params->vers.data.u.str.data; /* Decode header binary block. */ buf = params->hdrs_bin.data.u.str.area; end = buf + params->hdrs_bin.data.u.str.data; /* Decode each header. */ hdr_nb = 0; while (1) { /* Initialise the storage struct. It is useless * because the process fail if the struct is not * fully filled. This init is just does in order * to prevent bug after some improvements. */ memset(&hdr, 0, sizeof(hdr)); /* Decode header name. */ ret = decode_varint(&buf, end, &hdr.name_len); if (ret == -1) return -1; hdr.name = buf; buf += hdr.name_len; if (buf > end) return -1; /* Decode header value. */ ret = decode_varint(&buf, end, &hdr.value_len); if (ret == -1) return -1; hdr.value = buf; buf += hdr.value_len; if (buf > end) return -1; /* Detect the end of the headers. */ if (hdr.name_len == 0 && hdr.value_len == 0) break; /* Store the header. */ if (hdr_nb < 255) { memcpy(&hdrs[hdr_nb], &hdr, sizeof(hdr)); hdr_nb++; } } /* Decode body length. Note that the following control * is just set for avoifing a gcc warning. */ body_exposed_len = (uint64_t)params->body_length.data.u.sint; if (body_exposed_len < 0) return -1; /* Decode body. */ body = params->body.data.u.str.area; body_len = params->body.data.u.str.data; fail = 1; /* Init processing */ cr = modsecNewConnection(); req = modsecNewRequest(cr, modsec_config); /* Load request. */ req->proxyreq = PROXYREQ_NONE; req->header_only = 0; /* May modified later */ /* Copy header list. */ for (i = 0; i < hdr_nb; i++) { name = chunk_strdup(req, hdrs[i].name, hdrs[i].name_len); if (!name) { errno = ENOMEM; goto fail; } value = chunk_strdup(req, hdrs[i].value, hdrs[i].value_len); if (!value) { errno = ENOMEM; goto fail; } apr_table_setn(req->headers_in, name, value); } /* Process special headers. */ req->range = apr_table_get(req->headers_in, "Range"); req->content_type = apr_table_get(req->headers_in, "Content-Type"); req->content_encoding = apr_table_get(req->headers_in, "Content-Encoding"); req->hostname = apr_table_get(req->headers_in, "Host"); req->parsed_uri.hostname = chunk_strdup(req, req->hostname, strlen(req->hostname)); lang = apr_table_get(req->headers_in, "Content-Languages"); if (lang != NULL) { req->content_languages = apr_array_make(req->pool, 1, sizeof(const char *)); *(const char **)apr_array_push(req->content_languages) = lang; } lang = apr_table_get(req->headers_in, "Content-Length"); if (lang) { errno = 0; clength = strtol(lang, &err, 10); if (*err != '\0' || errno != 0 || clength < 0 || clength > INT_MAX) { errno = ERANGE; goto fail; } req->clength = clength; } /* Copy the first line of the request. */ req->the_request = printf_dup(req, "%.*s %.*s%s%.*s %.*s", meth_len, meth, path_len, path, qs_len > 0 ? "?" : "", qs_len, qs, vers_len, vers); if (!req->the_request) { errno = ENOMEM; goto fail; } /* Copy the method. */ req->method = chunk_strdup(req, meth, meth_len); if (!req->method) { errno = ENOMEM; goto fail; } /* Set the method number. */ if (meth_len < 3) { errno = EINVAL; goto fail; } /* Detect the method */ switch (meth_len) { case 3: if (strncmp(req->method, "GET", 3) == 0) req->method_number = M_GET; else if (strncmp(req->method, "PUT", 3) == 0) req->method_number = M_PUT; else { errno = EINVAL; goto fail; } break; case 4: if (strncmp(req->method, "POST", 4) == 0) req->method_number = M_POST; else if (strncmp(req->method, "HEAD", 4) == 0) { req->method_number = M_GET; req->header_only = 1; } else if (strncmp(req->method, "COPY", 4) == 0) req->method_number = M_COPY; else if (strncmp(req->method, "MOVE", 4) == 0) req->method_number = M_MOVE; else if (strncmp(req->method, "LOCK", 4) == 0) req->method_number = M_LOCK; else { errno = EINVAL; goto fail; } break; case 5: if (strncmp(req->method, "TRACE", 5) == 0) req->method_number = M_TRACE; else if (strncmp(req->method, "PATCH", 5) == 0) req->method_number = M_PATCH; else if (strncmp(req->method, "MKCOL", 5) == 0) req->method_number = M_MKCOL; else if (strncmp(req->method, "MERGE", 5) == 0) req->method_number = M_MERGE; else if (strncmp(req->method, "LABEL", 5) == 0) req->method_number = M_LABEL; else { errno = EINVAL; goto fail; } break; case 6: if (strncmp(req->method, "DELETE", 6) == 0) req->method_number = M_DELETE; else if (strncmp(req->method, "REPORT", 6) == 0) req->method_number = M_REPORT; else if (strncmp(req->method, "UPDATE", 6) == 0) req->method_number = M_UPDATE; else if (strncmp(req->method, "UNLOCK", 6) == 0) req->method_number = M_UNLOCK; else { errno = EINVAL; goto fail; } break; case 7: if (strncmp(req->method, "CHECKIN", 7) == 0) req->method_number = M_CHECKIN; else if (strncmp(req->method, "INVALID", 7) == 0) req->method_number = M_INVALID; else if (strncmp(req->method, "CONNECT", 7) == 0) req->method_number = M_CONNECT; else if (strncmp(req->method, "OPTIONS", 7) == 0) req->method_number = M_OPTIONS; else { errno = EINVAL; goto fail; } break; case 8: if (strncmp(req->method, "PROPFIND", 8) == 0) req->method_number = M_PROPFIND; else if (strncmp(req->method, "CHECKOUT", 8) == 0) req->method_number = M_CHECKOUT; else { errno = EINVAL; goto fail; } break; case 9: if (strncmp(req->method, "PROPPATCH", 9) == 0) req->method_number = M_PROPPATCH; else { errno = EINVAL; goto fail; } break; case 10: if (strncmp(req->method, "MKACTIVITY", 10) == 0) req->method_number = M_MKACTIVITY; else if (strncmp(req->method, "UNCHECKOUT", 10) == 0) req->method_number = M_UNCHECKOUT; else { errno = EINVAL; goto fail; } break; case 11: if (strncmp(req->method, "MKWORKSPACE", 11) == 0) req->method_number = M_MKWORKSPACE; else { errno = EINVAL; goto fail; } break; case 15: if (strncmp(req->method, "VERSION_CONTROL", 15) == 0) req->method_number = M_VERSION_CONTROL; else { errno = EINVAL; goto fail; } break; case 16: if (strncmp(req->method, "BASELINE_CONTROL", 16) == 0) req->method_number = M_BASELINE_CONTROL; else { errno = EINVAL; goto fail; } break; default: errno = EINVAL; goto fail; } /* Copy the protocol. */ req->protocol = chunk_strdup(req, vers, vers_len); if (!req->protocol) { errno = ENOMEM; goto fail; } /* Compute the protocol number. */ if (vers_len >= 8) req->proto_num = 1000 + !!(vers[7] == '1'); /* The request time. */ gettimeofday(&now, NULL); req->request_time = apr_time_make(now.tv_sec, now.tv_usec / 1000); /* No status line. */ req->status_line = NULL; req->status = 0; /* Copy path. */ req->parsed_uri.path = chunk_strdup(req, path, path_len); if (!req->parsed_uri.path) { errno = ENOMEM; goto fail; } /* Copy args (query string). */ req->args = chunk_strdup(req, qs, qs_len); if (!req->args) { errno = ENOMEM; goto fail; } /* Set parsed_uri */ req->parsed_uri.scheme = "http"; if (req->hostname && req->parsed_uri.scheme && req->parsed_uri.path) { i = snprintf(NULL, 0, "%s://%s%s", req->parsed_uri.scheme, req->hostname, req->parsed_uri.path); req->uri = apr_pcalloc(req->pool, i + 1); if (!req->uri) { errno = ENOMEM; goto fail; } i = snprintf(req->uri, i + 1, "%s://%s%s", req->parsed_uri.scheme, req->hostname, req->parsed_uri.path); } req->filename = req->parsed_uri.path; /* Set unique id */ apr_table_setn(req->subprocess_env, "UNIQUE_ID", chunk_strdup(req, uniqueid, uniqueid_len)); /* * * Load body. * */ /* Create an empty bucket brigade */ brigade = apr_brigade_create(req->pool, req->connection->bucket_alloc); if (!brigade) { errno = ENOMEM; goto fail; } /* Stores HTTP body avalaible data in a bucket */ data_bucket = apr_bucket_alloc(sizeof(*data_bucket), req->connection->bucket_alloc); if (!data_bucket) { errno = ENOMEM; goto fail; } data_bucket->buffer = (char *)body; data_bucket->length = body_len; /* Create linked bucket */ link_bucket = apr_bucket_alloc(sizeof(*link_bucket), req->connection->bucket_alloc); if (!link_bucket) { errno = ENOMEM; goto fail; } APR_BUCKET_INIT(link_bucket); /* link */ link_bucket->free = apr_bucket_free; link_bucket->list = req->connection->bucket_alloc; link_bucket = apr_bucket_shared_make(link_bucket, data_bucket, 0, body_len); link_bucket->type = &apr_bucket_type_haproxy; /* Insert the bucket at the end of the brigade. */ APR_BRIGADE_INSERT_TAIL(brigade, link_bucket); /* Insert the last bucket. */ last_bucket = apr_bucket_eos_create(req->connection->bucket_alloc); APR_BRIGADE_INSERT_TAIL(brigade, last_bucket); /* Declares the bucket brigade in modsecurity */ modsecSetBodyBrigade(req, brigade); /* * * Process analysis. * */ /* Process request headers analysis. */ status = modsecProcessRequestHeaders(req); if (status != DECLINED && status != DONE) return_code = status; /* Process request body analysis. */ status = modsecProcessRequestBody(req); if (status != DECLINED && status != DONE) return_code = status; /* End processing. */ fail = 0; if (return_code == -1) return_code = 0; fail: modsecFinishRequest(req); modsecFinishConnection(cr); if (fail) { /* errno == ERANGE / ENOMEM / EINVAL */ switch (errno) { case ERANGE: LOG(worker, "Invalid range"); case ENOMEM: LOG(worker, "Out of memory error"); case EINVAL: LOG(worker, "Invalid value"); default: LOG(worker, "Unknown error"); } } return return_code; }