Added support for JSON body processor

This commit is contained in:
Ulisses Albuquerque 2013-12-03 13:41:08 -08:00 committed by Felipe Zimmerle
parent 410aca9d78
commit c23097ce18
9 changed files with 421 additions and 7 deletions

View File

@ -12,6 +12,7 @@ mod_security2_la_SOURCES = acmp.c \
msc_crypt.c \ msc_crypt.c \
msc_geo.c \ msc_geo.c \
msc_gsb.c \ msc_gsb.c \
msc_json.c \
msc_logging.c \ msc_logging.c \
msc_lua.c \ msc_lua.c \
msc_multipart.c \ msc_multipart.c \

View File

@ -19,6 +19,7 @@
#include "modsecurity.h" #include "modsecurity.h"
#include "msc_parsers.h" #include "msc_parsers.h"
#include "msc_util.h" #include "msc_util.h"
#include "msc_json.h"
#include "msc_xml.h" #include "msc_xml.h"
#include "apr_version.h" #include "apr_version.h"
@ -255,6 +256,9 @@ static apr_status_t modsecurity_tx_cleanup(void *data) {
/* XML processor cleanup. */ /* XML processor cleanup. */
if (msr->xml != NULL) xml_cleanup(msr); 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? // TODO: Why do we ignore return code here?
modsecurity_request_body_clear(msr, &my_error_msg); modsecurity_request_body_clear(msr, &my_error_msg);
if (my_error_msg != NULL) { if (my_error_msg != NULL) {

View File

@ -38,6 +38,7 @@ typedef struct msc_parm msc_parm;
#include "msc_multipart.h" #include "msc_multipart.h"
#include "msc_pcre.h" #include "msc_pcre.h"
#include "msc_util.h" #include "msc_util.h"
#include "msc_json.h"
#include "msc_xml.h" #include "msc_xml.h"
#include "msc_geo.h" #include "msc_geo.h"
#include "msc_gsb.h" #include "msc_gsb.h"
@ -368,6 +369,7 @@ struct modsec_rec {
multipart_data *mpd; /* MULTIPART processor data structure */ multipart_data *mpd; /* MULTIPART processor data structure */
xml_data *xml; /* XML processor data structure */ xml_data *xml; /* XML processor data structure */
json_data *json; /* JSON processor data structure */
/* audit logging */ /* audit logging */
char *new_auditlog_boundary; char *new_auditlog_boundary;

View File

@ -1,9 +1,9 @@
MOD_SECURITY2 = mod_security2 apache2_config apache2_io apache2_util \ 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 \ 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 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_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 msc_geo.h msc_gsb.h msc_crypt.h msc_tree.h msc_unicode.h acmp.h utf8tables.h msc_lua.h

313
apache2/msc_json.c Normal file
View File

@ -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;
}

48
apache2/msc_json.h Normal file
View File

@ -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 <yajl/yajl_parse.h>
/* 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

View File

@ -127,6 +127,14 @@ apr_status_t modsecurity_request_body_start(modsec_rec *msr, char **error_msg) {
msr_log(msr, 2, "%s", *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) { else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) {
/* Do nothing, URLENCODED processor does not support streaming yet. */ /* Do nothing, URLENCODED processor does not support streaming yet. */
} }
@ -344,6 +352,18 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr,
msr_log(msr, 2, "%s", *error_msg); 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) { else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) {
/* Increase per-request data length counter. */ /* Increase per-request data length counter. */
msr->msc_reqbody_no_files_length += length; msr->msc_reqbody_no_files_length += length;
@ -601,6 +621,15 @@ apr_status_t modsecurity_request_body_end(modsec_rec *msr, char **error_msg) {
return -1; 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) { else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) {
return modsecurity_request_body_end_urlencoded(msr, error_msg); return modsecurity_request_body_end_urlencoded(msr, error_msg);
} }

View File

@ -698,6 +698,9 @@ if test "$build_mlogc" -ne 0; then
CHECK_CURL() CHECK_CURL()
fi fi
# Check for YAJL libs (for JSON body processor)
AC_SEARCH_LIBS([yajl_alloc], [yajl])
AC_CONFIG_FILES([Makefile]) AC_CONFIG_FILES([Makefile])
AC_CONFIG_FILES([tools/Makefile]) AC_CONFIG_FILES([tools/Makefile])
if test "$build_alp2" -ne 0; then if test "$build_alp2" -ne 0; then

View File

@ -23,6 +23,12 @@ SecRequestBodyAccess On
SecRule REQUEST_HEADERS:Content-Type "text/xml" \ SecRule REQUEST_HEADERS:Content-Type "text/xml" \
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=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 # 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 # file uploads then the value given on the first line has to be as large
@ -52,7 +58,7 @@ SecRequestBodyLimitAction Reject
# or log a high-severity alert (when deployed in detection-only mode). # or log a high-severity alert (when deployed in detection-only mode).
# #
SecRule REQBODY_ERROR "!@eq 0" \ 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 # 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 # request body. If the rule below proves to be too strict for your
@ -60,7 +66,7 @@ SecRule REQBODY_ERROR "!@eq 0" \
# _not_ to remove it altogether. # _not_ to remove it altogether.
# #
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
"id:'200002',phase:2,t:none,log,deny,status:400, \ "id:'200003',phase:2,t:none,log,deny,status:400, \
msg:'Multipart request body failed strict validation: \ msg:'Multipart request body failed strict validation: \
PE %{REQBODY_PROCESSOR_ERROR}, \ PE %{REQBODY_PROCESSOR_ERROR}, \
BQ %{MULTIPART_BOUNDARY_QUOTED}, \ BQ %{MULTIPART_BOUNDARY_QUOTED}, \
@ -78,7 +84,7 @@ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
# Did we see anything that might be a boundary? # Did we see anything that might be a boundary?
# #
SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \ SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \
"id:'200003',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
# PCRE Tuning # PCRE Tuning
# We want to avoid a potential RegEx DoS condition # We want to avoid a potential RegEx DoS condition
@ -92,7 +98,7 @@ SecPcreMatchLimitRecursion 1000
# MSC_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded. # MSC_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded.
# #
SecRule TX:/^MSC_/ "!@streq 0" \ 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 -------------------------------------------------- # -- Response body handling --------------------------------------------------
@ -211,4 +217,12 @@ SecCookieFormat 0
# these directives helps to reduce false positives and negatives. # these directives helps to reduce false positives and negatives.
# #
#SecUnicodeCodePage 20127 #SecUnicodeCodePage 20127
#SecUnicodeMapFile unicode.mapping #SecUnicodeMapFile unicode.mappinga
# Improve the quality of ModSecurity by sharing information about your
# current ModSecurity version and dependencies versions.
# The following information will be shared: ModSecurity version,
# Web Server version, APR version, PCRE version, Lua version, Libxml2
# version, Anonymous unique id for host.
SecStatusEngine On