diff --git a/headers/modsecurity/rules.h b/headers/modsecurity/rules.h index 4da33362..9e45433a 100644 --- a/headers/modsecurity/rules.h +++ b/headers/modsecurity/rules.h @@ -74,6 +74,7 @@ class Rules { int merge(Rules *rules); int evaluate(int phase, Assay *assay); + std::string getParserError(); std::vector rules[7]; // Number of Phases. @@ -101,6 +102,18 @@ class Rules { DetectionOnlyRuleEngine }; + enum BodyLimitAction { + /** + * Process partial + * + */ + ProcessPartialBodyLimitAction, + /** + * Process partial + * + */ + RejectBodyLimitAction + }; static const char *ruleEngineStateString(RuleEngine i) { switch (i) { case DisabledRuleEngine: @@ -128,6 +141,11 @@ class Rules { int requestBodyLimit; int responseBodyLimit; + int requestBodyLimitAction; + int responseBodyLimitAction; + + std::string parserError; + AuditLog *audit_log; private: diff --git a/src/assay.cc b/src/assay.cc index 7a0fea52..ec425521 100644 --- a/src/assay.cc +++ b/src/assay.cc @@ -31,6 +31,7 @@ #include "modsecurity/modsecurity.h" #include "modsecurity/intervention.h" #include "actions/action.h" +#include "actions/block.h" #include "src/utils.h" #include "src/audit_log.h" #include "src/unique_id.h" @@ -613,6 +614,24 @@ int Assay::appendRequestBody(const unsigned char *buf, size_t len) { if (this->m_rules->requestBodyLimit > 0 && this->m_rules->requestBodyLimit < len + current_size) { store_variable("INBOUND_DATA_ERROR", "1"); + debug(5, "Rquest body is bigger than the maximum expected."); + if (this->m_rules->requestBodyLimitAction == + Rules::BodyLimitAction::ProcessPartialBodyLimitAction) { + size_t spaceLeft = this->m_rules->requestBodyLimit - current_size; + this->m_requestBody.write(reinterpret_cast(buf), + spaceLeft); + debug(5, "Request body limit is marked to process partial"); + return false; + } else { + if (this->m_rules->requestBodyLimitAction == + Rules::BodyLimitAction::RejectBodyLimitAction) { + debug(5, "Request body limit is marked to reject the " \ + "request"); + Action *a = new actions::Block("block"); + actions.push_back(a); + } + return true; + } } this->m_requestBody.write(reinterpret_cast(buf), len); @@ -778,23 +797,42 @@ int Assay::processResponseBody() { * * @returns If the operation was successful or not. * @retval true Operation was successful. - * @retval false Operation failed. + * @retval false Operation failed, process partial demanded. * */ int Assay::appendResponseBody(const unsigned char *buf, size_t len) { int current_size = this->m_responseBody.tellp(); - debug(9, "Appending response body: " + std::to_string(len) + " bytes. " \ - "Limit set to: " + std::to_string(this->m_rules->responseBodyLimit)); + debug(9, "Appending response body: " + std::to_string(len + current_size) + + " bytes. Limit set to: " + + std::to_string(this->m_rules->responseBodyLimit)); if (this->m_rules->responseBodyLimit > 0 && this->m_rules->responseBodyLimit < len + current_size) { store_variable("OUTBOUND_DATA_ERROR", "1"); + debug(5, "Response body is bigger than the maximum expected."); + if (this->m_rules->responseBodyLimitAction == + Rules::BodyLimitAction::ProcessPartialBodyLimitAction) { + size_t spaceLeft = this->m_rules->responseBodyLimit - current_size; + this->m_responseBody.write(reinterpret_cast(buf), + spaceLeft); + debug(5, "Response body limit is marked to process partial"); + return false; + } else { + if (this->m_rules->responseBodyLimitAction == + Rules::BodyLimitAction::RejectBodyLimitAction) { + debug(5, "Response body limit is marked to reject the " \ + "request"); + Action *a = new actions::Block("block"); + actions.push_back(a); + } + return true; + } } this->m_responseBody.write(reinterpret_cast(buf), len); - return 0; + return true; } diff --git a/src/parser/driver.cc b/src/parser/driver.cc index 7c9264bc..e6133af4 100644 --- a/src/parser/driver.cc +++ b/src/parser/driver.cc @@ -74,8 +74,7 @@ int Driver::parse(const std::string &f) { int res = parser.parse(); - if (this->audit_log->init() == false) - { + if (this->audit_log->init() == false) { return false; } @@ -98,11 +97,18 @@ int Driver::parseFile(const std::string &f) { } -void Driver::error(const yy::location& l, const std::string& m) { - std::cerr << l << ": " << m << std::endl; +void Driver::error(const yy::location& l, const std::string& m, + const std::string& c) { + if (parserError.tellp() == 0) { + parserError << "Parser error, "; + parserError << "Filename: " << file << ". "; + parserError << "Line: " << l.end.line << ". "; + parserError << "Column: " << l.end.column << ". "; + } + parserError << c; } -void Driver::error(const std::string& m) { - std::cerr << m << std::endl; +void Driver::parser_error(const yy::location& l, const std::string& m) { + parserError << ". " << m << "." << std::endl; } diff --git a/src/parser/driver.h b/src/parser/driver.h index b377b02f..8361c72c 100644 --- a/src/parser/driver.h +++ b/src/parser/driver.h @@ -78,7 +78,8 @@ class Driver { // Error handling. void error(const yy::location& l, const std::string& m); - void error(const std::string& m); + void parser_error(const yy::location& l, const std::string& m); + void error(const yy::location& l, const std::string& m, const std::string& c); std::vector rules[7]; // Number of Phases. @@ -89,9 +90,13 @@ class Driver { bool sec_response_body_access; int requestBodyLimit; int responseBodyLimit; + int requestBodyLimitAction; + int responseBodyLimitAction; std::string debug_log_path; std::list components; + std::ostringstream parserError; + std::ostringstream syntaxError; ModSecurity::AuditLog *audit_log; diff --git a/src/parser/seclang-parser.yy b/src/parser/seclang-parser.yy index ad087509..4bd66b2c 100644 --- a/src/parser/seclang-parser.yy +++ b/src/parser/seclang-parser.yy @@ -73,7 +73,6 @@ using ModSecurity::Variables::TimeYear; QUOTATION_MARK ")" SPACE PIPE - NEW_LINE UNKNOWN FREE_TEXT ; @@ -83,6 +82,8 @@ using ModSecurity::Variables::TimeYear; %token CONFIG_DIRECTIVE %token CONFIG_DIR_REQ_BODY_LIMIT %token CONFIG_DIR_RES_BODY_LIMIT +%token CONFIG_DIR_REQ_BODY_LIMIT_ACTION +%token CONFIG_DIR_RES_BODY_LIMIT_ACTION %token CONFIG_DIR_RULE_ENG %token CONFIG_DIR_REQ_BODY %token CONFIG_DIR_RES_BODY @@ -93,6 +94,8 @@ using ModSecurity::Variables::TimeYear; %token CONFIG_VALUE_SERIAL %token CONFIG_VALUE_PARALLEL %token CONFIG_VALUE_RELEVANT_ONLY +%token CONFIG_VALUE_PROCESS_PARTIAL +%token CONFIG_VALUE_REJECT %token CONFIG_DIR_AUDIT_DIR %token CONFIG_DIR_AUDIT_DIR_MOD @@ -146,11 +149,12 @@ secrule: | secrule line line: - expression NEW_LINE - | SPACE expression NEW_LINE - | NEW_LINE - | SPACE NEW_LINE + expression + | SPACE expression | SPACE + { + + } audit_log: /* SecAuditLogDirMode */ @@ -280,6 +284,7 @@ expression: { GeoLookup::getInstance().setDataBase($1); } + /* Body limits */ | CONFIG_DIR_REQ_BODY_LIMIT { driver.requestBodyLimit = atoi($1.c_str()); @@ -288,6 +293,22 @@ expression: { driver.responseBodyLimit = atoi($1.c_str()); } + | CONFIG_DIR_REQ_BODY_LIMIT_ACTION SPACE CONFIG_VALUE_PROCESS_PARTIAL + { + driver.requestBodyLimitAction = ModSecurity::Rules::BodyLimitAction::ProcessPartialBodyLimitAction; + } + | CONFIG_DIR_REQ_BODY_LIMIT_ACTION SPACE CONFIG_VALUE_REJECT + { + driver.requestBodyLimitAction = ModSecurity::Rules::BodyLimitAction::RejectBodyLimitAction; + } + | CONFIG_DIR_RES_BODY_LIMIT_ACTION SPACE CONFIG_VALUE_PROCESS_PARTIAL + { + driver.responseBodyLimitAction = ModSecurity::Rules::BodyLimitAction::ProcessPartialBodyLimitAction; + } + | CONFIG_DIR_RES_BODY_LIMIT_ACTION SPACE CONFIG_VALUE_REJECT + { + driver.responseBodyLimitAction = ModSecurity::Rules::BodyLimitAction::RejectBodyLimitAction; + } variables: variables PIPE VARIABLE @@ -544,5 +565,5 @@ void yy::seclang_parser::error (const location_type& l, const std::string& m) { - driver.error (l, m); + driver.parser_error (l, m); } diff --git a/src/parser/seclang-scanner.ll b/src/parser/seclang-scanner.ll index 0123debd..bac2b8b1 100755 --- a/src/parser/seclang-scanner.ll +++ b/src/parser/seclang-scanner.ll @@ -17,14 +17,16 @@ static yy::location loc; %} %option noyywrap nounput batch debug noinput -ACTION (?i:accuracy|allow|append|auditlog|block|capture|chain|ctl|deny|deprecatevar|drop|exec|expirevar|id:[0-9]+|initcol|log|logdata|maturity|msg|multiMatch|noauditlog|nolog|pass|pause|phase:[0-9]+|prepend|proxy|redirect:[A-z0-9_\|\&\:\/\/\.]+|rev|sanitiseArg|sanitiseMatched|sanitiseMatchedBytes|sanitiseRequestHeader|sanitiseResponseHeader|setuid|setrsc|setsid|setenv|setvar|skip|skipAfter|status:[0-9]+|tag|ver|xmlns|t) +ACTION (?i:accuracy|allow|append|auditlog|block|capture|chain|ctl|deny|deprecatevar|drop|exec|expirevar|id:[0-9]+|initcol|log|logdata|maturity|msg|multiMatch|noauditlog|nolog|pass|pause|phase:[0-9]+|prepend|proxy|redirect:[A-z0-9_\|\&\:\/\/\.]+|rev|sanitiseArg|sanitiseMatched|sanitiseMatchedBytes|sanitiseRequestHeader|sanitiseResponseHeader|setuid|setrsc|setsid|setenv|setvar|skip|skipAfter|status:[0-9]+|tag|ver|xmlns) ACTION_SEVERITY (?i:severity:[0-9]+|severity:'[0-9]+'|severity:(EMERGENCY|ALERT|CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG)|severity:'(EMERGENCY|ALERT|CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG)') DIRECTIVE SecRule -CONFIG_DIRECTIVE SecRequestBodyLimitAction|SecRequestBodyNoFilesLimit|SecRequestBodyInMemoryLimit|SecPcreMatchLimitRecursion|SecPcreMatchLimit|SecResponseBodyMimeType|SecResponseBodyLimitAction|SecTmpDir|SecDataDir|SecArgumentSeparator|SecCookieFormat|SecStatusEngine +CONFIG_DIRECTIVE SecRequestBodyNoFilesLimit|SecRequestBodyInMemoryLimit|SecPcreMatchLimitRecursion|SecPcreMatchLimit|SecResponseBodyMimeType|SecTmpDir|SecDataDir|SecArgumentSeparator|SecCookieFormat|SecStatusEngine + CONFIG_DIR_REQ_BODY_LIMIT (?i:SecRequestBodyLimit) CONFIG_DIR_RES_BODY_LIMIT (?i:SecResponseBodyLimit) - +CONFIG_DIR_REQ_BODY_LIMIT_ACTION (?i:SecRequestBodyLimitAction) +CONFIG_DIR_RES_BODY_LIMIT_ACTION (?i:SecResponseBodyLimitAction) CONFIG_DIR_GEO_DB (?i:SecGeoLookupDb) @@ -85,6 +87,10 @@ CONFIG_VALUE_SERIAL Serial CONFIG_VALUE_PARALLEL Parallel CONFIG_VALUE_RELEVANT_ONLY RelevantOnly +CONFIG_VALUE_PROCESS_PARTIAL (?i:ProcessPartial) +CONFIG_VALUE_REJECT (?i:Reject) + + CONFIG_VALUE_PATH [A-Za-z_/\.]+ AUDIT_PARTS [ABCDEFHJKZ]+ CONFIG_VALUE_NUMBER [0-9]+ @@ -101,7 +107,7 @@ FREE_TEXT_NEW_LINE [^\"|\n]+ %{ // Code run each time yylex is called. - loc.step (); + loc.step(); %} {DIRECTIVE} { return yy::seclang_parser::make_DIRECTIVE(yytext, loc); } @@ -148,8 +154,10 @@ FREE_TEXT_NEW_LINE [^\"|\n]+ %{ /* Request body limit */ %} {CONFIG_DIR_REQ_BODY_LIMIT}[ ]{CONFIG_VALUE_NUMBER} { return yy::seclang_parser::make_CONFIG_DIR_REQ_BODY_LIMIT(strchr(yytext, ' ') + 1, loc); } +{CONFIG_DIR_REQ_BODY_LIMIT_ACTION} { return yy::seclang_parser::make_CONFIG_DIR_REQ_BODY_LIMIT_ACTION(yytext, loc); } %{ /* Reponse body limit */ %} {CONFIG_DIR_RES_BODY_LIMIT}[ ]{CONFIG_VALUE_NUMBER} { return yy::seclang_parser::make_CONFIG_DIR_RES_BODY_LIMIT(strchr(yytext, ' ') + 1, loc); } +{CONFIG_DIR_RES_BODY_LIMIT_ACTION} { return yy::seclang_parser::make_CONFIG_DIR_RES_BODY_LIMIT_ACTION(yytext, loc); } {CONFIG_COMPONENT_SIG}[ ]["]{FREE_TEXT}["] { return yy::seclang_parser::make_CONFIG_COMPONENT_SIG(strchr(yytext, ' ') + 2, loc); } @@ -159,6 +167,9 @@ FREE_TEXT_NEW_LINE [^\"|\n]+ {CONFIG_VALUE_PARALLEL} { return yy::seclang_parser::make_CONFIG_VALUE_PARALLEL(yytext, loc); } {CONFIG_VALUE_DETC} { return yy::seclang_parser::make_CONFIG_VALUE_DETC(yytext, loc); } {CONFIG_VALUE_RELEVANT_ONLY} { return yy::seclang_parser::make_CONFIG_VALUE_RELEVANT_ONLY(yytext, loc); } +{CONFIG_VALUE_PROCESS_PARTIAL} { return yy::seclang_parser::make_CONFIG_VALUE_PROCESS_PARTIAL(yytext, loc); } +{CONFIG_VALUE_REJECT} { return yy::seclang_parser::make_CONFIG_VALUE_REJECT(yytext, loc); } + ["]{OPERATOR}[ ]{FREE_TEXT}["] { return yy::seclang_parser::make_OPERATOR(yytext, loc); } ["]{OPERATORNOARG}["] { return yy::seclang_parser::make_OPERATOR(yytext, loc); } {ACTION} { return yy::seclang_parser::make_ACTION(yytext, loc); } @@ -168,8 +179,8 @@ FREE_TEXT_NEW_LINE [^\"|\n]+ [|] { return yy::seclang_parser::make_PIPE(loc); } {VARIABLENOCOLON} { return yy::seclang_parser::make_VARIABLE(yytext, loc); } [ \t]+ { return yy::seclang_parser::make_SPACE(loc); } -\n { return yy::seclang_parser::make_NEW_LINE(loc); } -. driver.error (loc, "invalid character"); +[\n]+ { loc.lines(yyleng); loc.step(); } +. { driver.error (loc, "invalid character", yytext); } <> { return yy::seclang_parser::make_END(loc); } %% diff --git a/src/rules.cc b/src/rules.cc index b3102842..43f7571d 100644 --- a/src/rules.cc +++ b/src/rules.cc @@ -124,6 +124,7 @@ int Rules::load(const char *plain_rules) { Driver *driver = new Driver(); if (driver->parse("/tmp/modsec_ugly_hack.txt")) { ret = false; + parserError = driver->parserError.str(); } this->merge(driver); delete driver; @@ -132,6 +133,11 @@ int Rules::load(const char *plain_rules) { } +std::string Rules::getParserError() { + return this->parserError; +} + + int Rules::evaluate(int phase, Assay *assay) { if (phase > ModSecurity::Phases::NUMBER_OF_PHASES) { return 0; @@ -169,6 +175,8 @@ int Rules::merge(Driver *from) { this->components = from->components; this->requestBodyLimit = from->requestBodyLimit; this->responseBodyLimit = from->responseBodyLimit; + this->requestBodyLimitAction = from->requestBodyLimitAction; + this->responseBodyLimitAction = from->responseBodyLimitAction; if (m_custom_debug_log) { this->debug_log = m_custom_debug_log->new_instance(); @@ -205,6 +213,8 @@ int Rules::merge(Rules *from) { this->components = from->components; this->requestBodyLimit = from->requestBodyLimit; this->responseBodyLimit = from->responseBodyLimit; + this->requestBodyLimitAction = from->requestBodyLimitAction; + this->responseBodyLimitAction = from->responseBodyLimitAction; this->debug_log = from->debug_log; diff --git a/test/test-cases/regression/config-body_limits.json b/test/test-cases/regression/config-body_limits.json new file mode 100644 index 00000000..c147581a --- /dev/null +++ b/test/test-cases/regression/config-body_limits.json @@ -0,0 +1,205 @@ +[ + { + "enabled":1, + "version_min":300000, + "title":"SecResponseBodyLimitAction Reject", + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*" + }, + "uri":"/?key=value&key=other_value", + "protocol":"GET" + }, + "response":{ + "headers":{ + "Date":"Mon, 13 Jul 2015 20:02:41 GMT", + "Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT", + "Content-Type":"text/html" + }, + "body":[ + "no need." + ] + }, + "expected":{ + "http_code":403 + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecResponseBodyLimitAction Reject", + "SecResponseBodyLimit 5" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"SecResponseBodyLimitAction ProcessPartial", + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*" + }, + "uri":"/?key=value&key=other_value", + "protocol":"GET" + }, + "response":{ + "headers":{ + "Date":"Mon, 13 Jul 2015 20:02:41 GMT", + "Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT", + "Content-Type":"text/html" + }, + "body":[ + "no need." + ] + }, + "expected":{ + "http_code":200 + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecResponseBodyLimitAction ProcessPartial", + "SecResponseBodyLimit 5" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"SecRequestBodyLimitAction Reject", + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*" + }, + "uri":"/?key=value&key=other_value", + "protocol":"POST", + "body":[ + "--------------------------756b6d74fa1a8ee2", + "Content-Disposition: form-data; name=\"name\"", + "", + "test", + "--------------------------756b6d74fa1a8ee2", + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"", + "Content-Type: text/plain", + "", + "This is a very small test file..", + "--------------------------756b6d74fa1a8ee2", + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"", + "Content-Type: text/plain", + "", + "This is another very small test file..", + "--------------------------756b6d74fa1a8ee2--" + ] + }, + "response":{ + "headers":{ + "Date":"Mon, 13 Jul 2015 20:02:41 GMT", + "Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT", + "Content-Type":"text/html" + }, + "body":[ + "no need." + ] + }, + "expected":{ + "http_code":403 + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecRequestBodyLimitAction Reject", + "SecRequestBodyLimit 5" + ] + }, + { + "enabled":1, + "version_min":300000, + "title":"SecRequestBodyLimitAction ProcessPartial", + "client":{ + "ip":"200.249.12.31", + "port":123 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "Host":"localhost", + "User-Agent":"curl/7.38.0", + "Accept":"*/*" + }, + "uri":"/?key=value&key=other_value", + "protocol":"POST", + "body":[ + "--------------------------756b6d74fa1a8ee2", + "Content-Disposition: form-data; name=\"name\"", + "", + "test", + "--------------------------756b6d74fa1a8ee2", + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"", + "Content-Type: text/plain", + "", + "This is a very small test file..", + "--------------------------756b6d74fa1a8ee2", + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"", + "Content-Type: text/plain", + "", + "This is another very small test file..", + "--------------------------756b6d74fa1a8ee2--" + ] + }, + "response":{ + "headers":{ + "Date":"Mon, 13 Jul 2015 20:02:41 GMT", + "Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT", + "Content-Type":"text/html" + }, + "body":[ + "no need." + ] + }, + "expected":{ + "http_code":200 + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecRequestBodyLimitAction ProcessPartial", + "SecRequestBodyLimit 5" + ] + } +] +