From 010c18f63ffe6ac2dcf37a3979ed9b56ef274ba1 Mon Sep 17 00:00:00 2001 From: Felipe Zimmerle Date: Fri, 4 Sep 2015 10:55:20 -0300 Subject: [PATCH] Adds support to SecDefaultAction configuration directive --- headers/modsecurity/rules_properties.h | 5 +- src/actions/action.h | 1 + src/actions/phase.cc | 9 +- src/actions/phase.h | 1 + src/parser/seclang-parser.yy | 48 +++ src/parser/seclang-scanner.ll | 3 + src/rule.cc | 22 ++ src/rules.cc | 14 + src/utils.cc | 27 ++ src/utils.h | 1 + .../regression/config-secdefaultaction.json | 299 ++++++++++++++++++ 11 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 test/test-cases/regression/config-secdefaultaction.json diff --git a/headers/modsecurity/rules_properties.h b/headers/modsecurity/rules_properties.h index c50799a8..18df1405 100644 --- a/headers/modsecurity/rules_properties.h +++ b/headers/modsecurity/rules_properties.h @@ -33,6 +33,9 @@ namespace ModSecurity { class Rule; class AuditLog; +namespace actions { +class Action; +} namespace Parser { class Driver; } @@ -72,7 +75,7 @@ class RulesProperties { } std::vector rules[7]; // ModSecurity::Phases::NUMBER_OF_PHASES - + std::vector defaultActions[7]; // ModSecurity::Phases::NUMBER_OF_PHASES /** * diff --git a/src/actions/action.h b/src/actions/action.h index 47ae759c..a6cb387b 100644 --- a/src/actions/action.h +++ b/src/actions/action.h @@ -88,6 +88,7 @@ class Action { Assay *assay); virtual bool evaluate(Rule *rule, Assay *assay); virtual bool init(std::string *error) { return true; } + virtual bool isDisruptive() { return false; } static Action *instantiate(const std::string& name); diff --git a/src/actions/phase.cc b/src/actions/phase.cc index da0605fc..9c148ea4 100644 --- a/src/actions/phase.cc +++ b/src/actions/phase.cc @@ -27,7 +27,9 @@ namespace ModSecurity { namespace actions { Phase::Phase(std::string action) - : Action(action) { + : Action(action), + m_secRulesPhase(0), + phase(0) { this->action_kind = ConfigurationKind; std::string a = action; a.erase(0, 6); @@ -42,20 +44,25 @@ Phase::Phase(std::string action) this->phase = 0; if (tolower(a) == "request") { this->phase = this->phase + ModSecurity::Phases::RequestHeadersPhase; + m_secRulesPhase = 2; } if (tolower(a) == "response") { this->phase = this->phase + ModSecurity::Phases::ResponseBodyPhase; + m_secRulesPhase = 4; } if (tolower(a) == "logging") { this->phase = this->phase + ModSecurity::Phases::LoggingPhase; + m_secRulesPhase = 5; } } if (this->phase == 0) { /* Phase 0 is something new, we want to use as ConnectionPhase */ this->phase = ModSecurity::Phases::ConnectionPhase; + m_secRulesPhase = 2; } else { /* Otherwise we want to shift the rule to the correct phase */ + m_secRulesPhase = phase; this->phase = phase + ModSecurity::Phases::RequestHeadersPhase - 1; } } diff --git a/src/actions/phase.h b/src/actions/phase.h index e9ad640c..e7341556 100644 --- a/src/actions/phase.h +++ b/src/actions/phase.h @@ -36,6 +36,7 @@ class Phase : public Action { bool evaluate(Rule *rule, Assay *assay) override; int phase; + int m_secRulesPhase; }; } // namespace actions diff --git a/src/parser/seclang-parser.yy b/src/parser/seclang-parser.yy index 9894bb2a..0b609be6 100644 --- a/src/parser/seclang-parser.yy +++ b/src/parser/seclang-parser.yy @@ -16,20 +16,25 @@ class Driver; } } +#include "modsecurity/modsecurity.h" + #include "actions/action.h" #include "actions/audit_log.h" #include "actions/ctl_audit_log_parts.h" #include "actions/set_var.h" #include "actions/severity.h" #include "actions/msg.h" +#include "actions/phase.h" #include "actions/log_data.h" #include "actions/rev.h" #include "actions/tag.h" #include "actions/transformations/transformation.h" +#include "actions/transformations/none.h" #include "operators/operator.h" #include "rule.h" #include "utils/geo_lookup.h" #include "audit_log.h" +#include "utils.h" #include "variables/variations/count.h" #include "variables/variations/exclusion.h" @@ -47,6 +52,8 @@ class Driver; #include "variables/time_wday.h" #include "variables/time_year.h" +using ModSecurity::ModSecurity; + using ModSecurity::actions::Action; using ModSecurity::actions::CtlAuditLogParts; using ModSecurity::actions::SetVar; @@ -54,6 +61,8 @@ using ModSecurity::actions::Severity; using ModSecurity::actions::Tag; using ModSecurity::actions::Rev; using ModSecurity::actions::Msg; +using ModSecurity::actions::Phase; +using ModSecurity::actions::transformations::None; using ModSecurity::actions::LogData; using ModSecurity::actions::transformations::Transformation; using ModSecurity::operators::Operator; @@ -181,6 +190,8 @@ using ModSecurity::Variables::Variable; %token CONFIG_DIR_DEBUG_LOG %token CONFIG_DIR_DEBUG_LVL +%token CONFIG_DIR_SEC_DEFAULT_ACTION + %token VARIABLE %token RUN_TIME_VAR_DUR %token RUN_TIME_VAR_ENV @@ -345,6 +356,43 @@ expression: ); driver.addSecRule(rule); } + | CONFIG_DIR_SEC_DEFAULT_ACTION SPACE QUOTATION_MARK actions QUOTATION_MARK + { + std::vector *actions = $4; + std::vector checkedActions; + int definedPhase = -1; + int secRuleDefinedPhase = -1; + for (Action *a : *actions) { + Phase *phase = dynamic_cast(a); + if (phase != NULL) { + definedPhase = phase->phase; + secRuleDefinedPhase = phase->m_secRulesPhase; + } else if (a->action_kind == Action::RunTimeOnlyIfMatchKind || + a->action_kind == Action::RunTimeBeforeMatchAttemptKind) { + None *none = dynamic_cast(a); + if (none != NULL) { + driver.parserError << "The transformation none is not suitable to be part of the SecDefaultActions"; + YYERROR; + } + checkedActions.push_back(a); + } else { + driver.parserError << "The action '" << a->action << "' is not suitable to be part of the SecDefaultActions"; + YYERROR; + } + } + if (definedPhase == -1) { + definedPhase = ModSecurity::ModSecurity::Phases::RequestHeadersPhase; + } + + if (!driver.defaultActions[definedPhase].empty()) { + driver.parserError << "SecDefaultActions can only be placed once per phase and configuration context. Phase " << secRuleDefinedPhase << " was informed already."; + YYERROR; + } + + for (Action *a : checkedActions) { + driver.defaultActions[definedPhase].push_back(a); + } + } | CONFIG_DIR_RULE_ENG SPACE CONFIG_VALUE_OFF { driver.secRuleEngine = ModSecurity::Rules::DisabledRuleEngine; diff --git a/src/parser/seclang-scanner.ll b/src/parser/seclang-scanner.ll index ca5739fe..43186d57 100755 --- a/src/parser/seclang-scanner.ll +++ b/src/parser/seclang-scanner.ll @@ -38,6 +38,8 @@ ACTION_CTL_AUDIT_LOG_PARTS (?i:ctl:auditLogParts) DIRECTIVE (?i:SecRule) LOG_DATA (?i:logdata) +CONFIG_DIR_SEC_DEFAULT_ACTION (?i:SecDefaultAction) + CONFIG_DIR_PCRE_MATCH_LIMIT_RECURSION (?i:SecPcreMatchLimitRecursion) CONFIG_DIR_PCRE_MATCH_LIMIT (?i:SecPcreMatchLimit) CONGIG_DIR_RESPONSE_BODY_MP (?i:SecResponseBodyMimeType) @@ -241,6 +243,7 @@ CONFIG_DIR_UNICODE_MAP_FILE (?i:SecUnicodeMapFile) {CONFIG_VALUE_PROCESS_PARTIAL} { return yy::seclang_parser::make_CONFIG_VALUE_PROCESS_PARTIAL(yytext, *driver.loc.back()); } {CONFIG_VALUE_REJECT} { return yy::seclang_parser::make_CONFIG_VALUE_REJECT(yytext, *driver.loc.back()); } +{CONFIG_DIR_SEC_DEFAULT_ACTION} { return yy::seclang_parser::make_CONFIG_DIR_SEC_DEFAULT_ACTION(yytext, *driver.loc.back()); } { ["][^@]{FREE_TEXT}["] { BEGIN(INITIAL); return yy::seclang_parser::make_FREE_TEXT(yytext, *driver.loc.back()); } diff --git a/src/rule.cc b/src/rule.cc index 9710f390..b83919f6 100644 --- a/src/rule.cc +++ b/src/rule.cc @@ -152,6 +152,22 @@ bool Rule::evaluate(Assay *assay) { none++; } } + + // Check for transformations on the SecDefaultAction + // Notice that first we make sure that won't be a t:none + // on the target rule. + if (none == 0) { + for (Action *a : assay->m_rules->defaultActions[this->phase]) { + if (a->action_kind == actions::Action::RunTimeBeforeMatchAttemptKind) { + value = a->evaluate(value, assay); + assay->debug(9, "(SecDefaultAction) T (" + \ + std::to_string(transformations) + ") " + \ + a->name + ": \"" + value +"\""); + transformations++; + } + } + } + for (Action *a : this->actions_runtime_pre) { None *z = dynamic_cast(a); if (none == 0) { @@ -206,6 +222,12 @@ bool Rule::evaluate(Assay *assay) { assay->delete_variable("MATCHED_VARS_NAMES:" + v.first); } if (this->chained && chainResult == true || !this->chained) { + for (Action *a : assay->m_rules->defaultActions[this->phase]) { + if (a->action_kind == actions::Action::RunTimeOnlyIfMatchKind) { + assay->debug(4, "(SecDefaultAction) Running action: " + a->action); + a->evaluate(this, assay); + } + } for (Action *a : this->actions_runtime_pos) { assay->debug(4, "Running action: " + a->action); diff --git a/src/rules.cc b/src/rules.cc index f2f7c47c..71a78455 100644 --- a/src/rules.cc +++ b/src/rules.cc @@ -208,6 +208,20 @@ int Rules::merge(Driver *from) { this->requestBodyLimitAction = from->requestBodyLimitAction; this->responseBodyLimitAction = from->responseBodyLimitAction; + /* + * + * default Actions is something per configuration context, there is + * need to merge anything. + * + */ + for (int i = 0; i < ModSecurity::Phases::NUMBER_OF_PHASES; i++) { + std::vector actions = from->defaultActions[i]; + this->defaultActions[i].clear(); + for (int j = 0; j < actions.size(); j++) { + Action *action = actions[j]; + this->defaultActions[i].push_back(action); + } + } if (from->audit_log != NULL && this->audit_log != NULL) { this->audit_log->refCountDecreaseAndCheck(); diff --git a/src/utils.cc b/src/utils.cc index 84e7a194..fe351a2f 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -49,6 +49,33 @@ namespace ModSecurity { +std::string phase_name(int x) { + switch(x) { + case ModSecurity::Phases::ConnectionPhase: + return "Connection Phase"; + break; + case ModSecurity::Phases::UriPhase: + return "URI Phase"; + break; + case ModSecurity::Phases::RequestHeadersPhase: + return "Request Headers"; + break; + case ModSecurity::Phases::RequestBodyPhase: + return "Request Headers"; + break; + case ModSecurity::Phases::ResponseHeadersPhase: + return "Response Headers"; + break; + case ModSecurity::Phases::ResponseBodyPhase: + return "Reponse Body"; + break; + case ModSecurity::Phases::LoggingPhase: + return "Logging"; + break; + } + return "Phase '" + std::to_string(x) + "' is not known."; +} + std::vector split(std::string str, char delimiter) { std::vector internal; diff --git a/src/utils.h b/src/utils.h index b2f2125d..59c0fe24 100644 --- a/src/utils.h +++ b/src/utils.h @@ -44,6 +44,7 @@ namespace ModSecurity { std::string string_to_hex(const std::string& input); int urldecode_uni_nonstrict_inplace_ex(Assay *assay, unsigned char *input, int64_t input_len, int *changed); + std::string phase_name(int x); } // namespace ModSecurity #define SRC_UTILS_H_ diff --git a/test/test-cases/regression/config-secdefaultaction.json b/test/test-cases/regression/config-secdefaultaction.json new file mode 100644 index 00000000..b35bfeb3 --- /dev/null +++ b/test/test-cases/regression/config-secdefaultaction.json @@ -0,0 +1,299 @@ +[ + { + "enabled":1, + "version_min":300000, + "version_max":0, + "title":"Testing action :: SecDefaultAction: supporting transformation", + "client":{ + "ip":"200.249.12.31", + "port":2313 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "User-Agent":"Mozilla\/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko\/20091102 Firefox\/3.5.5 (.NET CLR 3.5.30729)", + "Accept":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8", + "Accept-Language":"en-us,en;q=0.5", + "Accept-Encoding":"gzip,deflate", + "Accept-Charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Keep-Alive":"300", + "Connection":"keep-alive", + "Cookie":"PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Pragma":"no-cache", + "Cache-Control":"no-cache" + }, + "uri":"\/test.pl?param1= test ¶m2=test2", + "protocol":"GET", + "http_version":1.1, + "body":"" + }, + "response":{ + "headers":{ + "Content-Type":"text\/xml; charset=utf-8\n\r", + "Content-Length":"length\n\r" + }, + "body":[ + "\n\r", + "\n\r", + " \n\r", + " \n\r", + " string<\/EnlightenResult>\n\r", + " <\/EnlightenResponse>\n\r", + " <\/soap:Body>\n\r", + "<\/soap:Envelope>\n\r" + ] + }, + "expected":{ + "audit_log":"", + "debug_log":"lowercase: \"300\"", + "error_log":"" + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecDefaultAction \"phase:2,t:lowercase\"", + "SecRule REQUEST_HEADERS \"@contains PHPSESSID\" \"phase:2,id:1,msg:'This is a test, %{REQUEST_HEADERS:Accept}%'\"", + "SecRule TX \"@contains to_test\" \"id:2,t:lowercase,t:none\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "version_max":0, + "title":"Testing action :: SecDefaultAction: supporting transformation + t:none", + "client":{ + "ip":"200.249.12.31", + "port":2313 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "User-Agent":"Mozilla\/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko\/20091102 Firefox\/3.5.5 (.NET CLR 3.5.30729)", + "Accept":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8", + "Accept-Language":"en-us,en;q=0.5", + "Accept-Encoding":"gzip,deflate", + "Accept-Charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Keep-Alive":"300", + "Connection":"keep-alive", + "Cookie":"PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Pragma":"no-cache", + "Cache-Control":"no-cache" + }, + "uri":"\/test.pl?param1= test ¶m2=test2", + "protocol":"GET", + "http_version":1.1, + "body":"" + }, + "response":{ + "headers":{ + "Content-Type":"text\/xml; charset=utf-8\n\r", + "Content-Length":"length\n\r" + }, + "body":[ + "\n\r", + "\n\r", + " \n\r", + " \n\r", + " string<\/EnlightenResult>\n\r", + " <\/EnlightenResponse>\n\r", + " <\/soap:Body>\n\r", + "<\/soap:Envelope>\n\r" + ] + }, + "expected":{ + "audit_log":"", + "debug_log":" Target value: \"PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120\" ", + "error_log":"" + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecDefaultAction \"phase:2,t:lowercase\"", + "SecRule REQUEST_HEADERS \"@contains PHPSESSID\" \"t:none,phase:2,id:1,msg:'This is a test, %{REQUEST_HEADERS:Accept}%'\"", + "SecRule TX \"@contains to_test\" \"id:2,t:lowercase,t:none\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "version_max":0, + "title":"Testing action :: SecDefaultAction: t:none", + "expected":{ + "parser_error":"The transformation none is not suitable to be part of the SecDefaultActions" + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecDefaultAction \"phase:2,t:none\"", + "SecRule REQUEST_HEADERS \"@contains PHPSESSID\" \"t:none,phase:2,id:1,msg:'This is a test, %{REQUEST_HEADERS:Accept}%'\"", + "SecRule TX \"@contains to_test\" \"id:2,t:lowercase,t:none\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "version_max":0, + "title":"Testing action :: SecDefaultAction: simple test", + "client":{ + "ip":"200.249.12.31", + "port":2313 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "User-Agent":"Mozilla\/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko\/20091102 Firefox\/3.5.5 (.NET CLR 3.5.30729)", + "Accept":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8", + "Accept-Language":"en-us,en;q=0.5", + "Accept-Encoding":"gzip,deflate", + "Accept-Charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Keep-Alive":"300", + "Connection":"keep-alive", + "Cookie":"PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Pragma":"no-cache", + "Cache-Control":"no-cache" + }, + "uri":"\/test.pl?param1= test ¶m2=test2", + "protocol":"GET", + "http_version":1.1, + "body":"" + }, + "response":{ + "headers":{ + "Content-Type":"text\/xml; charset=utf-8\n\r", + "Content-Length":"length\n\r" + }, + "body":[ + "\n\r", + "\n\r", + " \n\r", + " \n\r", + " string<\/EnlightenResult>\n\r", + " <\/EnlightenResponse>\n\r", + " <\/soap:Body>\n\r", + "<\/soap:Envelope>\n\r" + ] + }, + "expected":{ + "audit_log":"", + "debug_log":"Saving msg: This is a test, text\/html,application", + "error_log":"" + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecDefaultAction \"phase:2,log,auditlog,pass\"", + "SecRule REQUEST_HEADERS \"@contains PHPSESSID\" \"id:1,t:lowercase,t:none,msg:'This is a test, %{REQUEST_HEADERS:Accept}%'\"", + "SecRule TX \"@contains to_test\" \"id:2,t:lowercase,t:none\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "version_max":0, + "title":"Testing action :: SecDefaultAction: action not suitable", + "expected":{ + "parser_error":"The action 'id:1' is not suitable to be part of the SecDefaultActions" + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecDefaultAction \"phase:2,id:1,log,auditlog,pass,tag:'teste'\"", + + "SecRule REQUEST_HEADERS \"@contains PHPSESSID\" \"id:1,tag:'teste',t:lowercase,t:none,msg:'This is a test, %{REQUEST_HEADERS:Accept}%'\"", + "SecRule TX \"@contains to_test\" \"id:2,t:lowercase,t:none\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "version_max":0, + "title":"Testing action :: SecDefaultAction: twice", + "expected":{ + "parser_error":"SecDefaultActions can only be placed once per phase and configuration context. Phase 2 was informed already." + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecDefaultAction \"phase:2,log,auditlog,pass,tag:'teste'\"", + "SecDefaultAction \"phase:2,log,auditlog,pass,tag:'teste'\"", + "SecRule REQUEST_HEADERS \"@contains PHPSESSID\" \"id:1,tag:'teste',t:lowercase,t:none,msg:'This is a test, %{REQUEST_HEADERS:Accept}%'\"", + "SecRule TX \"@contains to_test\" \"id:2,t:lowercase,t:none\"" + ] + }, + { + "enabled":1, + "version_min":300000, + "version_max":0, + "title":"Testing action :: SecDefaultAction: status + redirect", + "client":{ + "ip":"200.249.12.31", + "port":2313 + }, + "server":{ + "ip":"200.249.12.31", + "port":80 + }, + "request":{ + "headers":{ + "User-Agent":"Mozilla\/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko\/20091102 Firefox\/3.5.5 (.NET CLR 3.5.30729)", + "Accept":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8", + "Accept-Language":"en-us,en;q=0.5", + "Accept-Encoding":"gzip,deflate", + "Accept-Charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Keep-Alive":"300", + "Connection":"keep-alive", + "Cookie":"PHPSESSID=rAAAAAAA2t5uvjq435r4q7ib3vtdjq120", + "Pragma":"no-cache", + "Cache-Control":"no-cache" + }, + "uri":"\/test.pl?param1= test ¶m2=test2", + "protocol":"GET", + "http_version":1.1, + "body":"" + }, + "response":{ + "headers":{ + "Content-Type":"text\/xml; charset=utf-8\n\r", + "Content-Length":"length\n\r" + }, + "body":[ + "\n\r", + "\n\r", + " \n\r", + " \n\r", + " string<\/EnlightenResult>\n\r", + " <\/EnlightenResponse>\n\r", + " <\/soap:Body>\n\r", + "<\/soap:Envelope>\n\r" + ] + }, + "expected":{ + "audit_log":"", + "debug_log":"Request was relevant to be saved.", + "http_code": 500 + }, + "rules":[ + "SecRuleEngine On", + "SecDebugLog \/tmp\/modsec_debug.log", + "SecDebugLogLevel 9", + "SecDefaultAction \"phase:2,log,auditlog,status:500\"", + "SecRule REQUEST_HEADERS \"@contains PHPSESSID\" \"phase:2,id:1,redirect:http://www.google.com\"", + "SecRule TX \"@contains to_test\" \"id:2,t:lowercase,t:none\"" + ] + } +] \ No newline at end of file