1503 lines
47 KiB
C

/*
* ModSecurity for Apache 2.x, http://www.modsecurity.org/
* Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com)
*
* $Id: re.c,v 1.15 2006/12/29 10:44:25 ivanr Exp $
*
* You should have received a copy of the licence along with this
* program (stored in the file "LICENSE"). If the file is missing,
* or if you have any other questions related to the licence, please
* write to Thinking Stone at contact@thinkingstone.com.
*
*/
#include <ctype.h>
#include "re.h"
/* -- Actions, variables, functions and operator functions ----------------- */
/**
* Creates msre_var instances (rule variables) out of the
* given text string and places them into the supplied table.
*/
apr_status_t msre_parse_targets(msre_ruleset *ruleset, const char *text,
apr_array_header_t *arr, char **error_msg)
{
const apr_array_header_t *tarr;
const apr_table_entry_t *telts;
apr_table_t *vartable;
unsigned int count = 0;
apr_status_t rc;
msre_var *var;
int i;
if (text == NULL) return -1;
/* Extract name & value pairs first */
vartable = apr_table_make(ruleset->mp, 10);
if (vartable == NULL) return -1;
rc = msre_parse_generic(ruleset->mp, text, vartable, error_msg);
if (rc < 0) return rc;
/* Loop through the table and create variables */
tarr = apr_table_elts(vartable);
telts = (const apr_table_entry_t*)tarr->elts;
for (i = 0; i < tarr->nelts; i++) {
var = msre_create_var(ruleset, telts[i].key, telts[i].val, NULL, error_msg);
if (var == NULL) return -1;
*(msre_var **)apr_array_push(arr) = var;
count++;
}
return count;
}
/**
* Creates msre_action instances by parsing the given string, placing
* them into the supplied array.
*/
apr_status_t msre_parse_actions(msre_engine *engine, msre_actionset *actionset,
const char *text, char **error_msg)
{
const apr_array_header_t *tarr;
const apr_table_entry_t *telts;
apr_table_t *vartable;
unsigned int count = 0;
apr_status_t rc;
msre_action *action;
int i;
if (text == NULL) return -1;
/* Extract name & value pairs first */
vartable = apr_table_make(engine->mp, 10);
if (vartable == NULL) return -1;
rc = msre_parse_generic(engine->mp, text, vartable, error_msg);
if (rc < 0) return rc;
/* Loop through the table and create actions */
tarr = apr_table_elts(vartable);
telts = (const apr_table_entry_t*)tarr->elts;
for (i = 0; i < tarr->nelts; i++) {
/* Create action. */
action = msre_create_action(engine, telts[i].key, telts[i].val, error_msg);
if (action == NULL) return -1;
/* Initialise action (option). */
if (action->metadata->init != NULL) {
action->metadata->init(engine, actionset, action);
}
if (action->metadata->cardinality == ACTION_CARDINALITY_ONE) {
/* One action per actionlist. */
apr_table_setn(actionset->actions, action->metadata->name, (void *)action);
} else {
/* Multiple actions per actionlist. */
apr_table_addn(actionset->actions, action->metadata->name, (void *)action);
}
count++;
}
return count;
}
/**
* Locates variable metadata given the variable name.
*/
msre_var_metadata *msre_resolve_var(msre_engine *engine, const char *name) {
return (msre_var_metadata *)apr_table_get(engine->variables, name);
}
/**
* Locates action metadata given the action name.
*/
msre_action_metadata *msre_resolve_action(msre_engine *engine, const char *name) {
return (msre_action_metadata *)apr_table_get(engine->actions, name);
}
/**
* Creates a new variable instance given the variable name
* and an (optional) parameter.
*/
msre_var *msre_create_var_ex(apr_pool_t *pool, msre_engine *engine, const char *name, const char *param,
modsec_rec *msr, char **error_msg)
{
msre_var *var = apr_pcalloc(pool, sizeof(msre_var));
if (var == NULL) return NULL;
if (error_msg == NULL) return NULL;
*error_msg = NULL;
/* Handle negation and member counting */
if (name[0] == '!') {
var->is_negated = 1;
var->name = name + 1;
}
else
if (name[0] == '&') {
var->is_counting = 1;
var->name = name + 1;
}
else {
var->name = name;
}
/* CGI HTTP variables emulation. */
if (strncasecmp(var->name, "HTTP_", 5) == 0) {
if (var->param != NULL) {
*error_msg = apr_psprintf(pool, "Variable %s does not support parameters.",
var->name);
return NULL;
}
var->param = var->name + 5;
var->name = "REQUEST_HEADERS";
}
/* Resolve variable */
var->metadata = msre_resolve_var(engine, var->name);
if (var->metadata == NULL) {
*error_msg = apr_psprintf(engine->mp, "Unknown variable: %s", name);
return NULL;
}
/* The counting operator "&" can only be used against collections. */
if (var->is_counting) {
if (var->metadata->type == VAR_SIMPLE) {
*error_msg = apr_psprintf(engine->mp, "The & modificator does not apply to "
"non-collection variables.");
return NULL;
}
}
/* Check the parameter. */
if (param == NULL) {
if (var->metadata->argc_min > 0) {
*error_msg = apr_psprintf(engine->mp, "Missing mandatory parameter for variable %s.",
name);
return NULL;
}
} else { /* Parameter present */
/* Do we allow a parameter? */
if (var->metadata->argc_max == 0) {
*error_msg = apr_psprintf(engine->mp, "Variable %s does not support parameters.",
name);
return NULL;
}
var->param = param;
}
return var;
}
/**
* Create a new variable object from the provided name and value.
*
* NOTE: this allocates out of the global pool and should not be used
* per-request
*/
msre_var *msre_create_var(msre_ruleset *ruleset, const char *name, const char *param,
modsec_rec *msr, char **error_msg)
{
msre_var *var = msre_create_var_ex(ruleset->engine->mp, ruleset->engine, name, param, msr, error_msg);
if (var == NULL) return NULL;
/* Validate & initialise variable */
if (var->metadata->validate != NULL) {
*error_msg = var->metadata->validate(ruleset, var);
if (*error_msg != NULL) {
/* ENH Shouldn't we log the problem? */
return NULL;
}
}
return var;
}
/**
* Creates a new action instance given its name and an (optional) parameter.
*/
msre_action *msre_create_action(msre_engine *engine, const char *name, const char *param,
char **error_msg)
{
msre_action *action = apr_pcalloc(engine->mp, sizeof(msre_action));
if (action == NULL) return NULL;
if (error_msg == NULL) return NULL;
*error_msg = NULL;
/* Resolve action */
action->metadata = msre_resolve_action(engine, name);
if (action->metadata == NULL) {
*error_msg = apr_psprintf(engine->mp, "Unknown action: %s", name);
return NULL;
}
if (param == NULL) { /* Parameter not present */
if (action->metadata->argc_min > 0) {
*error_msg = apr_psprintf(engine->mp, "Missing mandatory parameter for action %s",
name);
return NULL;
}
} else { /* Parameter present */
/* Should we allow the parameter? */
if (action->metadata->argc_max == 0) {
*error_msg = apr_psprintf(engine->mp, "Extra parameter provided to action %s", name);
return NULL;
}
/* Handle +/- modificators */
if ((param[0] == '+')||(param[0] == '-')) {
if (action->metadata->allow_param_plusminus == 0) {
*error_msg = apr_psprintf(engine->mp,
"Action %s does not allow +/- modificators.", name);
return NULL;
}
else { /* Modificators allowed. */
if (param[0] == '+') {
action->param = param + 1;
action->param_plusminus = POSITIVE_VALUE;
} else
if (param[0] == '-') {
action->param = param + 1;
action->param_plusminus = NEGATIVE_VALUE;
}
}
} else {
action->param = param;
}
/* Validate parameter */
if (action->metadata->validate != NULL) {
*error_msg = action->metadata->validate(engine, action);
if (*error_msg != NULL) return NULL;
}
}
return action;
}
/**
* Generic parser that is used as basis for target and action parsing.
* It breaks up the input string into name-parameter pairs and places
* them into the given table.
*/
int msre_parse_generic(apr_pool_t *mp, const char *text, apr_table_t *vartable,
char **error_msg)
{
char *p = (char *)text;
int count = 0;
if (error_msg == NULL) return -1;
*error_msg = NULL;
count = 0;
while(*p != '\0') {
char *name = NULL, *value = NULL;
/* ignore whitespace */
while(isspace(*p)) p++;
if (*p == '\0') return count;
/* we are at the beginning of the name */
name = p;
while((*p != '\0')&&(*p != '|')&&(*p != ':')&&(*p != ',')&&(!isspace(*p))) p++; // ENH replace with isvarnamechar()
/* get the name */
name = apr_pstrmemdup(mp, name, p - name);
if (*p != ':') { /* we don't have a parameter */
/* add to the table with no value */
apr_table_addn(vartable, name, NULL);
count++;
/* go over any whitespace present */
while(isspace(*p)) p++;
/* we're done */
if (*p == '\0') {
return count;
}
/* skip over the separator character and continue */
if ((*p == ',')||(*p == '|')) {
p++;
continue;
}
*error_msg = apr_psprintf(mp, "Unexpected character at position %i: %s",
(int)(p - text), text);
return -1;
}
/* we have a parameter */
p++; /* move over the colon */
/* we'll allow empty values */
if (*p == '\0') {
apr_table_addn(vartable, name, NULL);
count++;
return count;
}
if ((*p == ',')||(*p == '|')) {
apr_table_addn(vartable, name, NULL);
count++;
/* move over the separator char and continue */
p++;
continue;
}
/* we really have a parameter */
if (*p == '\'') { /* quoted value */
char *d = NULL;
p++; /* go over the openning quote */
value = d = strdup(p);
if (d == NULL) return -1;
for(;;) {
if (*p == '\0') {
*error_msg = apr_psprintf(mp, "Missing closing quote at position %i: %s",
(int)(p - text), text);
free(value);
return -1;
} else
if (*p == '\\') {
if ( (*(p + 1) == '\0') || ((*(p + 1) != '\'')&&(*(p + 1) != '\\')) ) {
*error_msg = apr_psprintf(mp, "Invalid quoted pair at position %i: %s",
(int)(p - text), text);
free(value);
return -1;
}
p++;
*d++ = *p++;
} else
if (*p == '\'') {
*d = '\0';
p++;
break;
}
else {
*d++ = *p++;
}
}
d = value;
value = apr_pstrdup(mp, d);
free(d);
} else { /* non-quoted value */
value = p;
while((*p != '\0')&&(*p != ',')&&(*p != '|')&&(!isspace(*p))) p++;
value = apr_pstrmemdup(mp, value, p - value);
}
/* add to table */
apr_table_addn(vartable, name, value);
count++;
/* move to the first character of the next name-value pair */
while(isspace(*p)||(*p == ',')||(*p == '|')) p++;
}
return count;
}
/* -- Actionset functions -------------------------------------------------- */
/**
* Creates an actionset instance and (as an option) populates it by
* parsing the given string which contains a list of actions.
*/
msre_actionset *msre_actionset_create(msre_engine *engine, const char *text,
char **error_msg)
{
msre_actionset *actionset = (msre_actionset *)apr_pcalloc(engine->mp,
sizeof(msre_actionset));
if (actionset == NULL) return NULL;
actionset->actions = apr_table_make(engine->mp, 25);
if (actionset->actions == NULL) return NULL;
/* Metadata */
actionset->id = NOT_SET_P;
actionset->rev = NOT_SET_P;
actionset->msg = NOT_SET_P;
actionset->phase = NOT_SET;
actionset->severity = -1;
/* Flow */
actionset->is_chained = NOT_SET;
actionset->skip_count = NOT_SET;
/* Disruptive */
actionset->intercept_action = NOT_SET;
actionset->intercept_uri = NOT_SET_P;
actionset->intercept_status = NOT_SET;
actionset->intercept_pause = NOT_SET;
/* Other */
actionset->auditlog = NOT_SET;
actionset->log = NOT_SET;
/* Parse the list of actions, if it's present */
if (text != NULL) {
if (msre_parse_actions(engine, actionset, text, error_msg) < 0) {
return NULL;
}
}
return actionset;
}
/**
* Create a (shallow) copy of the supplied actionset.
*/
static msre_actionset *msre_actionset_copy(apr_pool_t *mp, msre_actionset *orig) {
msre_actionset *copy = NULL;
if (orig == NULL) return NULL;
copy = (msre_actionset *)apr_pmemdup(mp, orig, sizeof(msre_actionset));
if (copy == NULL) return NULL;
copy->actions = apr_table_copy(mp, orig->actions);
return copy;
}
/**
* Merges two actionsets into one.
*/
msre_actionset *msre_actionset_merge(msre_engine *engine, msre_actionset *parent,
msre_actionset *child, int inherit_by_default)
{
msre_actionset *merged = NULL;
const apr_array_header_t *tarr;
const apr_table_entry_t *telts;
int i;
if (inherit_by_default == 0) {
/* There is nothing to merge in this case. */
return msre_actionset_copy(engine->mp, child);
}
/* Start with a copy of the parent configuration. */
merged = msre_actionset_copy(engine->mp, parent);
if (merged == NULL) return NULL;
if (child == NULL) {
/* The child actionset does not exist, hence
* go with the parent one.
*/
return merged;
}
/* First merge the hard-coded stuff. */
/* Metadata */
if (child->id != NOT_SET_P) merged->id = child->id;
if (child->rev != NOT_SET_P) merged->rev = child->rev;
if (child->msg != NOT_SET_P) merged->msg = child->msg;
if (child->severity != NOT_SET) merged->severity = child->severity;
if (child->phase != NOT_SET) merged->phase = child->phase;
/* Flow */
merged->is_chained = child->is_chained;
if (child->skip_count != NOT_SET) merged->skip_count = child->skip_count;
/* Disruptive */
if (child->intercept_action != NOT_SET) {
merged->intercept_action = child->intercept_action;
merged->intercept_uri = child->intercept_uri;
}
if (child->intercept_status != NOT_SET) merged->intercept_status = child->intercept_status;
if (child->intercept_pause != NOT_SET) merged->intercept_pause = child->intercept_pause;
/* Other */
if (child->auditlog != NOT_SET) merged->auditlog = child->auditlog;
if (child->log != NOT_SET) merged->log = child->log;
/* Now merge the actions. */
tarr = apr_table_elts(child->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 (action->metadata->cardinality == ACTION_CARDINALITY_ONE) {
apr_table_setn(merged->actions, action->metadata->name, (void *)action);
} else {
apr_table_addn(merged->actions, action->metadata->name, (void *)action);
}
}
return merged;
}
/**
* Creates an actionset that contains a default list of actions.
*/
msre_actionset *msre_actionset_create_default(msre_engine *engine) {
char *my_error_msg = NULL;
return msre_actionset_create(engine,
"log,auditlog,deny,status:403,phase:2,t:lowercase,t:replaceNulls,t:compressWhitespace",
&my_error_msg);
}
/**
* Sets the default values for the hard-coded actionset configuration.
*/
static void msre_actionset_set_defaults(msre_actionset *actionset) {
/* Metadata */
if (actionset->id == NOT_SET_P) actionset->id = NULL;
if (actionset->rev == NOT_SET_P) actionset->rev = NULL;
if (actionset->msg == NOT_SET_P) actionset->msg = NULL;
if (actionset->phase == NOT_SET) actionset->phase = 2;
if (actionset->severity == -1); /* leave at -1 */
/* Flow */
if (actionset->is_chained == NOT_SET) actionset->is_chained = 0;
if (actionset->skip_count == NOT_SET) actionset->skip_count = 0;
/* Disruptive */
if (actionset->intercept_action == NOT_SET) actionset->intercept_action = ACTION_NONE;
if (actionset->intercept_uri == NOT_SET_P) actionset->intercept_uri = NULL;
if (actionset->intercept_status == NOT_SET) actionset->intercept_status = 403;
if (actionset->intercept_pause == NOT_SET) actionset->intercept_pause = 0;
/* Other */
if (actionset->auditlog == NOT_SET) actionset->auditlog = 1;
if (actionset->log == NOT_SET) actionset->log = 1;
}
/* -- Engine functions ----------------------------------------------------- */
/**
* Creates a new engine instance.
*/
msre_engine *msre_engine_create(apr_pool_t *parent_pool) {
msre_engine *engine;
apr_pool_t *mp;
/* Create new memory pool */
if (apr_pool_create(&mp, parent_pool) != APR_SUCCESS) return NULL;
/* Init fields */
engine = apr_pcalloc(mp, sizeof(msre_engine));
if (engine == NULL) return NULL;
engine->mp = mp;
engine->tfns = apr_table_make(mp, 25);
if (engine->tfns == NULL) return NULL;
engine->operators = apr_table_make(mp, 25);
if (engine->operators == NULL) return NULL;
engine->variables = apr_table_make(mp, 25);
if (engine->variables == NULL) return NULL;
engine->actions = apr_table_make(mp, 25);
if (engine->actions == NULL) return NULL;
return engine;
}
/**
* Destroys an engine instance, releasing the consumed memory.
*/
void msre_engine_destroy(msre_engine *engine) {
/* Destroyed automatically by the parent pool.
* apr_pool_destroy(engine->mp);
*/
}
/* -- Recipe functions ----------------------------------------------------- */
#define NEXT_CHAIN 1
#define NEXT_RULE 2
#define SKIP_RULES 3
/**
* Default implementation of the ruleset phase processing; it processes
* the rules in the ruleset attached to the currently active
* transaction phase.
*/
apr_status_t msre_ruleset_process_phase(msre_ruleset *ruleset, modsec_rec *msr) {
apr_array_header_t *arr = NULL;
msre_rule **rules;
apr_status_t rc;
int i, mode, skip;
/* First determine which set of rules we need to use. */
switch (msr->phase) {
case PHASE_REQUEST_HEADERS :
arr = ruleset->phase_request_headers;
break;
case PHASE_REQUEST_BODY :
arr = ruleset->phase_request_body;
break;
case PHASE_RESPONSE_HEADERS :
arr = ruleset->phase_response_headers;
break;
case PHASE_RESPONSE_BODY :
arr = ruleset->phase_response_body;
break;
case PHASE_LOGGING :
arr = ruleset->phase_logging;
break;
default :
/* ENH Log a warning message here. */
return -1;
}
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "This phase consists of %i rule(s).", arr->nelts);
}
/* Loop through the rules in the selected set. */
skip = 0;
mode = NEXT_RULE;
rules = (msre_rule **)arr->elts;
for (i = 0; i < arr->nelts; i++) {
msre_rule *rule = rules[i];
/* NEXT_CHAIN is used when one of the rules in a chain
* fails to match and then we need to skip the remaining
* rules in that chain in order to get to the next
* rule that can execute.
*/
if (mode == NEXT_CHAIN) {
if (rule->actionset->is_chained == 0) {
mode = NEXT_RULE;
}
/* Go to the next rule. */
continue;
}
/* If we are here that means the mode is NEXT_RULE, which
* then means we have done processing any chains. However,
* if the "skip" parameter is set we need to skip over.
*/
if ((mode == NEXT_RULE)&&(skip > 0)) {
/* Decrement the skip counter by one. */
skip--;
/* If the current rule is part of a chain then
* we need to skip over the entire chain. Thus
* we change the mode to NEXT_CHAIN. The skip
* counter will not decrement as we are moving
* over the rules belonging to the chain.
*/
if (rule->actionset->is_chained) {
mode = NEXT_CHAIN;
}
/* Go to the next rule. */
continue;
}
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Recipe: Invoking rule %x.", rule);
}
rc = msre_rule_process(rule, msr);
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Rule returned %i.", rc);
}
if (rc == RULE_NO_MATCH) {
if (rule->actionset->is_chained) {
/* If the current rule is part of a chain then
* we need to skip over all the rules in the chain.
*/
mode = NEXT_CHAIN;
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "No match, chained -> mode NEXT_CHAIN.");
}
} else {
/* This rule is not part of a chain so we simply
* move to the next rule.
*/
mode = NEXT_RULE;
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "No match, not chained -> mode NEXT_RULE.");
}
}
}
else
if (rc == RULE_MATCH) {
if (msr->was_intercepted) {
/* If the transaction was intercepted we will
* go back. Do note that we are relying on the
* rule to know if it is a part of a chain and
* not intercept if it is.
*/
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "Match, intercepted -> returning.");
}
return 1;
}
/* We had a match but the transaction was not
* intercepted. In that case we proceed with the
* next rule...
*/
mode = NEXT_RULE;
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "Match -> mode NEXT_RULE.");
}
/* ...unless we need to skip, in which case we
* determine how many rules/chains we need to
* skip and configure the counter accordingly.
*/
if (rule->actionset->is_chained == 0) {
if (rule->chain_starter != NULL) {
if (rule->chain_starter->actionset->skip_count > 0) {
skip = rule->chain_starter->actionset->skip_count;
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Skipping %i rules/chains (from a chain).", skip);
}
}
}
else if (rule->actionset->skip_count > 0) {
skip = rule->actionset->skip_count;
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Skipping %i rules/chains.", skip);
}
}
}
}
else {
msr_log(msr, 1, "Unknown rule processing return code: %i.", rc);
return -1;
}
}
/* ENH warn if chained rules are missing. */
return 0;
}
/**
* Creates a ruleset that will be handled by the default
* implementation.
*/
msre_ruleset *msre_ruleset_create(msre_engine *engine, apr_pool_t *mp) {
msre_ruleset *ruleset;
ruleset = apr_pcalloc(mp, sizeof(msre_ruleset));
if (ruleset == NULL) return NULL;
ruleset->mp = mp;
ruleset->engine = engine;
ruleset->phase_request_headers = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *));
ruleset->phase_request_body = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *));
ruleset->phase_response_headers = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *));
ruleset->phase_response_body = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *));
ruleset->phase_logging = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *));
return ruleset;
}
/**
* Adds one rule to the given phase of the ruleset.
*/
int msre_ruleset_rule_add(msre_ruleset *ruleset, msre_rule *rule, int phase) {
apr_array_header_t *arr = NULL;
switch (phase) {
case PHASE_REQUEST_HEADERS :
arr = ruleset->phase_request_headers;
break;
case PHASE_REQUEST_BODY :
arr = ruleset->phase_request_body;
break;
case PHASE_RESPONSE_HEADERS :
arr = ruleset->phase_response_headers;
break;
case PHASE_RESPONSE_BODY :
arr = ruleset->phase_response_body;
break;
case PHASE_LOGGING :
arr = ruleset->phase_logging;
break;
default :
return -1;
}
/* ENH verify the rule's use of targets is consistent with
* the phase it selected to run at.
*/
msre_actionset_set_defaults(rule->actionset);
*(const msre_rule **)apr_array_push(arr) = rule;
return 1;
}
static int msre_ruleset_phase_rule_remove_with_exception(msre_ruleset *ruleset, rule_exception *re,
apr_array_header_t *phase_arr)
{
msre_rule **rules;
int i, j, mode, removed_count;
j = 0;
mode = 0;
removed_count = 0;
rules = (msre_rule **)phase_arr->elts;
for (i = 0; i < phase_arr->nelts; i++) {
msre_rule *rule = (msre_rule *)rules[i];
if (mode == 0) { /* Looking for next rule. */
int remove_rule = 0;
switch(re->type) {
case RULE_EXCEPTION_REMOVE_ID :
if ((rule->actionset != NULL)&&(rule->actionset->id != NULL)) {
int ruleid = atoi(rule->actionset->id);
if (rule_id_in_range(ruleid, re->param)) {
remove_rule = 1;
}
}
break;
case RULE_EXCEPTION_REMOVE_MSG :
if ((rule->actionset != NULL)&&(rule->actionset->msg != NULL)) {
char *my_error_msg = NULL;
int rc = msc_regexec(re->param_data,
rule->actionset->msg, strlen(rule->actionset->msg),
&my_error_msg);
if (rc >= 0) {
remove_rule = 1;
}
}
break;
}
if (remove_rule) {
/* Do not increment j. */
removed_count++;
if (rule->actionset->is_chained) mode = 2; /* Remove rules in this chain. */
} else {
if (rule->actionset->is_chained) mode = 1; /* Keep rules in this chain. */
rules[j++] = rules[i];
}
} else { /* Handling rule that is part of a chain. */
if (mode == 2) { /* We want to remove the rule. */
/* Do not increment j. */
removed_count++;
} else {
rules[j++] = rules[i];
}
if ((rule->actionset == NULL)||(rule->actionset->is_chained == 0)) mode = 0;
}
}
/* Update the number of rules in the array. */
phase_arr->nelts -= removed_count;
return 0;
}
/**
* Removes from the ruleset all rules that match the given exception.
*/
int msre_ruleset_rule_remove_with_exception(msre_ruleset *ruleset, rule_exception *re) {
int count = 0;
if (ruleset == NULL) return 0;
count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_request_headers);
count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_request_body);
count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_response_headers);
count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_response_body);
return count;
}
/* -- Rule functions ------------------------------------------------------- */
/**
* Returns the name of the supplied severity level.
*/
static const char *msre_format_severity(int severity) {
static const char *const severities[] = {
"EMERGENCY",
"ALERT",
"CRITICAL",
"ERROR",
"WARNING",
"NOTICE",
"INFO",
"DEBUG",
NULL,
};
if ((severity >= 0)&&(severity <= 7)) {
return severities[severity];
}
else {
return "(invalid value)";
}
}
/**
* Creates a string containing the metadata of the supplied rule.
*/
char *msre_format_metadata(modsec_rec *msr, msre_actionset *actionset) {
const apr_array_header_t *tarr;
const apr_table_entry_t *telts;
char *id = "";
char *rev = "";
char *msg = "";
char *severity = "";
char *tags = "";
int k;
if (actionset == NULL) return "";
if (actionset->id != NULL) id = apr_psprintf(msr->mp, " [id \"%s\"]",
log_escape(msr->mp, actionset->id));
if (actionset->rev != NULL) rev = apr_psprintf(msr->mp, " [rev \"%s\"]",
log_escape(msr->mp, actionset->rev));
if (actionset->msg != NULL) {
/* Expand variables in the message string. */
msc_string *var = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string));
var->value = (char *)actionset->msg;
var->value_len = strlen(actionset->msg);
expand_macros(msr, var, NULL, msr->mp);
msg = apr_psprintf(msr->mp, " [msg \"%s\"]",
log_escape_ex(msr->mp, var->value, var->value_len));
}
if ((actionset->severity >= 0)&&(actionset->severity <= 7)) {
severity = apr_psprintf(msr->mp, " [severity \"%s\"]",
msre_format_severity(actionset->severity));
}
/* Extract rule tags from the action list. */
tarr = apr_table_elts(actionset->actions);
telts = (const apr_table_entry_t*)tarr->elts;
for (k = 0; k < tarr->nelts; k++) {
msre_action *action = (msre_action *)telts[k].val;
if (strcmp(telts[k].key, "tag") == 0) {
tags = apr_psprintf(msr->mp, "%s [tag \"%s\"]", tags,
log_escape(msr->mp, action->param));
}
}
return apr_pstrcat(msr->mp, id, rev, msg, severity, tags, NULL);
}
/**
* Assembles a new rule using the strings that contain a list
* of targets (variables), argumments, and actions.
*/
msre_rule *msre_rule_create(msre_ruleset *ruleset, const char *targets,
const char *args, const char *actions, char **error_msg)
{
msre_rule *rule;
char *my_error_msg;
const char *argsp;
int rc;
if (error_msg == NULL) return NULL;
*error_msg = NULL;
rule = (msre_rule *)apr_pcalloc(ruleset->mp, sizeof(msre_rule));
if (rule == NULL) return NULL;
rule->ruleset = ruleset;
rule->targets = apr_array_make(ruleset->mp, 10, sizeof(const msre_var *));
/* Parse targets */
rc = msre_parse_targets(ruleset, targets, rule->targets, &my_error_msg);
if (rc < 0) {
*error_msg = apr_psprintf(ruleset->mp, "Error creating rule: %s", my_error_msg);
return NULL;
}
/* Parse args */
argsp = args;
/* Is negation used? */
if (*argsp == '!') {
rule->op_negated = 1;
argsp++;
while((isspace(*argsp))&&(*argsp != '\0')) argsp++;
}
/* Is the operator explicitly selected? */
if (*argsp != '@') {
/* Go with a regular expression. */
rule->op_name = "rx";
rule->op_param = argsp;
} else {
/* Explicitly selected operator. */
char *p = (char *)(argsp + 1);
while((!isspace(*p))&&(*p != '\0')) p++;
rule->op_name = apr_pstrmemdup(ruleset->mp, argsp + 1, p - (argsp + 1));
while(isspace(*p)) p++; /* skip over the whitespace at the end*/
rule->op_param = p; /* IMP1 So we always have a parameter even when it's empty? */
}
/* Find the operator. */
rule->op_metadata = msre_engine_op_resolve(ruleset->engine, rule->op_name);
if (rule->op_metadata == NULL) {
*error_msg = apr_psprintf(ruleset->mp,
"Error creating rule: Failed to resolve operator: %s", rule->op_name);
return NULL;
}
/* Initialise & validate parameter */
if (rule->op_metadata->param_init != NULL) {
if (rule->op_metadata->param_init(rule, &my_error_msg) <= 0) {
*error_msg = apr_psprintf(ruleset->mp, "Error creating rule: %s", my_error_msg);
return NULL;
}
}
/* Parse actions */
if (actions != NULL) {
/* Create per-rule actionset */
rule->actionset = msre_actionset_create(ruleset->engine, actions, &my_error_msg);
if (rule->actionset == NULL) {
*error_msg = apr_psprintf(ruleset->mp, "Error parsing actions: %s", my_error_msg);
return NULL;
}
}
return rule;
}
/**
* Perform non-disruptive actions associated with the provided actionset.
*/
static void msre_perform_nondisruptive_actions(modsec_rec *msr, msre_rule *rule,
msre_actionset *actionset, apr_pool_t *mptmp)
{
const apr_array_header_t *tarr;
const apr_table_entry_t *telts;
int i;
tarr = apr_table_elts(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 (action->metadata->type == ACTION_NON_DISRUPTIVE) {
if (action->metadata->execute != NULL) {
action->metadata->execute(msr, mptmp, rule, action);
}
}
}
}
/**
* Perform the disruptive actions associated with the given actionset.
*/
static void msre_perform_disruptive_actions(modsec_rec *msr, msre_rule *rule,
msre_actionset *actionset, apr_pool_t *mptmp, const char *message)
{
const apr_array_header_t *tarr;
const apr_table_entry_t *telts;
int i;
/* Execute the disruptive actions. Do note that this does
* not mean the request will be interrupted straight away. All
* disruptive actions need to do here is update the information
* that will be used to act later.
*/
tarr = apr_table_elts(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 (action->metadata->type == ACTION_DISRUPTIVE) {
if (action->metadata->execute != NULL) {
action->metadata->execute(msr, mptmp, rule, action);
}
}
}
/* If "noauditlog" was used do not mark the transaction for audit logging. */
if (actionset->auditlog == 1) {
msr->is_relevant++;
}
/* We only do stuff when in ONLINE mode. In all other
* cases we only emit warnings.
*/
if ((msr->phase == PHASE_LOGGING)
|| (msr->txcfg->is_enabled == MODSEC_DETECTION_ONLY)
|| (msr->modsecurity->processing_mode == MODSEC_OFFLINE)
|| (actionset->intercept_action == ACTION_NONE))
{
/* If "nolog" was used log at a higher level. */
msc_alert(msr, (actionset->log == 0 ? 4 : 2), actionset,
"Warning.", message);
return;
}
/* Signal to the engine we need to intercept this
* transaction, and rememer the rule that caused it.
*/
msr->was_intercepted = 1;
msr->intercept_phase = msr->phase;
msr->intercept_actionset = actionset;
msr->intercept_message = message;
}
/**
* Invokes the rule operator against the given value.
*/
static int execute_operator(msre_var *var, msre_rule *rule, modsec_rec *msr,
msre_actionset *acting_actionset, apr_pool_t *mptmp)
{
apr_time_t time_before_regex;
char *my_error_msg = NULL;
char *full_varname = NULL;
int rc;
/* determine the full var name if not already resolved
*
* NOTE: this can happen if the var does not match but it is
* being tested for non-existance as in:
* @REQUEST_HEADERS:Foo "@eq 0"
* @REQUEST_HEADERS:Foo "!@eq 1"
*/
if (var->param != NULL && var->name != NULL && strchr(var->name,':') == NULL) {
full_varname = apr_psprintf(mptmp, "%s%s:%s",
(var->is_counting ? "&" : ""),
var->name, var->param);
}
else if ((var->name != NULL) && var->is_counting && (*var->name != '&')) {
full_varname = apr_pstrcat(mptmp, "&", var->name);
}
else {
full_varname = var->name;
}
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Executing operator %s%s with param \"%s\" against %s.",
(rule->op_negated ? "!" : ""), rule->op_name,
log_escape(msr->mp, rule->op_param), full_varname);
}
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "Target value: %s", log_escape_nq_ex(msr->mp, var->value,
var->value_len));
}
time_before_regex = apr_time_now(); /* IMP1 time_before_regex? */
rc = rule->op_metadata->execute(msr, rule, var, &my_error_msg);
if (msr->txcfg->debuglog_level >= 4) {
msr_log(msr, 4, "Operator completed in %" APR_TIME_T_FMT " usec.",
(apr_time_now() - time_before_regex));
}
if (rc < 0) {
msr_log(msr, 4, "Operator error: %s", my_error_msg);
return -1;
}
if (((rc == 0)&&(rule->op_negated == 0))||((rc == 1)&&(rule->op_negated == 1))) {
/* No match, do nothing. */
return RULE_NO_MATCH;
}
else {
/* Match. */
if (rc == 0) {
/* Operator did not match so we need to provide a message. */
my_error_msg = apr_psprintf(msr->mp, "Match of \"%s %s\" against \"%s\" required.",
log_escape(msr->mp, rule->op_name), log_escape(msr->mp, rule->op_param),
log_escape(msr->mp, full_varname));
}
msr->matched_var = apr_pstrdup(msr->mp, var->name);
/* Perform non-disruptive actions. */
msre_perform_nondisruptive_actions(msr, rule, rule->actionset, mptmp);
/* Perform disruptive actions, but only if
* this rule is not part of a chain.
*/
if (rule->actionset->is_chained == 0) {
msre_perform_disruptive_actions(msr, rule, acting_actionset, mptmp, my_error_msg);
}
return RULE_MATCH;
}
}
/**
* Executes rule against the given transaction.
*/
apr_status_t msre_rule_process(msre_rule *rule, modsec_rec *msr) {
const apr_array_header_t *arr = NULL;
const apr_table_entry_t *te = NULL;
msre_actionset *acting_actionset = NULL;
msre_var **targets = NULL;
apr_pool_t *mptmp = NULL;
apr_table_t *tartab = NULL;
apr_table_t *vartab = NULL;
int i, rc, match_count = 0;
int invocations = 0;
int multi_match = 0;
/* Choose the correct metadata/disruptive action actionset. */
acting_actionset = rule->actionset;
if (rule->chain_starter != NULL) {
acting_actionset = rule->chain_starter->actionset;
}
/* Configure recursive matching. */
if (apr_table_get(rule->actionset->actions, "multiMatch") != NULL) {
multi_match = 1;
}
/* Create a memory pool that will be used during the
* processing of this rule only.
*/
/* IMP1 Why not have one pool and just clear it between rules? */
if (apr_pool_create(&mptmp, NULL) != APR_SUCCESS) {
return -1;
}
tartab = apr_table_make(mptmp, 24);
if (tartab == NULL) return -1;
vartab = apr_table_make(mptmp, 24);
if (vartab == NULL) return -1;
/* Expand variables to create a list of targets. */
targets = (msre_var **)rule->targets->elts;
for (i = 0; i < rule->targets->nelts; i++) {
int j, list_count;
apr_table_clear(vartab);
/* ENH Introduce a new variable hook that would allow the code
* behind the variable to return the size of the collection
* without having to generate the variables.
*/
/* Expand individual variables first. */
list_count = targets[i]->metadata->generate(msr, targets[i], rule, vartab, mptmp);
if (targets[i]->is_counting) {
/* Count how many there are and just add the score to the target list. */
msre_var *newvar = (msre_var *)apr_pmemdup(mptmp, targets[i], sizeof(msre_var));
newvar->value = apr_psprintf(mptmp, "%i", list_count);
newvar->value_len = strlen(newvar->value);
apr_table_addn(tartab, newvar->name, (void *)newvar);
} else {
/* And either add them or remove from the final target list. */
arr = apr_table_elts(vartab);
te = (apr_table_entry_t *)arr->elts;
for(j = 0; j < arr->nelts; j++) {
if (targets[i]->is_negated == 0) {
apr_table_addn(tartab, te[j].key, te[j].val);
} else {
apr_table_unset(tartab, te[j].key);
}
}
}
}
/* Loop through targets on the final target list,
* perform transformations as necessary, and invoke
* the operator.
*/
arr = apr_table_elts(tartab);
te = (apr_table_entry_t *)arr->elts;
for (i = 0; i < arr->nelts; i++) {
int changed;
/* Take one target. */
msre_var *var = (msre_var *)te[i].val;
/* Transform target. */
{
const apr_array_header_t *tarr;
const apr_table_entry_t *telts;
apr_table_t *normtab;
int k;
msre_action *action;
msre_tfn_metadata *metadata;
normtab = apr_table_make(mptmp, 10);
if (normtab == NULL) return -1;
tarr = apr_table_elts(rule->actionset->actions);
telts = (const apr_table_entry_t*)tarr->elts;
/* Build the final list of transformation functions. */
for (k = 0; k < tarr->nelts; k++) {
action = (msre_action *)telts[k].val;
if (strcmp(telts[k].key, "t") == 0) {
if (strcmp(action->param, "none") == 0) {
apr_table_clear(normtab);
continue;
}
if (action->param_plusminus == NEGATIVE_VALUE) {
apr_table_unset(normtab, action->param);
} else {
apr_table_addn(normtab, action->param, (void *)action);
}
}
}
/* Perform transformations. */
tarr = apr_table_elts(normtab);
/* Make a copy of the variable value so that
* we can change it in-place.
*/
if (tarr->nelts) {
var->value = apr_pstrmemdup(mptmp, var->value, var->value_len);
/* var->value_len remains the same */
}
/* Execute transformations in a loop. */
changed = 1;
telts = (const apr_table_entry_t*)tarr->elts;
for (k = 0; k < tarr->nelts; k++) {
char *rval = NULL;
long int rval_length = -1;
/* In multi-match mode we execute the operator
* once at the beginning and then once every
* time the variable is changed by the transformation
* function.
*/
if (multi_match && changed) {
invocations++;
rc = execute_operator(var, rule, msr, acting_actionset, mptmp);
if (rc < 0) {
apr_pool_destroy(mptmp);
return -1;
}
if (rc == RULE_MATCH) {
match_count++;
/* Return straight away if the transaction
* was intercepted - no need to process the remaining
* targets.
*/
if (msr->was_intercepted) {
return RULE_MATCH;
}
}
}
/* Perform one transformation. */
action = (msre_action *)telts[k].val;
metadata = (msre_tfn_metadata *)action->param_data;
rc = metadata->execute(mptmp, (unsigned char *)var->value, var->value_len,
&rval, &rval_length);
if (rc < 0) {
apr_pool_destroy(mptmp);
return -1;
}
changed = rc;
var->value = rval;
var->value_len = rval_length;
if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "T (%i) %s: %s", rc, metadata->name,
log_escape_nq_ex(mptmp, var->value, var->value_len));
}
}
}
/* Execute operator if multi-matching is not enabled,
* or if it is and we need to process the result of the
* last transformation.
*/
if (!multi_match || changed) {
invocations++;
rc = execute_operator(var, rule, msr, acting_actionset, mptmp);
if (rc < 0) {
apr_pool_destroy(mptmp);
return -1;
}
if (rc == RULE_MATCH) {
match_count++;
/* Return straight away if the transaction
* was intercepted - no need to process the remaining
* targets.
*/
if (msr->was_intercepted) {
return RULE_MATCH;
}
}
}
}
apr_pool_destroy(mptmp);
return (match_count ? RULE_MATCH : RULE_NO_MATCH);
}
/**
* Checks whether the given rule ID is in the given range.
*/
int rule_id_in_range(int ruleid, const char *range) {
char *p = NULL, *saveptr = NULL;
char *data = NULL;
if (range == NULL) return 0;
data = strdup(range);
if (data == NULL) return 0;
p = apr_strtok(data, ",", &saveptr);
while(p != NULL) {
char *s = strstr(p, "-");
if (s == NULL) {
if (ruleid == atoi(p)) {
free(data);
return 1;
}
} else {
int start = atoi(p);
int end = atoi(s + 1);
if ((ruleid >= start)&&(ruleid <= end)) {
free(data);
return 1;
}
}
p = apr_strtok(NULL, ",", &saveptr);
}
free(data);
return 0;
}