diff --git a/headers/modsecurity/transaction.h b/headers/modsecurity/transaction.h index a3eb8a1b..a79891dc 100644 --- a/headers/modsecurity/transaction.h +++ b/headers/modsecurity/transaction.h @@ -74,6 +74,9 @@ class RuleMessage; namespace actions { class Action; } +namespace RequestBodyProcessor { +class XML; +} namespace operators { class Operator; } @@ -324,6 +327,8 @@ class Transaction { */ std::list m_matched; + RequestBodyProcessor::XML *m_xml; + private: std::string *m_ARGScombinedSizeStr; std::string *m_namesArgs; diff --git a/src/Makefile.am b/src/Makefile.am index 57113a2b..b1c4f4f9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -56,7 +56,8 @@ VARIABLES = \ variables/tx.cc \ variables/variable.cc \ variables/variations/count.cc \ - variables/variations/exclusion.cc + variables/variations/exclusion.cc \ + variables/xml.cc ACTIONS = \ @@ -179,6 +180,12 @@ COLLECTION = \ collection/backend/in_memory-per_process.cc +BODY_PROCESSORS = \ + request_body_processor/multipart.cc \ + request_body_processor/multipart_blob.cc \ + request_body_processor/xml.cc + + libmodsecurity_la_SOURCES = \ parser/seclang-parser.yy \ parser/seclang-scanner.ll \ @@ -196,10 +203,9 @@ libmodsecurity_la_SOURCES = \ debug_log_writer.cc \ debug_log_writer_agent.cc \ macro_expansion.cc \ - request_body_processor/multipart.cc \ - request_body_processor/multipart_blob.cc \ rule.cc \ unique_id.cc \ + ${BODY_PROCESSORS} \ ${ACTIONS} \ ${COLLECTION} \ ${OPERATORS} \ @@ -221,16 +227,18 @@ libmodsecurity_la_CPPFLAGS = \ $(GLOBAL_CPPFLAGS) \ $(MODSEC_NO_LOGS) \ $(YAJL_CFLAGS) \ - $(PCRE_CFLAGS) + $(PCRE_CFLAGS) \ + $(LIBXML2_CFLAGS) libmodsecurity_la_LIBADD = \ - $(GLOBAL_LDADD) \ - $(CURL_LDADD) \ - $(GEOIP_LDFLAGS) $(GEOIP_LDADD) \ - @LEXLIB@ \ - $(PCRE_LDADD) \ - $(YAJL_LDADD) \ - ../others/libinjection.la + $(GLOBAL_LDADD) \ + $(CURL_LDADD) \ + $(GEOIP_LDFLAGS) $(GEOIP_LDADD) \ + @LEXLIB@ \ + $(PCRE_LDADD) \ + $(YAJL_LDADD) \ + $(LIBXML2_LDADD) \ + ../others/libinjection.la libmodsecurity_la_LDFLAGS = \ diff --git a/src/operators/validate_schema.cc b/src/operators/validate_schema.cc index eab4bd0d..106c18ce 100644 --- a/src/operators/validate_schema.cc +++ b/src/operators/validate_schema.cc @@ -18,26 +18,116 @@ #include #include "operators/operator.h" +#include "request_body_processor/xml.h" +#include "src/utils.h" + namespace modsecurity { namespace operators { -bool ValidateSchema::evaluate(Transaction *transaction, - const std::string &str) { - /** - * @todo Implement the operator ValidateSchema. - * Reference: https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual#validateSchema - */ +bool ValidateSchema::init(const std::string &file, const char **error) { + m_resource = find_resource(param, file); + if (m_resource == "") { + std::string f("XML: File not found: " + param + "."); + *error = strdup(f.c_str()); + return false; + } + + m_parserCtx = xmlSchemaNewParserCtxt(m_resource.c_str()); + if (m_parserCtx == NULL) { + std::stringstream err; + err << "XML: Failed to load Schema from file: "; + err << m_resource; + err << ". "; + if (m_err.empty() == false) { + err << m_err; + } + *error = strdup(err.str().c_str()); + return false; + } + + xmlSchemaSetParserErrors(m_parserCtx, + (xmlSchemaValidityErrorFunc)error_load, + (xmlSchemaValidityWarningFunc)warn_load, &m_err); + + xmlThrDefSetGenericErrorFunc(m_parserCtx, + null_error); + + xmlSetGenericErrorFunc(m_parserCtx, + null_error); + + m_schema = xmlSchemaParse(m_parserCtx); + if (m_schema == NULL) { + std::stringstream err; + err << "XML: Failed to load Schema: "; + err << m_resource; + err << "."; + if (m_err.empty() == false) { + err << " " << m_err; + } + *error = strdup(err.str().c_str()); + xmlSchemaFreeParserCtxt(m_parserCtx); + return false; + } + + m_validCtx = xmlSchemaNewValidCtxt(m_schema); + if (m_validCtx == NULL) { + std::stringstream err("XML: Failed to create validation context."); + if (m_err.empty() == false) { + err << " " << m_err; + } + *error = strdup(err.str().c_str()); + return false; + } + return true; } -ValidateSchema::ValidateSchema(std::string op, std::string param, - bool negation) - : Operator() { - this->op = op; - this->param = param; +bool ValidateSchema::evaluate(Transaction *t, + const std::string &str) { + int rc; + + /* Send validator errors/warnings to msr_log */ + xmlSchemaSetValidErrors(m_validCtx, + (xmlSchemaValidityErrorFunc)error_runtime, + (xmlSchemaValidityWarningFunc)warn_runtime, t); + + if (t->m_xml->m_data.doc == NULL) { + t->debug(4, "XML document tree could not be found for " \ + "schema validation."); + return true; + } + + if (t->m_xml->m_data.well_formed != 1) { + t->debug(4, "XML: Schema validation failed because " \ + "content is not well formed."); + return true; + } + + /* Make sure there were no other generic processing errors */ + /* + if (msr->msc_reqbody_error) { + t->debug(4, "XML: Schema validation could not proceed due to previous" + " processing errors."); + return true; + } + */ + + rc = xmlSchemaValidateDoc(m_validCtx, t->m_xml->m_data.doc); + if (rc != 0) { + t->debug(4, "XML: Schema validation failed."); + xmlSchemaFree(m_schema); + xmlSchemaFreeParserCtxt(m_parserCtx); + return true; /* No match. */ + } + + t->debug(4, "XML: Successfully validated payload against " \ + "Schema: " + m_resource); + + return false; } + } // namespace operators } // namespace modsecurity diff --git a/src/operators/validate_schema.h b/src/operators/validate_schema.h index 4c2eafce..be54af84 100644 --- a/src/operators/validate_schema.h +++ b/src/operators/validate_schema.h @@ -16,6 +16,12 @@ #ifndef SRC_OPERATORS_VALIDATE_SCHEMA_H_ #define SRC_OPERATORS_VALIDATE_SCHEMA_H_ +#include +#include +#include +#include +#include + #include #include "operators/operator.h" @@ -27,8 +33,100 @@ namespace operators { class ValidateSchema : public Operator { public: /** @ingroup ModSecurity_Operator */ - ValidateSchema(std::string o, std::string p, bool i); + ValidateSchema(std::string o, std::string p, bool i) + : Operator(o, p, i), + m_schema(NULL), + m_validCtx(NULL), + m_parserCtx(NULL) { } + ~ValidateSchema() { + /* + if (m_schema != NULL) { + xmlSchemaFree(m_schema); + m_schema = NULL; + } + */ + if (m_validCtx != NULL) { + xmlSchemaFreeValidCtxt(m_validCtx); + m_validCtx = NULL; + } + } + bool evaluate(Transaction *transaction, const std::string &str) override; + bool init(const std::string &file, const char **error) override; + + + static void error_load(void *ctx, const char *msg, ...) { + std::string *t = reinterpret_cast(ctx); + char buf[1024]; + va_list args; + + va_start(args, msg); + int len = vsnprintf(buf, sizeof(buf), msg, args); + va_end(args); + + if (len > 0) { + t->append("XML Error: " + std::string(buf)); + } + } + + + static void warn_load(void *ctx, const char *msg, ...) { + std::string *t = reinterpret_cast(ctx); + char buf[1024]; + va_list args; + + va_start(args, msg); + int len = vsnprintf(buf, sizeof(buf), msg, args); + va_end(args); + + if (len > 0) { + t->append("XML Warning: " + std::string(buf)); + } + } + + + static void error_runtime(void *ctx, const char *msg, ...) { + Transaction *t = reinterpret_cast(ctx); + char buf[1024]; + std::string s; + va_list args; + + va_start(args, msg); + int len = vsnprintf(buf, sizeof(buf), msg, args); + va_end(args); + + if (len > 0) { + s = "XML Error: " + std::string(buf); + } + t->debug(4, s); + } + + + static void warn_runtime(void *ctx, const char *msg, ...) { + Transaction *t = reinterpret_cast(ctx); + char buf[1024]; + std::string s; + va_list args; + + va_start(args, msg); + int len = vsnprintf(buf, sizeof(buf), msg, args); + va_end(args); + + if (len > 0) { + s = "XML Warning: " + std::string(buf); + } + t->debug(4, s); + } + + static void null_error(void *ctx, const char *msg, ...) { + } + + private: + xmlSchemaParserCtxtPtr m_parserCtx; + xmlSchemaValidCtxtPtr m_validCtx; + xmlSchemaPtr m_schema; + std::string m_resource; + std::string m_err; }; } // namespace operators diff --git a/src/parser/seclang-parser.yy b/src/parser/seclang-parser.yy index 11309764..e7036fe4 100644 --- a/src/parser/seclang-parser.yy +++ b/src/parser/seclang-parser.yy @@ -61,6 +61,7 @@ class Driver; #include "variables/time_wday.h" #include "variables/time_year.h" #include "variables/tx.h" +#include "variables/xml.h" using modsecurity::ModSecurity; @@ -103,6 +104,7 @@ using modsecurity::Variables::TimeWDay; using modsecurity::Variables::TimeYear; using modsecurity::Variables::Variable; using modsecurity::Variables::Tx; +using modsecurity::Variables::XML; #define CHECK_VARIATION_DECL \ @@ -229,6 +231,7 @@ using modsecurity::Variables::Tx; %token RUN_TIME_VAR_TIME_SEC %token RUN_TIME_VAR_TIME_WDAY %token RUN_TIME_VAR_TIME_YEAR +%token RUN_TIME_VAR_XML %token CONFIG_SEC_REMOTE_RULES_FAIL_ACTION @@ -816,6 +819,15 @@ var: if (!var) { var = new TimeYear(name); } $$ = var; } + | RUN_TIME_VAR_XML + { + std::string name($1); + CHECK_VARIATION_DECL + CHECK_VARIATION(&) { var = new Count(new XML(name)); } + CHECK_VARIATION(!) { var = new Exclusion(new XML(name)); } + if (!var) { var = new XML(name); } + $$ = var; + } ; act: diff --git a/src/parser/seclang-scanner.ll b/src/parser/seclang-scanner.ll index 4f2d1b53..dc444a78 100755 --- a/src/parser/seclang-scanner.ll +++ b/src/parser/seclang-scanner.ll @@ -117,7 +117,7 @@ TRANSFORMATION t:(?i:(cmdLine|sha1|hexEncode|lowercase|urlDecodeUni|urlDecode|n VARIABLE (?i:(RESOURCE|ARGS_COMBINED_SIZE|ARGS_GET_NAMES|ARGS_POST_NAMES|FILES_COMBINED_SIZE|FULL_REQUEST_LENGTH|REQUEST_BODY_LENGTH|REQUEST_URI_RAW|UNIQUE_ID|SERVER_PORT|SERVER_ADDR|REMOTE_PORT|REMOTE_HOST|MULTIPART_STRICT_ERROR|PATH_INFO|MULTIPART_CRLF_LF_LINES|MATCHED_VAR_NAME|MATCHED_VAR|INBOUND_DATA_ERROR|OUTBOUND_DATA_ERROR|FULL_REQUEST|AUTH_TYPE|ARGS_NAMES|REMOTE_ADDR|REQUEST_BASENAME|REQUEST_BODY|REQUEST_FILENAME|REQUEST_HEADERS_NAMES|REQUEST_METHOD|REQUEST_PROTOCOL|REQUEST_URI|RESPONSE_BODY|RESPONSE_CONTENT_LENGTH|RESPONSE_CONTENT_TYPE|RESPONSE_HEADERS_NAMES|RESPONSE_PROTOCOL|RESPONSE_STATUS|REQBODY_PROCESSOR|USERID|SESSIONID)) -VARIABLE_COL (?i:(SESSION|GLOBAL|ARGS_POST|ARGS_GET|ARGS|FILES_SIZES|FILES_NAMES|FILES_TMP_CONTENT|MULTIPART_FILENAME|MULTIPART_NAME|MATCHED_VARS_NAMES|MATCHED_VARS|FILES|QUERY_STRING|REQUEST_COOKIES|REQUEST_HEADERS|RESPONSE_HEADERS|GEO|IP|XML|REQUEST_COOKIES_NAMES)) +VARIABLE_COL (?i:(SESSION|GLOBAL|ARGS_POST|ARGS_GET|ARGS|FILES_SIZES|FILES_NAMES|FILES_TMP_CONTENT|MULTIPART_FILENAME|MULTIPART_NAME|MATCHED_VARS_NAMES|MATCHED_VARS|FILES|QUERY_STRING|REQUEST_COOKIES|REQUEST_HEADERS|RESPONSE_HEADERS|GEO|IP|REQUEST_COOKIES_NAMES)) VARIABLE_TX (?i:TX) VARIABLE_WEBSERVER_ERROR_LOG (?:WEBSERVER_ERROR_LOG) @@ -136,6 +136,7 @@ RUN_TIME_VAR_TIME_MON (?i:TIME_MON) RUN_TIME_VAR_TIME_SEC (?i:TIME_SEC) RUN_TIME_VAR_TIME_WDAY (?i:TIME_WDAY) RUN_TIME_VAR_TIME_YEAR (?i:TIME_YEAR) +RUN_TIME_VAR_XML (?i:XML) VARIABLENOCOLON (?i:REQBODY_ERROR|MULTIPART_STRICT_ERROR|MULTIPART_UNMATCHED_BOUNDARY|REMOTE_ADDR|REQUEST_LINE) @@ -227,6 +228,8 @@ CONFIG_DIR_UNICODE_MAP_FILE (?i:SecUnicodeMapFile) [!&]?{VARIABLE}(\:[\']{FREE_TEXT_QUOTE}[\'])? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE(yytext, *driver.loc.back()); } [!&]?{VARIABLE_COL}(\:{DICT_ELEMENT})? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE_COL(yytext, *driver.loc.back()); } [!&]?{VARIABLE_COL}(\:[\']{FREE_TEXT_QUOTE}[\'])? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE_COL(yytext, *driver.loc.back()); } +[!&]?{RUN_TIME_VAR_XML}(\:{DICT_ELEMENT})? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_RUN_TIME_VAR_XML(yytext, *driver.loc.back()); } +[!&]?{RUN_TIME_VAR_XML}(\:[\']{FREE_TEXT_QUOTE}[\'])? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_RUN_TIME_VAR_XML(yytext, *driver.loc.back()); } [!&]?{VARIABLE_TX}(\:{DICT_ELEMENT})? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE_TX(yytext, *driver.loc.back()); } [!&]?{VARIABLE_TX}(\:[\']{FREE_TEXT_QUOTE}[\'])? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE_TX(yytext, *driver.loc.back()); } [!&]?{RUN_TIME_VAR_DUR} { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_RUN_TIME_VAR_DUR(yytext, *driver.loc.back()); } @@ -243,6 +246,8 @@ CONFIG_DIR_UNICODE_MAP_FILE (?i:SecUnicodeMapFile) ["][!&]?{VARIABLE_TX}(\:[\']{FREE_TEXT_QUOTE}[\'])?["] { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE_TX(yytext, *driver.loc.back()); } ["][!&]?{VARIABLE_COL}(\:{DICT_ELEMENT})? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE_COL(yytext, *driver.loc.back()); } ["][!&]?{VARIABLE_COL}(\:[\']{FREE_TEXT_QUOTE}[\'])?["] { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_VARIABLE_COL(yytext, *driver.loc.back()); } +["][!&]?{RUN_TIME_VAR_XML}(\:{DICT_ELEMENT})? { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_RUN_TIME_VAR_XML(yytext, *driver.loc.back()); } +["][!&]?{RUN_TIME_VAR_XML}(\:[\']{FREE_TEXT_QUOTE}[\'])?["] { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_RUN_TIME_VAR_XML(yytext, *driver.loc.back()); } ["][!&]?{RUN_TIME_VAR_DUR}["] { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_RUN_TIME_VAR_DUR(yytext, *driver.loc.back()); } ["][!&]?{RUN_TIME_VAR_ENV}(\:{DICT_ELEMENT})?["] { BEGIN(EXPECTING_OPERATOR); return yy::seclang_parser::make_RUN_TIME_VAR_ENV(yytext, *driver.loc.back()); } diff --git a/src/request_body_processor/xml.cc b/src/request_body_processor/xml.cc new file mode 100644 index 00000000..d2d405d7 --- /dev/null +++ b/src/request_body_processor/xml.cc @@ -0,0 +1,122 @@ +/* + * 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/xml.h" + +#include +#include +#include + + +namespace modsecurity { +namespace RequestBodyProcessor { + + +XML::XML(Transaction *transaction) + : m_transaction(transaction) { + m_data.doc = NULL; + m_data.parsing_ctx = NULL; + m_data.sax_handler = NULL; +} + + +XML::~XML() { + if (m_data.doc != NULL) { + xmlFreeDoc(m_data.doc); + m_data.doc = NULL; + } +} + + +bool XML::init() { + // xmlParserInputBufferCreateFilenameFunc entity; + // entity = xmlParserInputBufferCreateFilenameDefault( + // this->unloadExternalEntity); + return true; +} + + +bool XML::processChunk(const char *buf, unsigned int size) { + /* We want to initialise our parsing context here, to + * enable us to pass it the first chunk of data so that + * it can attempt to auto-detect the encoding. + */ + if (m_data.parsing_ctx == NULL) { + /* First invocation. */ + + debug(4, "XML: Initialising parser."); + + /* NOTE When Sax interface is used libxml will not + * create the document object, but we need it. + + msr->xml->sax_handler = (xmlSAXHandler *)apr_pcalloc(msr->mp, + sizeof(xmlSAXHandler)); + if (msr->xml->sax_handler == NULL) return -1; + msr->xml->sax_handler->error = xml_receive_sax_error; + msr->xml->sax_handler->warning = xml_receive_sax_error; + msr->xml->parsing_ctx = xmlCreatePushParserCtxt(msr->xml->sax_handler, + msr, buf, size, "body.xml"); + + */ + + m_data.parsing_ctx = xmlCreatePushParserCtxt(NULL, NULL, + buf, size, "body.xml"); + + if (m_data.parsing_ctx == NULL) { + debug(4, "XML: Failed to create parsing context."); + return false; + } + return true; + } + + /* Not a first invocation. */ + xmlParseChunk(m_data.parsing_ctx, buf, size, 0); + if (m_data.parsing_ctx->wellFormed != 1) { + debug(4, "XML: Failed parsing document."); + return false; + } + + return true; +} + + +bool XML::complete() { + /* Only if we have a context, meaning we've done some work. */ + if (m_data.parsing_ctx != NULL) { + /* This is how we signalise the end of parsing to libxml. */ + xmlParseChunk(m_data.parsing_ctx, NULL, 0, 1); + + /* Preserve the results for our reference. */ + m_data.well_formed = m_data.parsing_ctx->wellFormed; + m_data.doc = m_data.parsing_ctx->myDoc; + + /* Clean up everything else. */ + xmlFreeParserCtxt(m_data.parsing_ctx); + m_data.parsing_ctx = NULL; + debug(4, "XML: Parsing complete (well_formed " \ + + std::to_string(m_data.well_formed) + ")."); + + if (m_data.well_formed != 1) { + debug(4, "XML: Failed parsing document."); + return false; + } + } + + return true; +} + + +} // namespace RequestBodyProcessor +} // namespace modsecurity diff --git a/src/request_body_processor/xml.h b/src/request_body_processor/xml.h new file mode 100644 index 00000000..3136966a --- /dev/null +++ b/src/request_body_processor/xml.h @@ -0,0 +1,68 @@ +/* + * 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 + +#include "modsecurity/transaction.h" + +#ifndef SRC_REQUEST_BODY_PROCESSOR_XML_H_ +#define SRC_REQUEST_BODY_PROCESSOR_XML_H_ + + +namespace modsecurity { +namespace RequestBodyProcessor { + + +struct xml_data { + xmlSAXHandler *sax_handler; + xmlParserCtxtPtr parsing_ctx; + xmlDocPtr doc; + + unsigned int well_formed; +}; + +typedef struct xml_data xml_data; + +class XML { + public: + explicit XML(Transaction *transaction); + ~XML(); + bool init(); + bool processChunk(const char *buf, unsigned int size); + bool complete(); + static xmlParserInputBufferPtr unloadExternalEntity(const char *URI, + xmlCharEncoding enc) { return NULL; } + +#ifndef NO_LOGS + void debug(int a, std::string str) { + m_transaction->debug(a, str); + } +#endif + xml_data m_data; + + private: + Transaction *m_transaction; + std::string m_header; +}; + +} // namespace RequestBodyProcessor +} // namespace modsecurity + +#endif // SRC_REQUEST_BODY_PROCESSOR_XML_H_ diff --git a/src/transaction.cc b/src/transaction.cc index 6a1926ca..9ef3871d 100644 --- a/src/transaction.cc +++ b/src/transaction.cc @@ -37,6 +37,7 @@ #include "modsecurity/intervention.h" #include "modsecurity/modsecurity.h" #include "request_body_processor/multipart.h" +#include "request_body_processor/xml.h" #include "audit_log/audit_log.h" #include "src/unique_id.h" #include "src/utils.h" @@ -44,6 +45,7 @@ using modsecurity::actions::Action; using modsecurity::RequestBodyProcessor::Multipart; +using modsecurity::RequestBodyProcessor::XML; namespace modsecurity { @@ -113,7 +115,8 @@ Transaction::Transaction(ModSecurity *ms, Rules *rules, void *logCbData) m_logCbData(logCbData), m_ms(ms), m_collections(ms->m_global_collection, ms->m_ip_collection, - ms->m_session_collection, ms->m_user_collection) { + ms->m_session_collection, ms->m_user_collection), + m_xml(new RequestBodyProcessor::XML(this)) { m_id = std::to_string(this->m_timeStamp) + \ std::to_string(generate_transaction_unique_id()); m_rules->incrementReferenceCount(); @@ -156,6 +159,8 @@ Transaction::~Transaction() { m_rulesMessages.clear(); m_rules->decrementReferenceCount(); + + delete m_xml; } @@ -478,6 +483,10 @@ int Transaction::addRequestHeader(const std::string& key, if (l == "application/x-www-form-urlencoded") { this->m_requestBodyType = WWWFormUrlEncoded; } + + if (l == "text/xml") { + this->m_requestBodyType = XMLRequestBody; + } } return 1; } @@ -581,6 +590,18 @@ int Transaction::processRequestBody() { * */ + if (m_requestBodyType == XMLRequestBody) { + std::string *a = m_collections.resolveFirst( + "REQUEST_HEADERS:Content-Type"); + if (a != NULL) { + if (m_xml->init() == true) { + m_xml->processChunk(m_requestBody.str().c_str(), + m_requestBody.str().size()); + m_xml->complete(); + } + } + } + if (m_requestBodyType == MultiPartRequestBody) { std::string *a = m_collections.resolveFirst( "REQUEST_HEADERS:Content-Type"); diff --git a/src/variables/xml.cc b/src/variables/xml.cc new file mode 100644 index 00000000..63518134 --- /dev/null +++ b/src/variables/xml.cc @@ -0,0 +1,138 @@ +/* + * 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 "variables/xml.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "modsecurity/transaction.h" + +#include "src/request_body_processor/xml.h" + +namespace modsecurity { +namespace Variables { + +void XML::evaluateInternal(Transaction *t, + std::vector *l) { + xmlXPathContextPtr xpathCtx; + xmlXPathObjectPtr xpathObj; + xmlNodeSetPtr nodes; + std::string key; + std::string param; + const xmlChar* xpathExpr = NULL; + int i; + size_t pos; + + pos = m_name.find_first_of(":"); + if (pos == std::string::npos) { + key = std::string(m_name, 0); + param = ""; + } else { + key = std::string(m_name, 0, pos); + param = std::string(m_name, pos+1, m_name.length() - (pos + 1)); + } + + /* Is there an XML document tree at all? */ + if (t->m_xml->m_data.doc == NULL) { + /* Sorry, we've got nothing to give! */ + return; + } + if (param.empty() == true) { + /* Invocation without an XPath expression makes sense + * with functions that manipulate the document tree. + */ + l->push_back(new collection::Variable("XML", + std::string("[XML document tree]" + param))); + return; + } + /* Process the XPath expression. */ + xpathExpr = (const xmlChar*)param.c_str(); + xpathCtx = xmlXPathNewContext(t->m_xml->m_data.doc); + if (xpathCtx == NULL) { + t->debug(1, "XML: Unable to create new XPath context."); + return; + } +#if 0 + /* Look through the actionset of the associated rule + * for the namespace information. Register them if any are found. + */ + tarr = apr_table_elts(rule->actionset->actions); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msre_action *action = (msre_action *)telts[i].val; + + if (strcasecmp(action->metadata->name, "xmlns") == 0) { + char *prefix, *href; + + if (parse_name_eq_value(mptmp, action->param, &prefix, &href) < 0) return -1; + if ((prefix == NULL)||(href == NULL)) return -1; + + if(xmlXPathRegisterNs(xpathCtx, (const xmlChar*)prefix, (const xmlChar*)href) != 0) { + msr_log(msr, 1, "Failed to register XML namespace href \"%s\" prefix \"%s\".", + log_escape(mptmp, prefix), log_escape(mptmp, href)); + return -1; + } + + msr_log(msr, 4, "Registered XML namespace href \"%s\" prefix \"%s\".", + log_escape(mptmp, prefix), log_escape(mptmp, href)); + } + } +#endif + + /* Initialise XPath expression. */ + xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx); + if (xpathObj == NULL) { + t->debug(1, "XML: Unable to evaluate xpath expression."); + xmlXPathFreeContext(xpathCtx); + return; + } + /* Evaluate XPath expression. */ + nodes = xpathObj->nodesetval; + if (nodes == NULL) { + xmlXPathFreeObject(xpathObj); + xmlXPathFreeContext(xpathCtx); + return; + } + /* Create one variable for each node in the result. */ + for (i = 0; i < nodes->nodeNr; i++) { + char *content = NULL; + content = reinterpret_cast( + xmlNodeGetContent(nodes->nodeTab[i])); + if (content != NULL) { + l->push_back(new collection::Variable(m_name, + std::string(content))); + xmlFree(content); + } + } + xmlXPathFreeObject(xpathObj); + xmlXPathFreeContext(xpathCtx); +} + + +} // namespace Variables +} // namespace modsecurity diff --git a/src/variables/xml.h b/src/variables/xml.h new file mode 100644 index 00000000..f7a75220 --- /dev/null +++ b/src/variables/xml.h @@ -0,0 +1,45 @@ +/* + * 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 +#include + +#ifndef SRC_VARIABLES_XML_H_ +#define SRC_VARIABLES_XML_H_ + +#include "variables/variable.h" +#include "src/variables/xml.h" + +namespace modsecurity { + +class Transaction; +namespace Variables { + +class XML : public Variable { + public: + explicit XML(std::string _name) + : Variable(_name) { } + + void evaluateInternal(Transaction *transaction, + std::vector *l) override; +}; + +} // namespace Variables +} // namespace modsecurity + +#endif // SRC_VARIABLES_XML_H_ diff --git a/test/Makefile.am b/test/Makefile.am index 23d1e664..6c5ab3b5 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -34,7 +34,9 @@ unit_tests_LDADD = \ $(CURL_LDADD) \ $(GEOIP_LDFLAGS) $(GEOIP_LDADD) \ $(PCRE_LDADD) \ - $(YAJL_LDFLAGS) $(YAJL_LDADD) + $(YAJL_LDFLAGS) $(YAJL_LDADD) \ + $(LIBXML2_LDADD) + unit_tests_CPPFLAGS = \ -std=c++11 \ @@ -48,7 +50,8 @@ unit_tests_CPPFLAGS = \ $(GEOIP_CFLAGS) \ $(GLOBAL_CPPFLAGS) \ $(PCRE_CFLAGS) \ - $(YAJL_CFLAGS) + $(YAJL_CFLAGS) \ + $(LIBXML2_CFLAGS) # regression @@ -65,7 +68,9 @@ regression_tests_LDADD = \ $(CURL_LDADD) \ $(GEOIP_LDFLAGS) $(GEOIP_LDADD) \ $(PCRE_LDADD) \ - $(YAJL_LDFLAGS) $(YAJL_LDADD) + $(YAJL_LDFLAGS) $(YAJL_LDADD) \ + $(LIBXML2_LDADD) + regression_tests_CPPFLAGS = \ -std=c++11 \ @@ -79,7 +84,8 @@ regression_tests_CPPFLAGS = \ $(GEOIP_CFLAGS) \ $(GLOBAL_CPPFLAGS) \ $(PCRE_CFLAGS) \ - $(YAJL_CFLAGS) + $(YAJL_CFLAGS) \ + $(LIBXML2_CFLAGS) # optimization @@ -95,7 +101,9 @@ rules_optimization_LDADD = \ $(CURL_LDADD) \ $(GEOIP_LDFLAGS) $(GEOIP_LDADD) \ $(PCRE_LDADD) \ - $(YAJL_LDFLAGS) $(YAJL_LDADD) + $(YAJL_LDFLAGS) $(YAJL_LDADD) \ + $(LIBXML2_LDADD) + rules_optimization_CPPFLAGS = \ -std=c++11 \ @@ -109,5 +117,6 @@ rules_optimization_CPPFLAGS = \ $(GEOIP_CFLAGS) \ $(GLOBAL_CPPFLAGS) \ $(PCRE_CFLAGS) \ - $(YAJL_CFLAGS) + $(YAJL_CFLAGS) \ + $(LIBXML2_CFLAGS) diff --git a/test/benchmark/Makefile.am b/test/benchmark/Makefile.am index eb2d6f15..83f04fa5 100644 --- a/test/benchmark/Makefile.am +++ b/test/benchmark/Makefile.am @@ -11,13 +11,15 @@ benchmark_LDADD = \ $(CURL_LDADD) \ $(GEOIP_LDFLAGS) $(GEOIP_LDADD) \ $(PCRE_LDADD) \ - $(YAJL_LDFLAGS) $(YAJL_LDADD) + $(YAJL_LDFLAGS) $(YAJL_LDADD) \ + $(LIBXML2_LDADD) benchmark_CPPFLAGS = \ -std=c++11 \ -I$(top_builddir)/headers \ $(GLOBAL_CPPFLAGS) \ - $(PCRE_CFLAGS) + $(PCRE_CFLAGS) \ + $(LIBXML2_CFLAGS) MAINTAINERCLEANFILES = \ diff --git a/test/test-cases/data/SoapEnvelope-bad.dtd b/test/test-cases/data/SoapEnvelope-bad.dtd new file mode 100644 index 00000000..7d6c19f4 --- /dev/null +++ b/test/test-cases/data/SoapEnvelope-bad.dtd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/test-cases/data/SoapEnvelope-bad.xsd b/test/test-cases/data/SoapEnvelope-bad.xsd new file mode 100644 index 00000000..2acfd1da --- /dev/null +++ b/test/test-cases/data/SoapEnvelope-bad.xsd @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prose in the spec does not specify that attributes are allowed on the Body element + + + + + + + + + + + + + + + + + + + + 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification + + + + + + + + + + + + + + + Fault reporting structure + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test-cases/data/SoapEnvelope.dtd b/test/test-cases/data/SoapEnvelope.dtd new file mode 100644 index 00000000..0ad4a8ab --- /dev/null +++ b/test/test-cases/data/SoapEnvelope.dtd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/test-cases/data/SoapEnvelope.xsd b/test/test-cases/data/SoapEnvelope.xsd new file mode 100644 index 00000000..2b4a8c06 --- /dev/null +++ b/test/test-cases/data/SoapEnvelope.xsd @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prose in the spec does not specify that attributes are allowed on the Body element + + + + + + + + + + + + + + + + + + + + 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification + + + + + + + + + + + + + + + Fault reporting structure + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test-cases/data/SoapEnvelope2.xsd b/test/test-cases/data/SoapEnvelope2.xsd new file mode 100644 index 00000000..2b4a8c06 --- /dev/null +++ b/test/test-cases/data/SoapEnvelope2.xsd @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Prose in the spec does not specify that attributes are allowed on the Body element + + + + + + + + + + + + + + + + + + + + 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification + + + + + + + + + + + + + + + Fault reporting structure + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test-cases/regression/request-body-parser-xml.json b/test/test-cases/regression/request-body-parser-xml.json new file mode 100644 index 00000000..824fa546 --- /dev/null +++ b/test/test-cases/regression/request-body-parser-xml.json @@ -0,0 +1,246 @@ +[ + { + "enabled":1, + "version_min":300000, + "title":"Testing XML request body parser (validate ok)", + "expected":{ + "debug_log": "XML: Successfully validated payload against Schema:" + }, + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*", + "Cookie": "PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Content-Type": "text/xml" + }, + "uri":"/?key=value&key=other_value", + "method":"POST", + "body": [ + "", + " ", + " ", + " ", + " 12123", + " ", + " ", + "" + ] + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "rules":[ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecRule REQUEST_HEADERS:Content-Type \"^text/xml$\" \"id:500005,phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML\"", + "SecRule XML \"@validateSchema test-cases/data/SoapEnvelope.xsd\" \"id:500007,phase:3,deny\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"Testing XML request body parser (validate attribute value failed)", + "expected":{ + "debug_log": "'badval' is not a valid value of the local atomic type", + "http_code": 403 + }, + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*", + "Cookie": "PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Content-Type": "text/xml" + }, + "uri":"/?key=value&key=other_value", + "method":"POST", + "body": [ + "", + " ", + " ", + " ", + " 12123", + " ", + " ", + " " + ] + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "rules":[ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecRule REQUEST_HEADERS:Content-Type \"^text/xml$\" \"id:500008,phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML\"", + "SecRule XML \"@validateSchema test-cases/data/SoapEnvelope.xsd\" \"id:500007,phase:3,deny\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"Testing XML request body parser (validate failed)", + "expected":{ + "debug_log": "This element is not expected. Expected is one of", + "http_code": 403 + }, + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*", + "Cookie": "PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Content-Type": "text/xml" + }, + "uri":"/?key=value&key=other_value", + "method":"POST", + "body": [ + "", + " ", + " ", + " ", + " 12123", + " ", + " ", + " " + ] + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "rules":[ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecRule REQUEST_HEADERS:Content-Type \"^text/xml$\" \"id:500008,phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML\"", + "SecRule XML \"@validateSchema test-cases/data/SoapEnvelope.xsd\" \"id:500007,phase:3,deny\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"Testing XML request body parser (bad XML)", + "expected":{ + "debug_log": "XML Error: Element '{http://schemas.xmlsoap.org/soap/envelope/}xBody'", + "http_code": 403 + }, + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*", + "Cookie": "PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Content-Type": "text/xml" + }, + "uri":"/?key=value&key=other_value", + "method":"POST", + "body": [ + "", + " ", + " ", + " ", + " 12123", + " ", + " ", + " " + ] + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "rules":[ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecRule REQUEST_HEADERS:Content-Type \"^text/xml$\" \"id:500008,phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML\"", + "SecRule XML \"@validateSchema test-cases/data/SoapEnvelope.xsd\" \"id:500007,phase:3,deny\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"Testing XML request body parser (bad schema)", + "expected":{ + "parser_error": " XML: Failed to load Schema: test-cases/data/SoapEnvelope-bad.xsd. XML Error: Failed to parse the XML resource 'test-cases/data/SoapEnvelope-bad.xsd" + }, + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*", + "Cookie": "PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Content-Type": "text/xml" + }, + "uri":"/?key=value&key=other_value", + "method":"POST", + "body": [ + "", + " ", + " ", + " ", + " 12123", + " ", + " ", + "" + ] + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "rules":[ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecRule REQUEST_HEADERS:Content-Type \"^text/xml$\" \"id:500008,phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML\"", + "SecRule XML \"@validateSchema test-cases/data/SoapEnvelope-bad.xsd\" \"id:500007,phase:3,deny\"" + ] + } +] +