diff --git a/Makefile.am b/Makefile.am index 1378ef0d..6241a7f4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -222,3 +222,5 @@ TESTS+=test/test-cases/regression/variable-STATUS.json TESTS+=test/test-cases/regression/variable-RESPONSE_PROTOCOL.json TESTS+=test/test-cases/regression/variable-SERVER_NAME.json TESTS+=test/test-cases/regression/operator-UnconditionalMatch.json +TESTS+=test/test-cases/regression/request-body-parser-json.json + diff --git a/headers/modsecurity/transaction.h b/headers/modsecurity/transaction.h index 1fbdfee8..40825b7a 100644 --- a/headers/modsecurity/transaction.h +++ b/headers/modsecurity/transaction.h @@ -76,6 +76,7 @@ class Action; } namespace RequestBodyProcessor { class XML; +class JSON; } namespace operators { class Operator; @@ -337,6 +338,7 @@ class Transaction { std::list m_matched; RequestBodyProcessor::XML *m_xml; + RequestBodyProcessor::JSON *m_json; private: std::string *m_ARGScombinedSizeStr; diff --git a/src/Makefile.am b/src/Makefile.am index 5989af04..66230603 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -83,6 +83,7 @@ ACTIONS = \ actions/capture.cc \ actions/chain.cc \ actions/ctl_audit_log_parts.cc \ + actions/ctl_request_body_processor_json.cc \ actions/ctl_request_body_processor_xml.cc \ actions/init_col.cc \ actions/deny.cc \ @@ -202,7 +203,8 @@ COLLECTION = \ BODY_PROCESSORS = \ request_body_processor/multipart.cc \ - request_body_processor/xml.cc + request_body_processor/xml.cc \ + request_body_processor/json.cc libmodsecurity_la_SOURCES = \ diff --git a/src/actions/ctl_request_body_processor_json.cc b/src/actions/ctl_request_body_processor_json.cc new file mode 100644 index 00000000..6cc9bc2f --- /dev/null +++ b/src/actions/ctl_request_body_processor_json.cc @@ -0,0 +1,37 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 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 "actions/ctl_request_body_processor_json.h" + +#include +#include + +#include "modsecurity/transaction.h" + +namespace modsecurity { +namespace actions { + + +bool CtlRequestBodyProcessorJSON::evaluate(Rule *rule, + Transaction *transaction) { + transaction->m_requestBodyProcessor = Transaction::JSONRequestBody; + transaction->m_collections.store("REQBODY_PROCESSOR", "JSON"); + + return true; +} + + +} // namespace actions +} // namespace modsecurity diff --git a/src/actions/ctl_request_body_processor_json.h b/src/actions/ctl_request_body_processor_json.h new file mode 100644 index 00000000..e3e0c5af --- /dev/null +++ b/src/actions/ctl_request_body_processor_json.h @@ -0,0 +1,39 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 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 + +#include "actions/action.h" +#include "modsecurity/transaction.h" + +#ifndef SRC_ACTIONS_CTL_REQUEST_BODY_PROCESSOR_JSON_H_ +#define SRC_ACTIONS_CTL_REQUEST_BODY_PROCESSOR_JSON_H_ + +namespace modsecurity { +namespace actions { + + +class CtlRequestBodyProcessorJSON : public Action { + public: + explicit CtlRequestBodyProcessorJSON(std::string action) + : Action(action, RunTimeOnlyIfMatchKind) { } + + bool evaluate(Rule *rule, Transaction *transaction) override; +}; + +} // namespace actions +} // namespace modsecurity + +#endif // SRC_ACTIONS_CTL_REQUEST_BODY_PROCESSOR_JSON_H_ diff --git a/src/parser/seclang-parser.yy b/src/parser/seclang-parser.yy index 858d9a4d..5ec24430 100644 --- a/src/parser/seclang-parser.yy +++ b/src/parser/seclang-parser.yy @@ -23,6 +23,7 @@ class Driver; #include "actions/action.h" #include "actions/audit_log.h" #include "actions/ctl_audit_log_parts.h" +#include "actions/ctl_request_body_processor_json.h" #include "actions/ctl_request_body_processor_xml.h" #include "actions/init_col.h" #include "actions/set_sid.h" @@ -73,6 +74,7 @@ using modsecurity::actions::Accuracy; using modsecurity::actions::Action; using modsecurity::actions::CtlAuditLogParts; using modsecurity::actions::CtlRequestBodyProcessorXML; +using modsecurity::actions::CtlRequestBodyProcessorJSON; using modsecurity::actions::InitCol; using modsecurity::actions::SetSID; using modsecurity::actions::SetUID; @@ -1184,8 +1186,7 @@ act: } | ACTION_CTL_BDY_JSON { - /* not ready yet. */ - $$ = Action::instantiate($1); + $$ = new modsecurity::actions::CtlRequestBodyProcessorJSON($1); } | ACTION_CTL_AUDIT_LOG_PARTS { diff --git a/src/request_body_processor/json.cc b/src/request_body_processor/json.cc new file mode 100644 index 00000000..3a45c55c --- /dev/null +++ b/src/request_body_processor/json.cc @@ -0,0 +1,275 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 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 "request_body_processor/json.h" + +#include +#include +#include + + +namespace modsecurity { +namespace RequestBodyProcessor { + +/** + * 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. + */ +int JSON::yajl_map_key(void *ctx, const unsigned char *key, size_t length) { + JSON *tthis = reinterpret_cast(ctx); + std::string safe_key; + + /** + * 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.assign((const char *)key, length); + + tthis->debug(9, "New JSON hash key '" + safe_key + "'"); + + /** + * TODO: How do we free the previously string value stored here? + */ + tthis->m_data.current_key = safe_key; + + return 1; +} + +/** + * Callback for null values + * + */ +int JSON::yajl_null(void *ctx) { + JSON *tthis = reinterpret_cast(ctx); + + return tthis->addArgument(""); +} + +/** + * Callback for boolean values + */ +int JSON::yajl_boolean(void *ctx, int value) { + JSON *tthis = reinterpret_cast(ctx); + + if (value) { + return tthis->addArgument("true"); + } + + return tthis->addArgument("false"); +} + +/** + * Callback for string values + */ +int JSON::yajl_string(void *ctx, const unsigned char *value, size_t length) { + JSON *tthis = reinterpret_cast(ctx); + std::string v = std::string((const char*)value, length); + + return tthis->addArgument(v); +} + +/** + * 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 + */ +int JSON::yajl_number(void *ctx, const char *value, size_t length) { + JSON *tthis = reinterpret_cast(ctx); + std::string v = std::string((const char*)value, length); + + return tthis->addArgument(v); +} + +/** + * Callback for a new hash, which indicates a new subtree, labeled as the + * current argument name, is being created + */ +int JSON::yajl_start_map(void *ctx) { + JSON *tthis = reinterpret_cast(ctx); + + /** + * If we do not have a current_key, this is a top-level hash, so we do not + * need to do anything + */ + if (tthis->m_data.current_key.empty() == true) { + return true; + } + + /** + * Check if we are already inside a hash context, and append or create the + * current key name accordingly + */ + if (tthis->m_data.prefix.empty() == false) { + tthis->m_data.prefix.append("." + tthis->m_data.current_key); + } else { + tthis->m_data.prefix.assign(tthis->m_data.current_key); + } + + tthis->debug(9, "New JSON hash context (prefix '" + \ + tthis->m_data.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 + */ +int JSON::yajl_end_map(void *ctx) { + JSON *tthis = reinterpret_cast(ctx); + size_t sep_pos = std::string::npos; + + /** + * If we have no prefix, then this is the end of a top-level hash and + * we don't do anything + */ + if (tthis->m_data.prefix.empty() == true) { + return true; + } + + /** + * Current prefix might or not include a separator character; top-level + * hash keys do not have separators in the variable name + */ + sep_pos = tthis->m_data.prefix.find("."); + + if (sep_pos != std::string::npos) { + std::string tmp = tthis->m_data.prefix; + tthis->m_data.prefix.assign(tmp, 0, sep_pos); + tthis->m_data.current_key.assign(tmp, sep_pos + 1, + tmp.length() - sep_pos - 1); + } else { + tthis->m_data.current_key.assign(tthis->m_data.prefix); + tthis->m_data.prefix = ""; + } + + return 1; +} + + +int JSON::addArgument(const std::string& value) { + /** + * If we do not have a prefix, we cannot create a variable name + * to reference this argument; for now we simply ignore these + */ + if (m_data.current_key.empty()) { + debug(3, "Cannot add scalar value without an associated key"); + return 1; + } + + if (m_data.prefix.empty()) { + m_transaction->addArgument("JSON", m_data.current_key, value); + } else { + m_transaction->addArgument("JSON", m_data.prefix + "." + \ + m_data.current_key, value); + } + + return 1; +} + + +bool JSON::init() { + return true; +} + + +bool JSON::processChunk(const char *buf, unsigned int size, std::string *err) { + /* Feed our parser and catch any errors */ + m_data.status = yajl_parse(m_data.handle, + (const unsigned char *)buf, size); + if (m_data.status != yajl_status_ok) { + const unsigned char *e = yajl_get_error(m_data.handle, 0, + (const unsigned char *)buf, size); + /* We need to free the yajl error message later, how to do this? */ + err->assign((const char *)e); + return false; + } + + return true; +} + + +bool JSON::complete(std::string *err) { + /* Wrap up the parsing process */ + m_data.status = yajl_complete_parse(m_data.handle); + if (m_data.status != yajl_status_ok) { + const unsigned char *e = yajl_get_error(m_data.handle, 0, NULL, 0); + /* We need to free the yajl error message later, how to do this? */ + err->assign((const char *)e); + return false; + } + + return true; +} + + +JSON::JSON(Transaction *transaction) : m_transaction(transaction) { + /** + * yajl configuration and callbacks + */ + 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 */ + }; + + + debug(4, "JSON parser initialization"); + + /** + * Prefix and current key are initially empty + */ + m_data.prefix = ""; + m_data.current_key = ""; + + /** + * yajl initialization + * + * yajl_parser_config definition: + * http://lloyd.github.io/yajl/yajl-2.0.1/yajl__parse_8h.html#aec816c5518264d2ac41c05469a0f986c + * + * TODO: make UTF8 validation optional, as it depends on Content-Encoding + */ + debug(9, "yajl JSON parsing callback initialization"); + m_data.handle = yajl_alloc(&callbacks, NULL, this); + + yajl_config(m_data.handle, yajl_allow_partial_values, 0); +} + + +JSON::~JSON() { + debug(4, "JSON: Cleaning up JSON results"); + yajl_free(m_data.handle); +} + + +} // namespace RequestBodyProcessor +} // namespace modsecurity diff --git a/src/request_body_processor/json.h b/src/request_body_processor/json.h new file mode 100644 index 00000000..b4f1aeac --- /dev/null +++ b/src/request_body_processor/json.h @@ -0,0 +1,84 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 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 + +#include +#include + +#include "modsecurity/transaction.h" +#include "modsecurity/rules.h" + +#ifndef SRC_REQUEST_BODY_PROCESSOR_JSON_H_ +#define SRC_REQUEST_BODY_PROCESSOR_JSON_H_ + + +namespace modsecurity { +namespace RequestBodyProcessor { + + +struct json_data { + /* yajl configuration and parser state */ + yajl_handle handle; + yajl_status status; + + /* prefix is used to create data hierarchy (i.e., 'parent.child.value') */ + std::string prefix; + std::string current_key; +}; + +typedef struct json_data json_data; + + +class JSON { + public: + explicit JSON(Transaction *transaction); + ~JSON(); + + bool init(); + bool processChunk(const char *buf, unsigned int size, std::string *err); + bool complete(std::string *err); + + int addArgument(const std::string& value); + + static int yajl_end_map(void *ctx); + static int yajl_start_map(void *ctx); + static int yajl_number(void *ctx, const char *value, size_t length); + static int yajl_string(void *ctx, const unsigned char *value, + size_t length); + static int yajl_boolean(void *ctx, int value); + static int yajl_null(void *ctx); + static int yajl_map_key(void *ctx, const unsigned char *key, + size_t length); + +#ifndef NO_LOGS + void debug(int a, std::string str) { + m_transaction->debug(a, str); + } +#endif + json_data m_data; + + private: + Transaction *m_transaction; + std::string m_header; +}; + + +} // namespace RequestBodyProcessor +} // namespace modsecurity + + +#endif // SRC_REQUEST_BODY_PROCESSOR_JSON_H_ diff --git a/src/transaction.cc b/src/transaction.cc index c0cb6b1f..3746aa11 100644 --- a/src/transaction.cc +++ b/src/transaction.cc @@ -38,6 +38,7 @@ #include "modsecurity/modsecurity.h" #include "request_body_processor/multipart.h" #include "request_body_processor/xml.h" +#include "request_body_processor/json.h" #include "audit_log/audit_log.h" #include "src/unique_id.h" #include "src/utils.h" @@ -118,6 +119,7 @@ Transaction::Transaction(ModSecurity *ms, Rules *rules, void *logCbData) m_collections(ms->m_global_collection, ms->m_ip_collection, ms->m_session_collection, ms->m_user_collection, ms->m_resource_collection), + m_json(new RequestBodyProcessor::JSON(this)), m_xml(new RequestBodyProcessor::XML(this)) { m_id = std::to_string(this->m_timeStamp) + \ std::to_string(generate_transaction_unique_id()); @@ -163,6 +165,7 @@ Transaction::~Transaction() { m_rules->decrementReferenceCount(); + delete m_json; delete m_xml; } @@ -629,7 +632,6 @@ int Transaction::processRequestBody() { * } * */ - if (m_requestBodyProcessor == XMLRequestBody) { std::string error; if (m_xml->init() == true) { @@ -649,6 +651,25 @@ int Transaction::processRequestBody() { m_collections.storeOrUpdateFirst("REQBODY_ERROR", "0"); m_collections.storeOrUpdateFirst("REQBODY_PROCESSOR_ERROR", "0"); } + } else if (m_requestBodyProcessor == JSONRequestBody) { + std::string error; + if (m_json->init() == true) { + m_json->processChunk(m_requestBody.str().c_str(), + m_requestBody.str().size(), + &error); + m_json->complete(&error); + } + if (error.empty() == false) { + m_collections.storeOrUpdateFirst("REQBODY_ERROR", "1"); + m_collections.storeOrUpdateFirst("REQBODY_PROCESSOR_ERROR", "1"); + m_collections.storeOrUpdateFirst("REQBODY_ERROR_MSG", + "XML parsing error: " + error); + m_collections.storeOrUpdateFirst("REQBODY_PROCESSOR_ERROR_MSG", + "XML parsing error: " + error); + } else { + m_collections.storeOrUpdateFirst("REQBODY_ERROR", "0"); + m_collections.storeOrUpdateFirst("REQBODY_PROCESSOR_ERROR", "0"); + } } else if (m_requestBodyType == MultiPartRequestBody) { std::string error; std::string *a = m_collections.resolveFirst( diff --git a/test/test-cases/regression/request-body-parser-json.json b/test/test-cases/regression/request-body-parser-json.json new file mode 100644 index 00000000..e7b8f0c0 --- /dev/null +++ b/test/test-cases/regression/request-body-parser-json.json @@ -0,0 +1,84 @@ +[ + { + "enabled":1, + "version_min":300000, + "title":"Testing JSON request body parser 1/1", + "expected":{ + "debug_log": "Target value: \"bar\" \\(Variable: ARGS:foo\\)" + }, + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*", + "Cookie": "PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Content-Type": "application/json" + }, + "uri":"/?key=value&key=other_value", + "method":"POST", + "body": [ + "{", + " \"foo\":\"bar\",", + " \"mod\":\"sec\"", + "}" + ] + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "rules":[ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecRule REQUEST_HEADERS:Content-Type \"application/json\" \"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON\"", + "SecRule ARGS:foo \"bar\" \"id:'200441',phase:3,log\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"Testing JSON request body parser 1/1", + "expected":{ + "debug_log": "Target value: \"bar\" \\(Variable: ARGS:first_level.first_key\\)" + }, + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*", + "Cookie": "PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Content-Type": "application/json" + }, + "uri":"/?key=value&key=other_value", + "method":"POST", + "body": [ + "{", + "\"first_level\":", + "{", + " \"first_key\":\"bar\",", + " \"second_key\":\"sec\"", + "}", + "}" + ] + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "rules":[ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecRule REQUEST_HEADERS:Content-Type \"application/json\" \"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON\"", + "SecRule ARGS \"bar\" \"id:'200441',phase:3,log\"" + ] + } +] +