diff --git a/apache2/Makefile.am b/apache2/Makefile.am index 38211f9b9d..cca50cd08f 100644 --- a/apache2/Makefile.am +++ b/apache2/Makefile.am @@ -8,7 +8,7 @@ pkglib_LTLIBRARIES = mod_security2.la mod_security2_la_SOURCES = mod_security2.c \ apache2_config.c apache2_io.c apache2_util.c \ re.c re_operators.c re_actions.c re_tfns.c \ - re_variables.c msc_logging.c msc_xml.c \ + re_variables.c msc_logging.c msc_xml.c msc_json.c \ msc_multipart.c modsecurity.c msc_parsers.c \ msc_util.c msc_pcre.c persist_dbm.c msc_reqbody.c \ msc_geo.c msc_gsb.c msc_crypt.c msc_tree.c msc_unicode.c acmp.c msc_lua.c msc_release.c diff --git a/apache2/modsecurity.c b/apache2/modsecurity.c index 0e6df481cb..0bdc3c4b03 100644 --- a/apache2/modsecurity.c +++ b/apache2/modsecurity.c @@ -19,6 +19,7 @@ #include "modsecurity.h" #include "msc_parsers.h" #include "msc_util.h" +#include "msc_json.h" #include "msc_xml.h" #include "apr_version.h" @@ -255,6 +256,9 @@ static apr_status_t modsecurity_tx_cleanup(void *data) { /* XML processor cleanup. */ if (msr->xml != NULL) xml_cleanup(msr); + /* JSON processor cleanup. */ + if (msr->json != NULL) json_cleanup(msr); + // TODO: Why do we ignore return code here? modsecurity_request_body_clear(msr, &my_error_msg); if (my_error_msg != NULL) { diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index 8eb8af88fc..afc7ed6528 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -38,6 +38,7 @@ typedef struct msc_parm msc_parm; #include "msc_multipart.h" #include "msc_pcre.h" #include "msc_util.h" +#include "msc_json.h" #include "msc_xml.h" #include "msc_geo.h" #include "msc_gsb.h" @@ -358,6 +359,7 @@ struct modsec_rec { multipart_data *mpd; /* MULTIPART processor data structure */ xml_data *xml; /* XML processor data structure */ + json_data *json; /* JSON processor data structure */ /* audit logging */ char *new_auditlog_boundary; diff --git a/apache2/modules.mk b/apache2/modules.mk index 3a8dd00adf..d4de03710b 100644 --- a/apache2/modules.mk +++ b/apache2/modules.mk @@ -1,9 +1,9 @@ MOD_SECURITY2 = mod_security2 apache2_config apache2_io apache2_util \ - re re_operators re_actions re_tfns re_variables \ + re re_operators re_actions re_tfns re_variables msc_json \ msc_logging msc_xml msc_multipart modsecurity msc_parsers msc_util msc_pcre \ persist_dbm msc_reqbody pdf_protect msc_geo msc_gsb msc_crypt msc_tree msc_unicode acmp msc_lua -H = re.h modsecurity.h msc_logging.h msc_multipart.h msc_parsers.h \ +H = re.h modsecurity.h msc_logging.h msc_multipart.h msc_parsers.h msc_json.h \ msc_pcre.h msc_util.h msc_xml.h persist_dbm.h apache2.h pdf_protect.h \ msc_geo.h msc_gsb.h msc_crypt.h msc_tree.h msc_unicode.h acmp.h utf8tables.h msc_lua.h diff --git a/apache2/msc_json.c b/apache2/msc_json.c new file mode 100755 index 0000000000..91763d759f --- /dev/null +++ b/apache2/msc_json.c @@ -0,0 +1,313 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2011 Trustwave Holdings, Inc. (http://www.trustwave.com/) + * + * You may not use this file except in compliance with + * the License.  You may obtain a copy of the License at + * + *     http://www.apache.org/licenses/LICENSE-2.0 + * + * If any of the files related to licensing are missing or if you have any + * other questions related to licensing please contact Trustwave Holdings, Inc. + * directly using the email address security@modsecurity.org. + */ + +#include "msc_json.h" + +int json_add_argument(modsec_rec *msr, const char *value, unsigned length) +{ + msc_arg *arg = (msc_arg *) NULL; + + /** + * If we do not have a prefix, we cannot create a variable name + * to reference this argument; for now we simply ignore these + */ + if (!msr->json->current_key) { + msr_log(msr, 3, "Cannot add scalar value without an associated key"); + return 1; + } + + arg = (msc_arg *) apr_pcalloc(msr->mp, sizeof(msc_arg)); + + /** + * Argument name is 'prefix + current_key' + */ + if (msr->json->prefix) { + arg->name = apr_psprintf(msr->mp, "%s.%s", msr->json->prefix, + msr->json->current_key); + } + else { + arg->name = apr_psprintf(msr->mp, "%s", msr->json->current_key); + } + arg->name_len = strlen(arg->name); + + /** + * Argument value is copied from the provided string + */ + arg->value = apr_pstrmemdup(msr->mp, value, length); + arg->value_len = length; + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Adding JSON argument '%s' with value '%s'", + arg->name, arg->value); + } + + apr_table_addn(msr->arguments, + log_escape_nq_ex(msr->mp, arg->name, arg->name_len), (void *) arg); + + return 1; +} + +/** + * yajl callback functions + * For more information on the function signatures and order, check + * http://lloyd.github.com/yajl/yajl-1.0.12/structyajl__callbacks.html + */ + +/** + * Callback for hash key values; we use those to define the variable names + * under ARGS. Whenever we reach a new key, we update the current key value. + */ +static int yajl_map_key(void *ctx, const unsigned char *key, unsigned int length) +{ + modsec_rec *msr = (modsec_rec *) ctx; + unsigned char *safe_key = (unsigned char *) NULL; + + /** + * yajl does not provide us with null-terminated strings, but + * rather expects us to copy the data from the key up to the + * length informed; we create a standalone null-termined copy + * in safe_key + */ + safe_key = apr_pstrndup(msr->mp, key, length); + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "New JSON hash key '%s'", safe_key); + } + + /** + * TODO: How do we free the previously string value stored here? + */ + msr->json->current_key = safe_key; + + return 1; +} + +/** + * Callback for null values + * + * TODO: Is there a way to define true null parameter values instead of + * empty values? + */ +static int yajl_null(void *ctx) +{ + modsec_rec *msr = (modsec_rec *) ctx; + + return json_add_argument(msr, "", 0); +} + +/** + * Callback for boolean values + */ +static int yajl_boolean(void *ctx, int value) +{ + modsec_rec *msr = (modsec_rec *) ctx; + + if (value) { + return json_add_argument(msr, "true", strlen("true")); + } + else { + return json_add_argument(msr, "false", strlen("false")); + } +} + +/** + * Callback for string values + */ +static int yajl_string(void *ctx, const unsigned char *value, unsigned int length) +{ + modsec_rec *msr = (modsec_rec *) ctx; + + return json_add_argument(msr, value, length); +} + +/** + * Callback for numbers; YAJL can use separate callbacks for integers/longs and + * float/double values, but since we are not interested in using the numeric + * values here, we use a generic handler which uses numeric strings + */ +static int yajl_number(void *ctx, const unsigned char *value, unsigned int length) +{ + modsec_rec *msr = (modsec_rec *) ctx; + + return json_add_argument(msr, value, length); +} + +/** + * Callback for a new hash, which indicates a new subtree, labeled as the current + * argument name, is being created + */ +static int yajl_start_map(void *ctx) +{ + modsec_rec *msr = (modsec_rec *) ctx; + + /** + * If we do not have a current_key, this is a top-level hash, so we do not + * need to do anything + */ + if (!msr->json->current_key) return 1; + + /** + * Check if we are already inside a hash context, and append or create the + * current key name accordingly + */ + if (msr->json->prefix) { + msr->json->prefix = apr_psprintf(msr->mp, "%s.%s", msr->json->prefix, + msr->json->current_key); + } + else { + msr->json->prefix = apr_pstrdup(msr->mp, msr->json->current_key); + } + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "New JSON hash context (prefix '%s')", msr->json->prefix); + } + + return 1; +} + +/** + * Callback for end hash, meaning the current subtree is being closed, and that + * we should go back to the parent variable label + */ +static int yajl_end_map(void *ctx) +{ + modsec_rec *msr = (modsec_rec *) ctx; + unsigned char *separator = (unsigned char *) NULL; + + /** + * If we have no prefix, then this is the end of a top-level hash and + * we don't do anything + */ + if (msr->json->prefix == NULL) return 1; + + /** + * Current prefix might or not include a separator character; top-level + * hash keys do not have separators in the variable name + */ + separator = strrchr(msr->json->prefix, '.'); + + if (separator) { + msr->json->prefix = apr_pstrmemdup(msr->mp, msr->json->prefix, + separator - msr->json->prefix); + msr->json->current_key = apr_psprintf(msr->mp, "%s", separator + 1); + } + else { + /** + * TODO: Check if it is safe to do this kind of pointer tricks + */ + msr->json->current_key = msr->json->prefix; + msr->json->prefix = (unsigned char *) NULL; + } + + return 1; +} + +/** + * Initialise JSON parser. + */ +int json_init(modsec_rec *msr, char **error_msg) { + /** + * yajl configuration and callbacks + */ + static yajl_parser_config config = { 0, 1 }; + static yajl_callbacks callbacks = { + yajl_null, + yajl_boolean, + NULL /* yajl_integer */, + NULL /* yajl_double */, + yajl_number, + yajl_string, + yajl_start_map, + yajl_map_key, + yajl_end_map, + NULL /* yajl_start_array */, + NULL /* yajl_end_array */ + }; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + msr_log(msr, 4, "JSON parser initialization"); + msr->json = apr_pcalloc(msr->mp, sizeof(json_data)); + if (msr->json == NULL) return -1; + + /** + * Prefix and current key are initially empty + */ + msr->json->prefix = (unsigned char *) NULL; + msr->json->current_key = (unsigned char *) NULL; + + /** + * yajl initialization + * + * yajl_parser_config definition: + * http://lloyd.github.com/yajl/yajl-1.0.12/structyajl__parser__config.html + * + * TODO: make UTF8 validation optional, as it depends on Content-Encoding + */ + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "yajl JSON parsing callback initialization"); + } + msr->json->handle = yajl_alloc(&callbacks, &config, NULL, msr); + + return 1; +} + +/** + * Feed one chunk of data to the JSON parser. + */ +int json_process_chunk(modsec_rec *msr, const char *buf, unsigned int size, char **error_msg) { + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* Feed our parser and catch any errors */ + msr->json->status = yajl_parse(msr->json->handle, buf, size); + if (msr->json->status != yajl_status_ok && + msr->json->status != yajl_status_insufficient_data) { + /* We need to free the yajl error message later, how to do this? */ + *error_msg = yajl_get_error(msr->json->handle, 0, buf, size); + } + + return 1; +} + +/** + * Finalise JSON parsing. + */ +int json_complete(modsec_rec *msr, char **error_msg) { + char *json_data = (char *) NULL; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* Wrap up the parsing process */ + msr->json->status = yajl_parse_complete(msr->json->handle); + if (msr->json->status != yajl_status_ok && + msr->json->status != yajl_status_insufficient_data) { + /* We need to free the yajl error message later, how to do this? */ + *error_msg = yajl_get_error(msr->json->handle, 0, NULL, 0); + } + + return 1; +} + +/** + * Frees the resources used for XML parsing. + */ +apr_status_t json_cleanup(modsec_rec *msr) { + msr_log(msr, 4, "JSON: Cleaning up JSON results"); + + return 1; +} + diff --git a/apache2/msc_json.h b/apache2/msc_json.h new file mode 100644 index 0000000000..06fa6c82fe --- /dev/null +++ b/apache2/msc_json.h @@ -0,0 +1,48 @@ +/* +* ModSecurity for Apache 2.x, http://www.modsecurity.org/ +* Copyright (c) 2004-2011 Trustwave Holdings, Inc. (http://www.trustwave.com/) +* +* You may not use this file except in compliance with +* the License.  You may obtain a copy of the License at +* +*     http://www.apache.org/licenses/LICENSE-2.0 +* +* If any of the files related to licensing are missing or if you have any +* other questions related to licensing please contact Trustwave Holdings, Inc. +* directly using the email address security@modsecurity.org. +*/ + +#ifndef _MSC_JSON_H_ +#define _MSC_JSON_H_ + +typedef struct json_data json_data; + +#include "modsecurity.h" +#include + +/* Structures */ +struct json_data { + /* yajl configuration and parser state */ + yajl_handle handle; + yajl_status status; + + /* error reporting and JSON array flag */ + unsigned char *yajl_error; + + /* prefix is used to create data hierarchy (i.e., 'parent.child.value') */ + unsigned char *prefix; + unsigned char *current_key; +}; + +/* Functions */ + +int DSOLOCAL json_init(modsec_rec *msr, char **error_msg); + +int DSOLOCAL json_process(modsec_rec *msr, const char *buf, + unsigned int size, char **error_msg); + +int DSOLOCAL json_complete(modsec_rec *msr, char **error_msg); + +apr_status_t DSOLOCAL json_cleanup(modsec_rec *msr); + +#endif diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index b76056bad0..2a5c949f12 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -127,6 +127,14 @@ apr_status_t modsecurity_request_body_start(modsec_rec *msr, char **error_msg) { msr_log(msr, 2, "%s", *error_msg); } } + else if (strcmp(msr->msc_reqbody_processor, "JSON") == 0) { + if (json_init(msr, &my_error_msg) < 0) { + *error_msg = apr_psprintf(msr->mp, "JSON parsing error (init): %s", my_error_msg); + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + msr_log(msr, 2, "%s", *error_msg); + } + } else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { /* Do nothing, URLENCODED processor does not support streaming yet. */ } @@ -343,6 +351,18 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, msr_log(msr, 2, "%s", *error_msg); } } + else if (strcmp(msr->msc_reqbody_processor, "JSON") == 0) { + /* Increase per-request data length counter. */ + msr->msc_reqbody_no_files_length += length; + + /* Process data as XML. */ + if (json_process_chunk(msr, data, length, &my_error_msg) < 0) { + *error_msg = apr_psprintf(msr->mp, "JSON parsing error: %s", my_error_msg); + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = *error_msg; + msr_log(msr, 2, "%s", *error_msg); + } + } else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { /* Increase per-request data length counter. */ msr->msc_reqbody_no_files_length += length; @@ -600,6 +620,15 @@ apr_status_t modsecurity_request_body_end(modsec_rec *msr, char **error_msg) { return -1; } } + else if (strcmp(msr->msc_reqbody_processor, "JSON") == 0) { + if (json_complete(msr, &my_error_msg) < 0) { + *error_msg = apr_psprintf(msr->mp, "JSON parser error: %s", my_error_msg); + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = *error_msg; + msr_log(msr, 2, "%s", *error_msg); + return -1; + } + } else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { return modsecurity_request_body_end_urlencoded(msr, error_msg); } diff --git a/configure.ac b/configure.ac index d48813ca78..c4a0f180a2 100644 --- a/configure.ac +++ b/configure.ac @@ -681,6 +681,9 @@ if test "$build_mlogc" -ne 0; then CHECK_CURL() fi +# Check for YAJL libs (for JSON body processor) +AC_SEARCH_LIBS([yajl_alloc], [yajl]) + AC_CONFIG_FILES([Makefile]) AC_CONFIG_FILES([tools/Makefile]) if test "$build_alp2" -ne 0; then diff --git a/modsecurity.conf-recommended b/modsecurity.conf-recommended index 90670af3ba..18ef267d5f 100644 --- a/modsecurity.conf-recommended +++ b/modsecurity.conf-recommended @@ -22,6 +22,12 @@ SecRequestBodyAccess On SecRule REQUEST_HEADERS:Content-Type "text/xml" \ "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" +# Enable JSON request body parser. +# Initiate JSON Processor in case of JSON content-type; change accordingly +# if your application does not use 'application/json' +# +SecRule REQUEST_HEADERS:Content-Type "application/json" \ + "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" # Maximum request body size we will accept for buffering. If you support # file uploads then the value given on the first line has to be as large @@ -51,7 +57,7 @@ SecRequestBodyLimitAction Reject # or log a high-severity alert (when deployed in detection-only mode). # SecRule REQBODY_ERROR "!@eq 0" \ -"id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" +"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" # By default be strict with what we accept in the multipart/form-data # request body. If the rule below proves to be too strict for your @@ -59,7 +65,7 @@ SecRule REQBODY_ERROR "!@eq 0" \ # _not_ to remove it altogether. # SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ -"id:'200002',phase:2,t:none,log,deny,status:44, \ +"id:'200003',phase:2,t:none,log,deny,status:44, \ msg:'Multipart request body failed strict validation: \ PE %{REQBODY_PROCESSOR_ERROR}, \ BQ %{MULTIPART_BOUNDARY_QUOTED}, \ @@ -77,7 +83,7 @@ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" # Did we see anything that might be a boundary? # SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \ -"id:'200003',phase:2,t:none,log,deny,status:44,msg:'Multipart parser detected a possible unmatched boundary.'" +"id:'200004',phase:2,t:none,log,deny,status:44,msg:'Multipart parser detected a possible unmatched boundary.'" # PCRE Tuning # We want to avoid a potential RegEx DoS condition @@ -91,7 +97,7 @@ SecPcreMatchLimitRecursion 1000 # MSC_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded. # SecRule TX:/^MSC_/ "!@streq 0" \ - "id:'200004',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'" + "id:'200005',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'" # -- Response body handling -------------------------------------------------- diff --git a/standalone/Makefile.in b/standalone/Makefile.in index aa6e8acf7c..81e359380b 100644 --- a/standalone/Makefile.in +++ b/standalone/Makefile.in @@ -230,6 +230,7 @@ PCRE_CONFIG = @PCRE_CONFIG@ PCRE_CPPFLAGS = @PCRE_CPPFLAGS@ PCRE_LDADD = @PCRE_LDADD@ PCRE_LDFLAGS = @PCRE_LDFLAGS@ +PCRE_LD_PATH = @PCRE_LD_PATH@ PCRE_VERSION = @PCRE_VERSION@ PERL = @PERL@ PKG_CONFIG = @PKG_CONFIG@