diff --git a/apache2/Makefile.in b/apache2/Makefile.in index 6b373a7c..320b4152 100644 --- a/apache2/Makefile.in +++ b/apache2/Makefile.in @@ -76,7 +76,7 @@ clean: clean-extras @rm -rf *.la *.lo *.o *.slo .libs msc_test msc-test-debug.log maintainer-clean: clean - @rm -rf Makefile mlogc-src/Makefile t/run-tests.pl config config.log config.status configure mod_security2_config.h ../tools/*.pl autoscan.log configure.scan build/libtool.m4 build/config.guess build/config.sub build/ltmain.sh build/apxs-wrapper + @rm -rf Makefile mlogc-src/Makefile t/run-unit-tests.pl t/run-regression-tests.pl t/gen_rx-pm.pl t/csv_rx-pm.pl t/run-tests.pl t/regression/server_root/conf/httpd.conf t/regression/server_root/conf/config_*.t_*.conf config config.log config.status configure mod_security2_config.h ../tools/*.pl autoscan.log configure.scan build/libtool.m4 build/config.guess build/config.sub build/ltmain.sh build/apxs-wrapper distclean: maintainer-clean @@ -117,14 +117,17 @@ msc_test.lo: msc_test.c $(LIBTOOL) --mode=compile $(CC) $(APXS_INCLUDES) $(APXS_CFLAGS) $(EXTRA_CFLAGS) $(MODSEC_EXTRA_CFLAGS) $(CPPFLAGS) $(APR_CFLAGS) $(APU_CFLAGS) -o msc_test.lo -c msc_test.c msc_test: $(TESTOBJS) $(MOD_SECURITY2_H}) msc_test.lo - @objs=""; \ + objs=""; \ for f in $(MSC_TEST); do \ objs="$$objs $$f.lo"; \ done; \ $(LIBTOOL) --mode=link $(CC) $$objs -o msc_test msc_test.lo $(LDFLAGS) $(LIBS) $(APR_LINK_LD) $(APU_LINK_LD) -test: t/run-tests.pl msc_test +test: t/run-unit-tests.pl msc_test @rm -f msc-test-debug.log; \ - $(PERL) t/run-tests.pl + $(PERL) t/run-unit-tests.pl -.PHONY: all install clean-extras clean maintainer-clean distclean install-mods test +test-regression: t/run-regression-tests.pl + @$(PERL) t/run-regression-tests.pl + +.PHONY: all install clean-extras clean maintainer-clean distclean install-mods test test-regression diff --git a/apache2/configure.in b/apache2/configure.in index 4490d733..717221d6 100644 --- a/apache2/configure.in +++ b/apache2/configure.in @@ -39,6 +39,25 @@ AC_FUNC_MALLOC AC_FUNC_MEMCMP AC_CHECK_FUNCS([atexit fchmod getcwd memset strcasecmp strchr strdup strerror strncasecmp strrchr strstr strtol]) +# Some directories +MSC_BASE_DIR=`pwd` +MSC_PKGBASE_DIR="$MSC_BASE_DIR/.." +MSC_TEST_DIR="$MSC_BASE_DIR/t" +MSC_REGRESSION_DIR="$MSC_TEST_DIR/regression" +MSC_REGRESSION_SERVERROOT_DIR="$MSC_REGRESSION_DIR/server_root" +MSC_REGRESSION_CONF_DIR="$MSC_REGRESSION_SERVERROOT_DIR/conf" +MSC_REGRESSION_LOGS_DIR="$MSC_REGRESSION_SERVERROOT_DIR/logs" +MSC_REGRESSION_DOCROOT_DIR="$MSC_REGRESSION_SERVERROOT_DIR/htdocs" + +AC_SUBST(MSC_BASE_DIR) +AC_SUBST(MSC_PKGBASE_DIR) +AC_SUBST(MSC_TEST_DIR) +AC_SUBST(MSC_REGRESSION_DIR) +AC_SUBST(MSC_REGRESSION_SERVERROOT_DIR) +AC_SUBST(MSC_REGRESSION_CONF_DIR) +AC_SUBST(MSC_REGRESSION_LOGS_DIR) +AC_SUBST(MSC_REGRESSION_DOCROOT_DIR) + # Find apxs AC_MSG_NOTICE(looking for Apache module support via DSO through APXS) AC_ARG_WITH(apxs, @@ -107,6 +126,14 @@ VERSION_OK fi APXS_LIBTOOL="`$APXS -q LIBTOOL`" APXS_CC="`$APXS -q CC`" + APXS_BINDIR="`$APXS -q BINDIR`" + APXS_SBINDIR="`$APXS -q SBINDIR`" + APXS_PROGNAME="`$APXS -q PROGNAME`" + if test "$APXS_SBINDIR" = "/"; then + APXS_HTTPD="$APXS_SBINDIR/$APXS_PROGNAME" + else + APXS_HTTPD="$APXS_SBINDIR/$APXS_PROGNAME" + fi else AC_MSG_ERROR(couldn't find APXS) fi @@ -263,6 +290,11 @@ AC_SUBST(APXS_LIBS) AC_SUBST(APXS_CFLAGS) AC_SUBST(APXS_LIBTOOL) AC_SUBST(APXS_CC) +AC_SUBST(APXS_LIBDIR) +AC_SUBST(APXS_BINDIR) +AC_SUBST(APXS_SBINDIR) +AC_SUBST(APXS_PROGNAME) +AC_SUBST(APXS_HTTPD) CHECK_PCRE() CHECK_APR() @@ -274,7 +306,11 @@ CHECK_CURL() AC_CONFIG_FILES([Makefile]) AC_CONFIG_FILES([build/apxs-wrapper], [chmod +x build/apxs-wrapper]) if test -e "$PERL"; then - AC_CONFIG_FILES([t/run-tests.pl], [chmod +x t/run-tests.pl]) + AC_CONFIG_FILES([t/run-unit-tests.pl], [chmod +x t/run-unit-tests.pl]) + AC_CONFIG_FILES([t/run-regression-tests.pl], [chmod +x t/run-regression-tests.pl]) + AC_CONFIG_FILES([t/gen_rx-pm.pl], [chmod +x t/gen_rx-pm.pl]) + AC_CONFIG_FILES([t/csv_rx-pm.pl], [chmod +x t/csv_rx-pm.pl]) + AC_CONFIG_FILES([t/regression/server_root/conf/httpd.conf]) # Perl based tools AC_CONFIG_FILES([../tools/rules-updater.pl], [chmod +x ../tools/rules-updater.pl]) diff --git a/apache2/msc_test.c b/apache2/msc_test.c index 8adb85c0..ec93e16a 100644 --- a/apache2/msc_test.c +++ b/apache2/msc_test.c @@ -9,6 +9,7 @@ * */ #include +#include #include "modsecurity.h" #include "re.h" @@ -16,7 +17,7 @@ #define ISHEX(X) (((X >= '0')&&(X <= '9')) || ((X >= 'a')&&(X <= 'f')) || ((X >= 'A')&&(X <= 'F'))) -#define BUFLEN 8192 +#define BUFLEN 32768 #define RESULT_SUCCESS 0 #define RESULT_ERROR -1 @@ -24,10 +25,50 @@ #define RESULT_WRONGSIZE -3 #define RESULT_WRONGRET -4 +#define CMDLINE_OPTS "t:n:p:P:r:I:D:Nh" + +/* Types */ +typedef struct tfn_data_t tfn_data_t; +typedef struct op_data_t op_data_t; +typedef struct action_data_t action_data_t; + +struct tfn_data_t { + const char *name; + const char *param; + unsigned char *input; + apr_size_t input_len; + msre_tfn_metadata *metadata; +}; + +struct op_data_t { + const char *name; + const char *param; + unsigned char *input; + apr_size_t input_len; + msre_ruleset *ruleset; + msre_rule *rule; + msre_var *var; + msre_op_metadata *metadata; +}; + +struct action_data_t { + const char *name; + unsigned char *input; + apr_size_t input_len; + msre_ruleset *ruleset; + msre_rule *rule; + msre_var *var; + msre_actionset *actionset; + msre_action *action; +}; + + /* Globals */ +static int debuglog_level = 0; static char *test_name = NULL; static apr_pool_t *g_mp = NULL; static modsec_rec *g_msr = NULL; +static unsigned char buf[BUFLEN]; msc_engine *modsecurity = NULL; @@ -45,7 +86,10 @@ int apache2_exec(modsec_rec *msr, const char *command, const char **argv, char * } char *get_apr_error(apr_pool_t *p, apr_status_t rc) { - return "FAKE APR ERROR"; + char *text = apr_pcalloc(p, 201); + if (text == NULL) return NULL; + apr_strerror(rc, text, 200); + return text; } void msr_log(modsec_rec *msr, int level, const char *text, ...) { @@ -67,7 +111,7 @@ void msr_log(modsec_rec *msr, int level, const char *text, ...) { if (msr->txcfg->debuglog_fd != NULL) { apr_size_t nbytes_written = 0; apr_vsnprintf(str1, sizeof(str1), text, ap); - apr_snprintf(str2, sizeof(str2), "[%d] [%s] %s\n", level, test_name, str1); + apr_snprintf(str2, sizeof(str2), "%lu: [%d] [%s] %s\n", (unsigned long)getpid(), level, test_name, str1); apr_file_write_full(msr->txcfg->debuglog_fd, str2, strlen(str2), &nbytes_written); } @@ -145,42 +189,48 @@ static char *escape(unsigned char *str, apr_size_t *len) /* Testing functions */ -static int test_tfn(const char *name, unsigned char *input, apr_size_t input_len, unsigned char **rval, apr_size_t *rval_len, char **errmsg) -{ - int rc = -1; - msre_tfn_metadata *metadata = NULL; - +static int init_tfn(tfn_data_t *data, const char *name, unsigned char *input, apr_size_t input_len, char **errmsg) { *errmsg = NULL; - /* Lookup the tfn */ - metadata = msre_engine_tfn_resolve(modsecurity->msre, name); - - if (metadata == NULL) { + data->name = name; + data->input = apr_pmemdup(g_mp, input, input_len); + data->input_len = input_len; + data->metadata = msre_engine_tfn_resolve(modsecurity->msre, name); + if (data->metadata == NULL) { *errmsg = apr_psprintf(g_mp, "Failed to fetch tfn \"%s\".", name); return -1; } + return 0; +} + +static int test_tfn(tfn_data_t *data, unsigned char **rval, apr_size_t *rval_len, char **errmsg) +{ + int rc = -1; + + *errmsg = NULL; + /* Execute the tfn */ - rc = metadata->execute(g_mp, input, (long)input_len, (char **)rval, (long *)rval_len); + rc = data->metadata->execute(g_mp, data->input, (long)(data->input_len), (char **)rval, (long *)rval_len); if (rc < 0) { - *errmsg = apr_psprintf(g_mp, "Failed to execute tfn \"%s\".", name); + *errmsg = apr_psprintf(g_mp, "Failed to execute tfn \"%s\".", data->name); } return rc; } -static int test_op(const char *name, const char *param, const unsigned char *input, apr_size_t input_len, char **errmsg) -{ +static int init_op(op_data_t *data, const char *name, const char *param, unsigned char *input, apr_size_t input_len, char **errmsg) { const char *args = apr_psprintf(g_mp, "@%s %s", name, param); char *conf_fn; - msre_ruleset *ruleset = NULL; - msre_rule *rule = NULL; - msre_var *var = NULL; - msre_op_metadata *metadata = NULL; int rc = -1; *errmsg = NULL; + data->name = name; + data->param = param; + data->input = input; + data->input_len = input_len; + if ( apr_filepath_merge(&conf_fn, NULL, "t/unit-test.conf", APR_FILEPATH_TRUENAME, g_mp) != APR_SUCCESS) { *errmsg = apr_psprintf(g_mp, "Failed to build a conf filename."); return -1; @@ -198,49 +248,136 @@ static int test_op(const char *name, const char *param, const unsigned char *inp ); /* Lookup the operator */ - metadata = msre_engine_op_resolve(modsecurity->msre, name); - if (metadata == NULL) { + data->metadata = msre_engine_op_resolve(modsecurity->msre, name); + if (data->metadata == NULL) { *errmsg = apr_psprintf(g_mp, "Failed to fetch op \"%s\".", name); return -1; } /* Create a ruleset/rule */ - ruleset = msre_ruleset_create(modsecurity->msre, g_mp); - if (ruleset == NULL) { + data->ruleset = msre_ruleset_create(modsecurity->msre, g_mp); + if (data->ruleset == NULL) { *errmsg = apr_psprintf(g_mp, "Failed to create ruleset for op \"%s\".", name); return -1; } - rule = msre_rule_create(ruleset, RULE_TYPE_NORMAL, conf_fn, 1, "UNIT_TEST", args, "t:none,pass,nolog", errmsg); - if (rule == NULL) { + data->rule = msre_rule_create(data->ruleset, RULE_TYPE_NORMAL, conf_fn, 1, "UNIT_TEST", args, "t:none,pass,nolog", errmsg); + if (data->rule == NULL) { *errmsg = apr_psprintf(g_mp, "Failed to create rule for op \"%s\": %s", name, *errmsg); return -1; } /* Create a fake variable */ - var = (msre_var *)apr_pcalloc(g_mp, sizeof(msre_var)); - var->name = "UNIT_TEST"; - var->value = apr_pstrmemdup(g_mp, (char *)input, input_len); - var->value_len = input_len; - var->metadata = msre_resolve_var(modsecurity->msre, var->name); - if (var->metadata == NULL) { - *errmsg = apr_psprintf(g_mp, "Failed to resolve variable for op \"%s\": %s", name, var->name); + data->var = (msre_var *)apr_pcalloc(g_mp, sizeof(msre_var)); + data->var->name = "UNIT_TEST"; + data->var->value = apr_pstrmemdup(g_mp, (char *)input, input_len); + data->var->value_len = input_len; + data->var->metadata = msre_resolve_var(modsecurity->msre, data->var->name); + if (data->var->metadata == NULL) { + *errmsg = apr_psprintf(g_mp, "Failed to resolve variable for op \"%s\": %s", name, data->var->name); return -1; } /* Initialize the operator parameter */ - if (metadata->param_init != NULL) { - rc = metadata->param_init(rule, errmsg); + if (data->metadata->param_init != NULL) { + rc = data->metadata->param_init(data->rule, errmsg); if (rc <= 0) { *errmsg = apr_psprintf(g_mp, "Failed to init op \"%s\": %s", name, *errmsg); return rc; } } + return 0; +} + +static int test_op(op_data_t *data, char **errmsg) +{ + int rc = -1; + + *errmsg = NULL; + /* Execute the operator */ - if (metadata->execute != NULL) { - rc = metadata->execute(g_msr, rule, var, errmsg); + if (data->metadata->execute != NULL) { + rc = data->metadata->execute(g_msr, data->rule, data->var, errmsg); if (rc < 0) { - *errmsg = apr_psprintf(g_mp, "Failed to execute op \"%s\": %s", name, *errmsg); + *errmsg = apr_psprintf(g_mp, "Failed to execute op \"%s\": %s", data->name, *errmsg); + } + } + + return rc; +} + +static int init_action(action_data_t *data, const char *name, const char *param, char **errmsg) +{ + const char *action_string = NULL; + char *conf_fn; + + *errmsg = NULL; + + if ((param == NULL) || (strcmp("", param) == 0)) { + action_string = apr_psprintf(g_mp, "%s", name); + } + else { + action_string = apr_psprintf(g_mp, "%s:%s", name, param); + } + if (action_string == NULL) { + *errmsg = apr_psprintf(g_mp, "Failed to build action string for action: \"%s\".", name); + return -1; + } + + if ( apr_filepath_merge(&conf_fn, NULL, "t/unit-test.conf", APR_FILEPATH_TRUENAME, g_mp) != APR_SUCCESS) { + *errmsg = apr_psprintf(g_mp, "Failed to build a conf filename."); + return -1; + } + + /* Register UNIT_TEST variable */ + msre_engine_variable_register(modsecurity->msre, + "UNIT_TEST", + VAR_SIMPLE, + 0, 0, + NULL, + NULL, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* Create a ruleset/rule */ + data->ruleset = msre_ruleset_create(modsecurity->msre, g_mp); + if (data->ruleset == NULL) { + *errmsg = apr_psprintf(g_mp, "Failed to create ruleset for action \"%s\".", name); + return -1; + } + data->rule = msre_rule_create(data->ruleset, RULE_TYPE_NORMAL, conf_fn, 1, "UNIT_TEST", "@unconditionalMatch", action_string, errmsg); + if (data->rule == NULL) { + *errmsg = apr_psprintf(g_mp, "Failed to create rule for action \"%s\": %s", name, *errmsg); + return -1; + } + + /* Get the actionset/action */ + data->actionset = data->rule->actionset; + if (data->actionset == NULL) { + *errmsg = apr_psprintf(g_mp, "Failed to fetch actionset for action \"%s\"", name); + return -1; + } + data->action = (msre_action *)apr_table_get(data->actionset->actions, name); + if (data->action == NULL) { + *errmsg = apr_psprintf(g_mp, "Failed to fetch action for action \"%s\"", name); + return -1; + } + + return 0; +} + +static int test_action(action_data_t *data, char **errmsg) +{ + int rc = -1; + + *errmsg = NULL; + + /* Execute the action */ + if (data->action->metadata->execute != NULL) { + rc = data->action->metadata->execute(g_msr, g_mp, data->rule, data->action); + if (rc < 0) { + *errmsg = apr_psprintf(g_mp, "Failed to execute action \"%s\": %d", data->name, rc); } } @@ -265,7 +402,7 @@ static void init_msr() { dcfg->of_limit_action = RESPONSE_BODY_LIMIT_ACTION_REJECT; dcfg->debuglog_fd = NOT_SET_P; dcfg->debuglog_name = "msc-test-debug.log"; - dcfg->debuglog_level = 9; + dcfg->debuglog_level = debuglog_level; dcfg->cookie_format = 0; dcfg->argument_separator = '&'; dcfg->rule_inheritance = 0; @@ -282,7 +419,7 @@ static void init_msr() { dcfg->upload_dir = NULL; dcfg->upload_keep_files = KEEP_FILES_OFF; dcfg->upload_validates_files = 0; - dcfg->data_dir = NULL; + dcfg->data_dir = "."; dcfg->webappid = "default"; dcfg->content_injection_enabled = 0; dcfg->pdfp_enabled = 0; @@ -322,115 +459,167 @@ static void init_msr() { g_msr->request_headers = NULL; g_msr->hostname = "localhost"; g_msr->msc_rule_mptmp = g_mp; - g_msr->tx_vars = apr_table_make(g_mp, 10); + g_msr->tx_vars = apr_table_make(g_mp, 1); } +/** + * Usage text. + */ +static void usage() { + fprintf(stderr, "ModSecurity Unit Tester v%s\n", MODULE_RELEASE); + fprintf(stderr, " Usage: msc_test [options]\n"); + fprintf(stderr, "\n"); + fprintf(stderr, " Options:\n"); + fprintf(stderr, " -t Type (required)\n"); + fprintf(stderr, " -n Name (required)\n"); + fprintf(stderr, " -p Parameter (required)\n"); + fprintf(stderr, " -P Prerun (optional for actions)\n"); + fprintf(stderr, " -r Function return code (required for some types)\n"); + fprintf(stderr, " -I Iterations (default 1)\n"); + fprintf(stderr, " -D Debug log level (default 0)\n"); + fprintf(stderr, " -N No input on stdin.\n\n"); + fprintf(stderr, " -h This help\n\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "Input is from stdin unless -N is used.\n"); +} + + /* Main */ int main(int argc, const char * const argv[]) { + apr_getopt_t *opt; apr_file_t *fd; - unsigned char buf[BUFLEN]; - apr_size_t nbytes = BUFLEN; - unsigned char input[BUFLEN]; + apr_size_t nbytes = 0; const char *type = NULL; const char *name = NULL; unsigned char *param = NULL; + unsigned char *prerun = NULL; const char *returnval = NULL; + int iterations = 1; char *errmsg = NULL; unsigned char *out = NULL; - apr_size_t input_len = 0; apr_size_t param_len = 0; + apr_size_t prerun_len = 0; apr_size_t out_len = 0; + int noinput = 0; int rc = 0; int result = 0; int ec = 0; + int i; + apr_time_t T0 = 0; + apr_time_t T1 = 0; + tfn_data_t tfn_data; + op_data_t op_data; + action_data_t action_data; + int ret = 0; + + memset(&tfn_data, 0, sizeof(tfn_data_t)); + memset(&op_data, 0, sizeof(op_data_t)); + memset(&action_data, 0, sizeof(action_data_t)); apr_app_initialize(&argc, &argv, NULL); atexit(apr_terminate); - if (argc < 4) { - fprintf(stderr, "Usage: %s []\n", argv[0]); + apr_pool_create(&g_mp, NULL); + + rc = apr_getopt_init(&opt, g_mp, argc, argv); + if (rc != APR_SUCCESS) { + fprintf(stderr, "Failed to initialize.\n\n"); + usage(); exit(1); } - apr_pool_create(&g_mp, NULL); - modsecurity = modsecurity_create(g_mp, MODSEC_OFFLINE); + do { + char ch; + const char *val; + rc = apr_getopt(opt, CMDLINE_OPTS, &ch, &val); + switch (rc) { + case APR_SUCCESS: + switch (ch) { + case 't': + type = val; + break; + case 'n': + name = val; + break; + case 'p': + param_len = strlen(val); + param = apr_pmemdup(g_mp, val, param_len + 1); + unescape_inplace(param, ¶m_len); + break; + case 'P': + prerun_len = strlen(val); + prerun = apr_pmemdup(g_mp, val, prerun_len + 1); + unescape_inplace(prerun, &prerun_len); + break; + case 'r': + returnval = val; + break; + case 'I': + iterations = atoi(val); + break; + case 'D': + debuglog_level = atoi(val); + break; + case 'N': + noinput = 1; + break; + case 'h': + usage(); + exit(0); + } + break; + case APR_BADCH: + case APR_BADARG: + usage(); + exit(1); + } + } while (rc != APR_EOF); - type = argv[1]; - name = argv[2]; - param_len = strlen(argv[3]); - param = apr_pmemdup(g_mp, argv[3], param_len + 1); - unescape_inplace(param, ¶m_len); - if (argc >= 5) { - returnval = argv[4]; + rc = apr_getopt_init(&opt, g_mp, argc, argv); + if (!type || !name || !param) { + usage(); + exit(1); } + modsecurity = modsecurity_create(g_mp, MODSEC_OFFLINE); test_name = apr_psprintf(g_mp, "%s/%s", type, name); - if (apr_file_open_stdin(&fd, g_mp) != APR_SUCCESS) { - fprintf(stderr, "Failed to open stdin\n"); - exit(1); + if (noinput == 0) { + if (apr_file_open_stdin(&fd, g_mp) != APR_SUCCESS) { + fprintf(stderr, "Failed to open stdin\n"); + exit(1); + } + + /* Read in the input */ + nbytes = BUFLEN; + memset(buf, 0, nbytes); + rc = apr_file_read(fd, buf, &nbytes); + if ((rc != APR_EOF) && (rc != APR_SUCCESS)) { + fprintf(stderr, "Failed to read data\n"); + exit(1); + } + + if (nbytes < 0) { + fprintf(stderr, "Error reading data\n"); + exit(1); + } + + apr_file_close(fd); } - memset(buf, 0, BUFLEN); - rc = apr_file_read(fd, buf, &nbytes); - if ((rc != APR_EOF) && (rc != APR_SUCCESS)) { - fprintf(stderr, "Failed to read data\n"); - exit(1); - } - - if (nbytes < 0) { - fprintf(stderr, "Error reading data\n"); - exit(1); - } - - apr_file_close(fd); - - /* Make a copy as transformations are done in-place */ - memcpy(input, buf, BUFLEN); - input_len = nbytes; - if (strcmp("tfn", type) == 0) { - /* Transformations */ - int ret = returnval ? atoi(returnval) : -8888; - rc = test_tfn(name, input, input_len, &out, &out_len, &errmsg); + ret = returnval ? atoi(returnval) : -8888; + + rc = init_tfn(&tfn_data, name, buf, nbytes, &errmsg); if (rc < 0) { fprintf(stderr, "ERROR: %s\n", errmsg); result = RESULT_ERROR; } - else if ((ret != -8888) && (rc != ret)) { - fprintf(stderr, "Returned %d (expected %d)\n", rc, ret); - result = RESULT_WRONGRET; - } - else if (param_len != out_len) { - fprintf(stderr, "Lenth %" APR_SIZE_T_FMT " (expected %" APR_SIZE_T_FMT ")\n", out_len, param_len); - result = RESULT_WRONGSIZE; - } - else { - result = memcmp(param, out, param_len) ? RESULT_MISMATCHED : RESULT_SUCCESS; - } - - if (result != RESULT_SUCCESS) { - apr_size_t s0len = nbytes; - const char *s0 = escape(buf, &s0len); - apr_size_t s1len = out_len; - const char *s1 = escape(out, &s1len); - apr_size_t s2len = param_len; - const char *s2 = escape(param, &s2len); - - fprintf(stderr, " Input: '%s' len=%" APR_SIZE_T_FMT "\n" - "Output: '%s' len=%" APR_SIZE_T_FMT "\n" - "Expect: '%s' len=%" APR_SIZE_T_FMT "\n", - s0, nbytes, s1, out_len, s2, param_len); - ec = 1; - } } else if (strcmp("op", type) == 0) { - /* Operators */ - int ret = 0; - if (!returnval) { fprintf(stderr, "Return value required for type \"%s\"\n", type); exit(1); @@ -439,34 +628,176 @@ int main(int argc, const char * const argv[]) init_msr(); - rc = test_op(name, (const char *)param, (const unsigned char *)input, input_len, &errmsg); + rc = init_op(&op_data, name, (const char *)param, buf, nbytes, &errmsg); if (rc < 0) { fprintf(stderr, "ERROR: %s\n", errmsg); result = RESULT_ERROR; } - else if (rc != ret) { - fprintf(stderr, "Returned %d (expected %d)\n", rc, ret); - result = RESULT_WRONGRET; + } + else if (strcmp("action", type) == 0) { + if (!returnval) { + fprintf(stderr, "Return value required for type \"%s\"\n", type); + exit(1); + } + ret = atoi(returnval); + + init_msr(); + + if (prerun) { + action_data_t paction_data; + char *pname = apr_pstrdup(g_mp, (const char *)prerun); + char *pparam = NULL; + + if ((pparam = strchr((const char *)pname, ':'))) { + pparam[0] = '\0'; + pparam++; + } + + rc = init_action(&paction_data, pname, (const char *)pparam, &errmsg); + if (rc < 0) { + fprintf(stderr, "ERROR: prerun - %s\n", errmsg); + exit(1); + } + + rc = test_action(&paction_data, &errmsg); + if (rc < 0) { + fprintf(stderr, "ERROR: prerun - %s\n", errmsg); + exit(1); + } + } + + rc = init_action(&action_data, name, (const char *)param, &errmsg); + if (rc < 0) { + fprintf(stderr, "ERROR: %s\n", errmsg); + result = RESULT_ERROR; + } + } + + if (iterations > 1) { + apr_time_clock_hires (g_mp); + T0 = apr_time_now(); + } + + for (i = 1; i <= iterations; i++) { + #ifdef VERBOSE + if (i % 100 == 0) { + if (i == 100) { + fprintf(stderr, "Iterations/100: ."); + } + else { + fprintf(stderr, "."); + } + } + #endif + + if (strcmp("tfn", type) == 0) { + /* Transformations */ + rc = test_tfn(&tfn_data, &out, &out_len, &errmsg); + if (rc < 0) { + fprintf(stderr, "ERROR: %s\n", errmsg); + result = RESULT_ERROR; + } + else if ((ret != -8888) && (rc != ret)) { + fprintf(stderr, "Returned %d (expected %d)\n", rc, ret); + result = RESULT_WRONGRET; + } + else if (param_len != out_len) { + fprintf(stderr, "Lenth %" APR_SIZE_T_FMT " (expected %" APR_SIZE_T_FMT ")\n", out_len, param_len); + result = RESULT_WRONGSIZE; + } + else { + result = memcmp(param, out, param_len) ? RESULT_MISMATCHED : RESULT_SUCCESS; + } + + if (result != RESULT_SUCCESS) { + apr_size_t s0len = nbytes; + const char *s0 = escape(buf, &s0len); + apr_size_t s1len = out_len; + const char *s1 = escape(out, &s1len); + apr_size_t s2len = param_len; + const char *s2 = escape(param, &s2len); + + fprintf(stderr, " Input: '%s' len=%" APR_SIZE_T_FMT "\n" + "Output: '%s' len=%" APR_SIZE_T_FMT "\n" + "Expect: '%s' len=%" APR_SIZE_T_FMT "\n", + s0, nbytes, s1, out_len, s2, param_len); + ec = 1; + } + } + else if (strcmp("op", type) == 0) { + /* Operators */ + rc = test_op(&op_data, &errmsg); + if (rc < 0) { + fprintf(stderr, "ERROR: %s\n", errmsg); + result = RESULT_ERROR; + } + else if (rc != ret) { + fprintf(stderr, "Returned %d (expected %d)\n", rc, ret); + result = RESULT_WRONGRET; + } + else { + result = RESULT_SUCCESS; + } + + if (result != RESULT_SUCCESS) { + apr_size_t s0len = nbytes; + const char *s0 = escape(buf, &s0len); + + fprintf(stderr, " Test: '@%s %s'\n" + "Input: '%s' len=%" APR_SIZE_T_FMT "\n", + name, param, s0, nbytes); + ec = 1; + } + } + else if (strcmp("action", type) == 0) { + /* Actions */ + + rc = test_action(&action_data, &errmsg); + if (rc < 0) { + fprintf(stderr, "ERROR: %s\n", errmsg); + result = RESULT_ERROR; + } + else if (rc != ret) { + fprintf(stderr, "Returned %d (expected %d)\n", rc, ret); + result = RESULT_WRONGRET; + } + else { + result = RESULT_SUCCESS; + } + + if (result != RESULT_SUCCESS) { + fprintf(stderr, " Test: '%s:%s'\n" + "Prerun: '%s'\n", + name, param, (prerun ? (const char *)prerun : "")); + ec = 1; + } + } else { - result = RESULT_SUCCESS; + fprintf(stderr, "Unknown type: \"%s\"\n", type); + exit(1); } - if (result != RESULT_SUCCESS) { - apr_size_t s0len = nbytes; - const char *s0 = escape(buf, &s0len); - - fprintf(stderr, " Test: '@%s %s'\n" - "Input: '%s' len=%" APR_SIZE_T_FMT "\n", - name, param, s0, nbytes); - ec = 1; + if (ec != 0) { + fprintf(stdout, "%s\n", errmsg ? errmsg : ""); + return ec; } } - else { - fprintf(stderr, "Unknown type: \"%s\"\n", type); - exit(1); - } + if (iterations > 1) { + double dT; + T1 = apr_time_now(); + + dT = apr_time_as_msec(T1 - T0); + + #ifdef VERBOSE + if (i >= 100) { + fprintf(stderr, "\n"); + } + #endif + + fprintf(stdout, "%d @ %.4f msec per iteration.\n", iterations, dT / iterations); + } fprintf(stdout, "%s\n", errmsg ? errmsg : ""); return ec; diff --git a/apache2/t/csv_rx-pm.pl.in b/apache2/t/csv_rx-pm.pl.in new file mode 100755 index 00000000..6ea02ce8 --- /dev/null +++ b/apache2/t/csv_rx-pm.pl.in @@ -0,0 +1,21 @@ +#!@PERL@ +# +# Example to generate CSV performance data from test results taken from +# test generated by gen_rx-pm.pl. +# +use strict; + +my %H = (); +while (<>) { + chomp; + my ($op, $label, $n, $i, $value) = (m/\s*\d+\)\s+\S+\s+"([^"]*)"\s+(\S+)\s+(\d+) item\(s\): passed\s+\((\d+)\s+\@\s+([-\+\d\.E]+) msec\s.*/); + + next unless defined($value); + $H{$n}{$label} = $value; + +} + +printf "%s, %s, %s, %s\n", qw(N rx1 rx2 pm1); +for (sort {$a <=> $b} keys %H) { + printf "%s, %s, %s, %s\n", $_, $H{$_}{rx1}, $H{$_}{rx2}, $H{$_}{pm1} +}; diff --git a/apache2/t/gen_rx-pm.pl.in b/apache2/t/gen_rx-pm.pl.in new file mode 100755 index 00000000..c5e56adb --- /dev/null +++ b/apache2/t/gen_rx-pm.pl.in @@ -0,0 +1,99 @@ +#!@PERL@ +# +# Generates a test file for comparing @rx and @pm speed. +# +use strict; +use Regexp::Assemble; + +srand(424242); # We want this static, so we can compare different runs + +my $MIN = $ARGV[0] || 0; +my $MAX = $ARGV[1] || 5000; +my $INC = $ARGV[2] || int($MAX * .05); +my $ITERATIONS = 10000; +my $MINSTRLEN = 2; +my $MAXSTRLEN = 8; + +my $match = join '', ('a' .. 'z'); +my @param = (); +my $i=$MIN; +while ($i <= $MAX) { + my $ra = Regexp::Assemble->new; + + while (@param < $i) { + unshift @param, rndstr(); + } + + $ra->add(@param); + + printf ( + "# rx: %6d\n". + "{\n". + " comment => \"rx1 %6d item(s)\",\n". + " type => \"op\",\n". + " name => \"rx\",\n". + " param => qr/%s/,\n". + " input => \"%s\",\n". + " ret => " . (@param ? 0 : 1) . ",". + " iterations => %d,\n". + "},\n", + $i, + $i, + (@param ? '(?:' . join('|', @param) . ')' : ""), + $match, + $ITERATIONS, + ); + + printf ( + "# rx-optimized: %6d\n". + "{\n". + " comment => \"rx2 %6d item(s)\",\n". + " type => \"op\",\n". + " name => \"rx\",\n". + " param => qr/%s/,\n". + " input => \"%s\",\n". + " ret => " . (@param ? 0 : 1) . ",". + " iterations => %d,\n". + "},\n", + $i, + $i, + (@param ? $ra->as_string : ""), + $match, + $ITERATIONS, + ); + + printf ( + "# pm: %6d\n". + "{\n". + " comment => \"pm1 %6d item(s)\",\n". + " type => \"op\",\n". + " name => \"pm\",\n". + " param => \"%s\",\n". + " input => \"%s\",\n". + " ret => 0,". + " iterations => %d,\n". + "},\n", + $i, + $i, + join(' ', @param ? @param : ("''")), + $match, + $ITERATIONS, + ); + + $i = ($i == $MIN) ? ($i + $INC) - ($i % $INC) : $i + $INC; + +} + +sub rndstr { + my @c = ('a' .. 'z'); + my $rndstr; + my $max = int(rand($MAXSTRLEN - $MINSTRLEN)) + $MINSTRLEN; + foreach (1 .. $max) { + $rndstr .= $c[rand @c]; + } + # We need a string that is not in another string for "last" + if ($match =~ m/$rndstr/) { + $rndstr = rndstr(); + } + return $rndstr; +} diff --git a/apache2/t/regression/action/00-disruptive-actions.t b/apache2/t/regression/action/00-disruptive-actions.t new file mode 100644 index 00000000..c37133b1 --- /dev/null +++ b/apache2/t/regression/action/00-disruptive-actions.t @@ -0,0 +1,531 @@ +### Tests all of the actions in each phase + +# Pass +{ + type => "action", + comment => "pass in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,pass" + SecAction "phase:1,deny" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,pass" + SecAction "phase:2,deny" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,pass" + SecAction "phase:3,deny" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,pass" + SecAction "phase:4,deny" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Allow +{ + type => "action", + comment => "allow in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,allow" + SecAction "phase:1,deny" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 1\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,allow" + SecAction "phase:2,deny" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 2\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,allow" + SecAction "phase:3,deny" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 3\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,allow" + SecAction "phase:4,deny" + ), + match_log => { + error => [ qr/ModSecurity: Access allowed \(phase 4\). Unconditional match in SecAction/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Deny +{ + type => "action", + comment => "deny in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 1\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 3\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 4\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Drop +{ + type => "action", + comment => "drop in phase:1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,drop" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 1\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,drop" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 2\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,drop" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 3\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,drop" + ), + match_log => { + error => [ qr/Access denied with connection close \(phase 4\). Unconditional match in SecAction./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Redirect +{ + type => "action", + comment => "redirect in phase:1 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:2 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 2\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:3 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 3\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:4 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with redirection to .* using status 302 \(phase 4\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, + +# Proxy +{ + type => "action", + comment => "proxy in phase:1 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied using proxy to \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "proxy in phase:2 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied using proxy to \(phase 2\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "proxy in phase:3 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 500 \(phase 3\) \(Configuration Error: Proxy action requested but it does not work in output phases\)./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "proxy in phase:4 (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt'" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 500 \(phase 4\) \(Configuration Error: Proxy action requested but it does not work in output phases\)./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, diff --git a/apache2/t/regression/action/00-meta.t b/apache2/t/regression/action/00-meta.t new file mode 100644 index 00000000..9e7b92a8 --- /dev/null +++ b/apache2/t/regression/action/00-meta.t @@ -0,0 +1,8 @@ +### Test meta actions + +# TODO: id +# TODO: logdata +# TODO: msg +# TODO: rev +# TODO: severity +# TODO: tag diff --git a/apache2/t/regression/action/00-misc.t b/apache2/t/regression/action/00-misc.t new file mode 100644 index 00000000..0a99b4c5 --- /dev/null +++ b/apache2/t/regression/action/00-misc.t @@ -0,0 +1,24 @@ +### Test misc actions + +# TODO: append +# TODO: block +# TODO: capture +# TODO: chain +# TODO: deprecatevar +# TODO: exec +# TODO: expirevar +# TODO: initcol +# TODO: multiMatch +# TODO: pause +# TODO: prepend +# TODO: sanitiseArg +# TODO: sanitiseMatched +# TODO: sanitiseRequestHeader +# TODO: sanitiseResponseHeader +# TODO: setuid +# TODO: setsid +# TODO: setenv +# TODO: setvar +# TODO: skip +# TODO: skipAfter +# TODO: xmlns diff --git a/apache2/t/regression/action/00-transformations.t b/apache2/t/regression/action/00-transformations.t new file mode 100644 index 00000000..e8f7bb2b --- /dev/null +++ b/apache2/t/regression/action/00-transformations.t @@ -0,0 +1,8 @@ +### Transformation tests + +# NOTE: individual tests done in unit tests + +# TODO: t:none to override default +# TODO: t:none inline +# TODO: combined +# TODO: caching diff --git a/apache2/t/regression/action/10-ctl.t b/apache2/t/regression/action/10-ctl.t new file mode 100644 index 00000000..1aa9fd15 --- /dev/null +++ b/apache2/t/regression/action/10-ctl.t @@ -0,0 +1,56 @@ +### ctl + +### ruleRemoveById +{ + type => "action", + comment => "ruleRemoveById existing rule across phases", + conf => qq( + SecRuleEngine On + SecAction "phase:2,id:666,deny" + SecAction "phase:1,pass,ctl:ruleRemoveById=666" + ), + match_log => { + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "ruleRemoveById future rule across phases", + conf => qq( + SecRuleEngine On + SecAction "phase:1,pass,ctl:ruleRemoveById=666" + SecAction "phase:2,id:666,deny" + ), + match_log => { + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "ruleRemoveById future rule same phase", + conf => qq( + SecRuleEngine On + SecAction "phase:1,pass,ctl:ruleRemoveById=666" + SecAction "phase:1,id:666,deny" + ), + match_log => { + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + + diff --git a/apache2/t/regression/action/10-detectiononly-actions.t b/apache2/t/regression/action/10-detectiononly-actions.t new file mode 100644 index 00000000..74d71456 --- /dev/null +++ b/apache2/t/regression/action/10-detectiononly-actions.t @@ -0,0 +1,545 @@ +### Tests all of the actions in each phase in detection only mode + +# Pass +{ + type => "action", + comment => "pass in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecAction "phase:1,pass,msg:'PASSED'" + SecAction "phase:1,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,pass,msg:'PASSED'" + SecAction "phase:2,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:3,pass,msg:'PASSED'" + SecAction "phase:3,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "pass in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecAction "phase:4,pass,msg:'PASSED'" + SecAction "phase:4,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*PASSED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Allow +{ + type => "action", + comment => "allow in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,allow,msg:'ALLOWED'" + SecAction "phase:1,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably rule stop execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,allow,msg:'ALLOWED'" + SecAction "phase:2,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably rule stop execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:3,allow,msg:'ALLOWED'" + SecAction "phase:3,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably rule stop execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "allow in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:4,allow,msg:'ALLOWED'" + SecAction "phase:4,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*ALLOWED/, 1 ], + -error => [ qr/Access allowed/, 1 ], +# TODO: Allow should probably rule stop execution +# -error => [ qr/DENIED/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Deny +{ + type => "action", + comment => "deny in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:3,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "deny in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:4,deny,msg:'DENIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DENIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Drop +{ + type => "action", + comment => "drop in phase:1", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:1,drop,msg:'DROPPED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:2", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:2,drop,msg:'DROPPED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:3", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:3,drop,msg:'DROPPED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "drop in phase:4", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecAction "phase:4,drop,msg:'DROPPED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction.*DROPPED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# Redirect +{ + type => "action", + comment => "redirect in phase:1 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:2 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:3 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "redirect in phase:4 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,redirect:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'REDIRECTED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*REDIRECTED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, + +# Proxy +{ + type => "action", + comment => "proxy in phase:1 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:1,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'PROXIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*PROXIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "proxy in phase:2 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule REQUEST_URI "\@streq /test2.txt" "phase:2,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'PROXIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*PROXIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + content => qr/^TEST 2$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "proxy in phase:3 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:3,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'PROXIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*PROXIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, +{ + type => "action", + comment => "proxy in phase:4 (get)", + conf => qq( + SecRuleEngine DetectionOnly + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 4 + SecRule REQUEST_URI "\@streq /test2.txt" "phase:4,proxy:'http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt',msg:'PROXIED'" + ), + match_log => { + error => [ qr/ModSecurity: Warning. String match "\/test2.txt" at REQUEST_URI.*PROXIED/, 1 ], + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test2.txt", + ), +}, diff --git a/apache2/t/regression/action/10-logging.t b/apache2/t/regression/action/10-logging.t new file mode 100644 index 00000000..0c15bd42 --- /dev/null +++ b/apache2/t/regression/action/10-logging.t @@ -0,0 +1,248 @@ +### Logging tests + +# log/nolog +{ + type => "action", + comment => "log", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# auditlog/noauditlog +{ + type => "action", + comment => "auditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# All log/nolog auditlog/noauditlog combos +{ + type => "action", + comment => "log,auditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log,auditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "log,noauditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log,noauditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,auditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog,auditlog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + # No message, but should have data. This may need changed + audit => [ qr/-H--\s+Stopwatch: /s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,noauditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog,noauditlog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,log", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog,log" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,nolog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog,nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,log", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog,log" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,nolog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog,nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + diff --git a/apache2/t/regression/config/00-load-modsec.t b/apache2/t/regression/config/00-load-modsec.t new file mode 100644 index 00000000..9c6ecc5f --- /dev/null +++ b/apache2/t/regression/config/00-load-modsec.t @@ -0,0 +1,23 @@ +{ + type => "config", + comment => "module loaded", + match_log => { + error => [ qr/ModSecurity for Apache.* configured\./, 10 ], + }, +}, +{ + type => "config", + comment => "minimal config", + conf => sub { + # Open the minimal conf file, substituting the + # relative log paths with full paths. + open(C, "<$ENV{DIST_ROOT}/modsecurity.conf-minimal") or die "$!\n"; + (my $conf = join('', )) =~ s#Log logs/#Log $ENV{TEST_SERVER_ROOT}/logs/#g; + close C; + + return $conf; + }, + match_log => { + error => [ qr/ModSecurity for Apache.* configured\./, 10 ], + }, +}, diff --git a/apache2/t/regression/config/10-audit-directives.t b/apache2/t/regression/config/10-audit-directives.t new file mode 100644 index 00000000..8a9d1663 --- /dev/null +++ b/apache2/t/regression/config/10-audit-directives.t @@ -0,0 +1,264 @@ +### SecAudit* directive tests + +# SecAuditEngine +{ + type => "config", + comment => "SecAuditEngine On", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecAuditEngine Off", + conf => qq( + SecAuditEngine Off + SecAuditLog $ENV{AUDIT_LOG} + ), + match_log => { + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecAuditEngine RelevantOnly (pos)", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecResponseBodyAccess On + SecDefaultAction "phase:2,log,auditlog,pass" + SecRule REQUEST_URI "." "phase:4,deny" + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecAuditEngine RelevantOnly (neg)", + conf => qq( + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecResponseBodyAccess On + SecDefaultAction "phase:2,log,auditlog,pass" + ), + match_log => { + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecAuditLogType & SecAuditLogStorageDir +{ + type => "config", + comment => "SecAuditLogType Serial", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogType Serial + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/bogus", + ), +}, +{ + type => "config", + comment => "SecAuditLogType Concurrent", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogType Concurrent + SecAuditLogStorageDir "$ENV{LOGS_DIR}/audit" + ), + test => sub { + ### Perl code to parse the audit log entry and verify + ### that the concurrent audit log exists and contains + ### the correct data. + ### + ### TODO: Need some API for this :) + ### + + # Parse log + my $alogre = qr/^(?:\S+)\ (?:\S+)\ (?:\S+)\ (?:\S+)\ \[(?:[^:]+):(?:\d+:\d+:\d+)\ (?:[^\]]+)\]\ \"(?:.*)\"\ (?:\d+)\ (?:\S+)\ \"(?:.*)\"\ \"(?:.*)\"\ (\S+)\ \"(?:.*)\"\ (\S+)\ (?:\d+)\ (?:\d+)\ (?:\S+)(?:.*)$/m; + my $alog = match_log("audit", $alogre, 1); + chomp $alog; + my @log = ($alog =~ m/$alogre/); + my($id, $fn) = ($log[0], $log[1]); + if (!$id or !$fn) { + dbg("LOG ENTRY: $alog"); + die "Failed to parse audit log: $ENV{AUDIT_LOG}\n"; + } + + # Verify concurrent log exists + my $alogdatafn = "$ENV{LOGS_DIR}/audit$fn"; + if (! -e "$alogdatafn") { + die "Audit log does not exist: $alogdatafn\n"; + } + + # Verify concurrent log contents + if (defined match_file($alogdatafn, qr/^--[^-]+-A--.*$id.*-Z--$/s)) { + return 0; + } + + # Error + dbg("LOGDATA: \"$FILE{$alogdatafn}{buf}\""); + die "Audit log data did not match.\n"; + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecAuditLogRelevantStatus +{ + type => "config", + comment => "SecAuditLogRelevantStatus (pos)", + conf => qq( + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogRelevantStatus "^4" + ), + match_log => { + audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^404$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/bogus", + ), +}, +{ + type => "config", + comment => "SecAuditLogRelevantStatus (neg)", + conf => qq( + SecAuditEngine RelevantOnly + SecAuditLog $ENV{AUDIT_LOG} + SecAuditLogRelevantStatus "^4" + ), + match_log => { + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecAuditLogParts +{ + type => "config", + comment => "SecAuditLogParts (minimal)", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess On + SecResponseBodyAccess On + SecAuditLogParts "AZ" + ), + match_log => { + audit => [ qr/-A--.*-Z--/s, 1 ], + -audit => [ qr/-[B-Y]--/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1r&=2", + ), +}, +{ + type => "config", + comment => "SecAuditLogParts (default)", + conf => qq( + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess On + SecResponseBodyAccess On + ), + match_log => { + audit => [ qr/-A--.*-B--.*-F--.*-H--.*-Z--/s, 1 ], + -audit => [ qr/-[DEGIJK]--/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1r&=2", + ), +}, +{ + type => "config", + comment => "SecAuditLogParts (all)", + conf => qq( + SecRuleEngine On + SecAuditEngine On + SecAuditLog $ENV{AUDIT_LOG} + SecRequestBodyAccess On + SecResponseBodyAccess On + SecAuditLogParts "ABCDEFGHIJKZ" + SecAction "phase:4,log,auditlog,allow" + ), + match_log => { + audit => [ qr/-A--.*-B--.*-C--.*-F--.*-E--.*-H--.*-K--.*-Z--/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1r&=2", + ), +}, diff --git a/apache2/t/regression/config/10-debug-directives.t b/apache2/t/regression/config/10-debug-directives.t new file mode 100644 index 00000000..b5376879 --- /dev/null +++ b/apache2/t/regression/config/10-debug-directives.t @@ -0,0 +1,278 @@ +### SecDebug* directive tests +{ + type => "config", + comment => "SecDebugLog (pos)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + ), + match_log => { + debug => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecDebugLog (neg)", + conf => qq( + SecRuleEngine On + ), + match_log => { + -debug => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 0", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 0 + SecRule REQUEST_URI "." "phase:1,deny" + ), + match_log => { + -debug => [ qr/./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 1", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 1 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[[1]\] /, 1 ], + -debug => [ qr/\]\[[2-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 2", + conf => qq( + SecRuleEngine DetectionOnly + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 2 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[2\] /, 1 ], + -debug => [ qr/\]\[[3-9]\] /, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 3", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 3 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[3\] /, 1 ], + -debug => [ qr/\]\[[4-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 4", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[4\] /, 1 ], + -debug => [ qr/\]\[[5-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 5", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[5\] /, 1 ], + -debug => [ qr/\]\[[6-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 6", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 6 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[6\] /, 1 ], + -debug => [ qr/\]\[[7-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 7", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 7 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[7\] /, 1 ], + -debug => [ qr/\]\[[8-9]\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 8", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 8 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[8\] /, 1 ], + -debug => [ qr/\]\[9\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecDebugLogLevel 9", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRuleScript "test.lua" "phase:1" + SecRule REQUEST_URI "(.)" "phase:4,deny,deprecatevar:bogus" + ), + match_log => { + debug => [ qr/\]\[9\] /, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, diff --git a/apache2/t/regression/config/10-misc-directives.t b/apache2/t/regression/config/10-misc-directives.t new file mode 100644 index 00000000..7968785e --- /dev/null +++ b/apache2/t/regression/config/10-misc-directives.t @@ -0,0 +1,148 @@ +### Misc directive tests + +### TODO: +# SecTmpDir +# SecUploadKeepFiles +# SecChrootDir +# SecGuardianLog + +# SecDefaultAction +{ + type => "config", + comment => "SecDefaultAction", + conf => qq( + SecRuleEngine on + SecDefaultAction "phase:1,deny,status:500" + SecRule REQUEST_URI "test.txt" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 500 \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecServerSignature +{ + type => "config", + comment => "SecServerSignature On", + conf => qq( + SecServerSignature "NewServerSignature" + ), + match_log => { + error => [ qr/NewServerSignature/, 1 ], + }, + match_response => { + status => qr/^200$/, + raw => qr/^Server: +NewServerSignature$/m, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecDataDir +{ + type => "config", + comment => "SecDataDir", + conf => qq( + SecRuleEngine On + SecDataDir "$ENV{DATA_DIR}" + SecAction initcol:ip=%{REMOTE_ADDR},setvar:ip.dummy=1,pass + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_file => { + "$ENV{DATA_DIR}/ip.pag" => qr/\x00\x06dummy\x00\x00\x021\x00/, + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecTmpDir/SecUploadDir/SecUploadKeepFiles +{ + type => "config", + comment => "SecTmpDir/SecUploadDir/SecUploadKeepFiles", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecTmpDir "$ENV{TEMP_DIR}" + SecUploadKeepFiles On + SecUploadDir "$ENV{UPLOAD_DIR}" + ), + test => sub { + # Get the filename and make sure the file exists + my $fn = match_log(debug => qr/Moved file from .* to ".*"\./, 5); + die "Failed to determine uploaded filename\n" unless (defined $fn); + + $fn =~ s/Moved file from .* to "(.*)"\..*/$1/; + die "File does not exist: $fn\n" unless (-e $fn); + + # Check the contents of the file + return 0 if (match_file($fn, qr/^TESTFILE$/m)); + + msg("Failed to match contents of uploaded file: $fn"); + return 1; + }, + match_log => { + debug => [ qr/Created temporary file: $ENV{TEMP_DIR}/, 1 ], + -debug => [ qr/Failed to /, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------19813181771830765643996187206", + ], + q(-----------------------------19813181771830765643996187206 +Content-Disposition: form-data; name="upload-file"; filename="test" +Content-Type: application/octet-stream + +TESTFILE +-----------------------------19813181771830765643996187206 +Content-Disposition: form-data; name="file" + +Upload File +-----------------------------19813181771830765643996187206--), + ), +}, + +# SecWebAppId +{ + type => "config", + comment => "SecWebAppId", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + SecWebAppId "app-1" + SecAction "pass,log,auditlog,id:1" + ), + match_log => { + error => [ qr/Warning\. Unconditional match in SecAction\./, 1 ], + debug => [ qr/Warning\. Unconditional match in SecAction\./, 1 ], + audit => [ qr/^WebApp-Info: "app-1"/m, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, diff --git a/apache2/t/regression/config/10-request-directives.t b/apache2/t/regression/config/10-request-directives.t new file mode 100644 index 00000000..19e7802e --- /dev/null +++ b/apache2/t/regression/config/10-request-directives.t @@ -0,0 +1,328 @@ +### Tests for directives altering how a request is handled + +# SecArgumentSeparator +{ + type => "config", + comment => "SecArgumentSeparator (get-pos)", + conf => q( + SecRuleEngine On + SecArgumentSeparator ";" + SecRule ARGS:a "@streq 1" "phase:1,deny,chain" + SecRule ARGS:b "@streq 2" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 1\)\. String match "2" at ARGS:b\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?a=1;b=2", + ), +}, +{ + type => "config", + comment => "SecArgumentSeparator (get-neg)", + conf => q( + SecRuleEngine On + SecRule ARGS:a "@streq 1" "phase:1,deny,chain" + SecRule ARGS:b "@streq 2" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?a=1;b=2", + ), +}, +{ + type => "config", + comment => "SecArgumentSeparator (post-pos)", + conf => q( + SecRuleEngine On + SecRequestBodyAccess On + SecArgumentSeparator ";" + SecRule ARGS:a "@streq 1" "phase:2,deny,chain" + SecRule ARGS:b "@streq 2" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\)\. String match "2" at ARGS:b\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1;b=2", + ), +}, +{ + type => "config", + comment => "SecArgumentSeparator (post-neg)", + conf => q( + SecRuleEngine On + SecRequestBodyAccess On + SecRule ARGS:a "@streq 1" "phase:2,deny" + SecRule ARGS:b "@streq 2" "phase:2,deny" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1;b=2", + ), +}, + +# SecRequestBodyAccess +{ + type => "config", + comment => "SecRequestBodyAccess (pos)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRule ARGS:a "\@streq 1" "phase:2,deny,chain" + SecRule ARGS:b "\@streq 2" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\)\. String match "2" at ARGS:b\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyAccess (neg)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess Off + SecRule ARGS:a "\@streq 1" "phase:2,deny" + SecRule ARGS:b "\@streq 2" "phase:2,deny" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, + +# SecRequestBodyLimit +{ + type => "config", + comment => "SecRequestBodyLimit (equal)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 7 + ), + match_log => { + -error => [ qr/Request body is larger than the configured limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimit (greater)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimit 5 + ), + match_log => { + error => [ qr/Request body is larger than the configured limit \(5\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "a=1&b=2", + ), +}, + +# SecRequestBodyInMemoryLimit +{ + type => "config", + comment => "SecRequestBodyInMemoryLimit (equal)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimit 1000 + SecRequestBodyInMemoryLimit 276 + ), + match_log => { + -debug => [ qr/Input filter: Request too large to store in memory, switching to disk\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, +{ + type => "config", + comment => "SecRequestBodyInMemoryLimit (greater)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimit 1000 + SecRequestBodyInMemoryLimit 16 + ), + match_log => { + debug => [ qr/Input filter: Request too large to store in memory, switching to disk\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ) + ), + 1024 + ), +}, + +# SecCookieFormat +{ + type => "config", + comment => "SecCookieFormat (pos)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecCookieFormat 1 + SecRule REQUEST_COOKIES_NAMES "\@streq SESSIONID" "phase:1,deny,chain" + SecRule REQUEST_COOKIES:\$SESSIONID_PATH "\@streq /" "chain" + SecRule REQUEST_COOKIES:SESSIONID "\@streq cookieval" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 1\)\. String match "cookieval" at REQUEST_COOKIES:SESSIONID\./, 1 ], + debug => [ qr(Adding request cookie: name "\$SESSIONID_PATH", value "/"), 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Cookie" => q($Version="1"; SESSIONID="cookieval"; $PATH="/"), + ], + undef, + ), +}, +{ + type => "config", + comment => "SecCookieFormat (neg)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 5 + SecCookieFormat 0 + SecRule REQUEST_COOKIES_NAMES "\@streq SESSIONID" "phase:1,deny,chain" + SecRule REQUEST_COOKIES:\$SESSIONID_PATH "\@streq /" "chain" + SecRule REQUEST_COOKIES:SESSIONID "\@streq cookieval" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + -debug => [ qr(Adding request cookie: name "\$SESSIONID_PATH", value "/"), 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Cookie" => q($Version="1"; SESSIONID="cookieval"; $PATH="/"), + ], + undef, + ), +}, + diff --git a/apache2/t/regression/config/10-response-directives.t b/apache2/t/regression/config/10-response-directives.t new file mode 100644 index 00000000..60617a29 --- /dev/null +++ b/apache2/t/regression/config/10-response-directives.t @@ -0,0 +1,174 @@ +### Tests for directives altering how a response is handled + +# SecResponseBodyMimeTypesClear +{ + type => "config", + comment => "SecResponseBodyMimeTypesClear", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeTypesClear + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule RESPONSE_BODY "TEST" "phase:4,deny" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + debug => [ qr/Not buffering response body for unconfigured MIME type/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecResponseBodyAccess & SecResponseBodyMimeType +{ + type => "config", + comment => "SecResponseBodyAccess On", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule RESPONSE_BODY "TEST" "phase:4,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 4\)\. Pattern match "TEST" at RESPONSE_BODY\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyAccess Off", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecResponseBodyAccess Off + SecResponseBodyMimeType text/plain null + SecRule RESPONSE_BODY "TEST" "phase:4,deny" + ), + match_log => { + -error => [ qr/Access denied/, 1 ], + debug => [ qr/Response body buffering is not enabled\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecResponseBodyLimit +{ + type => "config", + comment => "SecResponseBodyLimit (equal)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 8192 + ), + match_log => { + -error => [ qr/Content-Length \(\d+\) over the limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyLimit (less)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 9000 + ), + match_log => { + -error => [ qr/Content-Length \(\d+\) over the limit/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyLimit (greater)", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 8000 + ), + match_log => { + error => [ qr/Content-Length \(\d+\) over the limit \(8000\)\./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, + +# ResponseBodyLimitAction +{ + type => "config", + comment => "SecResponseBodyLimitAction Reject", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 5 + SecResponseBodyLimitAction Reject + ), + match_log => { + error => [ qr/Content-Length \(\d+\) over the limit \(5\)\./, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, +{ + type => "config", + comment => "SecResponseBodyLimitAction ProcessPartial", + conf => qq( + SecRuleEngine On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecResponseBodyLimit 5 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecResponseBodyLimitAction ProcessPartial + ), + match_log => { + -error => [ qr/Content-Length \(\d+\) over the limit/, 1 ], + debug => [ qr/Processing partial response body \(limit 5\)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/8k.txt", + ), +}, diff --git a/apache2/t/regression/config/20-chroot.t b/apache2/t/regression/config/20-chroot.t new file mode 100644 index 00000000..2147d2b6 --- /dev/null +++ b/apache2/t/regression/config/20-chroot.t @@ -0,0 +1,35 @@ +### SecChroot tests +# TODO: Will not work as we need root access + +#{ +# type => "config", +# comment => "SecChroot", +# httpd_opts => qw( +# -DCHROOT +# ), +# conf => qq( +# # These will be in the chroot +# PidFile /logs/httpd.pid +# ScoreBoardFile /logs/httpd.scoreboard +# User nobody +# Group nogroup +# +# SecAuditEngine On +# SecDebugLog $ENV{DEBUG_LOG} +# SecDebugLogLevel 9 +# SecAuditLog $ENV{AUDIT_LOG} +# SecAuditLogStorageDir "/logs/audit" +# SecAuditLogType Concurrent +# SecChrootDir "$ENV{TEST_SERVER_ROOT}" +# ), +# match_log => { +# debug => [ qr/./, 1 ], +# audit => [ qr/./, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", +# ), +#}, diff --git a/apache2/t/regression/misc/00-multipart-parser.t b/apache2/t/regression/misc/00-multipart-parser.t new file mode 100644 index 00000000..4efc0551 --- /dev/null +++ b/apache2/t/regression/misc/00-multipart-parser.t @@ -0,0 +1,364 @@ +### Multipart parser tests + +# Final CRLF or not, we should still work +{ + type => "misc", + comment => "multipart parser (final CRLF)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (no final CRLF)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + -----------------------------69343412719991675451336310646--), + ), + ), +}, + +# Should work with a boundary of "boundary" +{ + type => "misc", + comment => "multipart parser (boundary contains \"boundary\")", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=------------------------------------------------boundary", + ], + normalize_raw_request_data( + q( + --------------------------------------------------boundary + Content-Disposition: form-data; name="a" + + 1 + --------------------------------------------------boundary + Content-Disposition: form-data; name="b" + + 2 + --------------------------------------------------boundary-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser (boundary contains \"bOuNdArY\")", + note => q( + KHTML Boundary + ), + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny" + SecRule MULTIPART_UNMATCHED_BOUNDARY "\@eq 1" "phase:2,deny" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "1".*Adding request argument \(BODY\): name "b", value "2"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=--------0xKhTmLbOuNdArY", + ], + normalize_raw_request_data( + q( + ----------0xKhTmLbOuNdArY + Content-Disposition: form-data; name="a" + + 1 + ----------0xKhTmLbOuNdArY + Content-Disposition: form-data; name="b" + + 2 + ----------0xKhTmLbOuNdArY-- + ), + ), + ), +}, + +# We should handle data starting with a "--" +{ + type => "misc", + comment => "multipart parser (data contains \"--\")", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny" + SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny" + ), + match_log => { + debug => [ qr/Adding request argument \(BODY\): name "a", value "--test".*Adding request argument \(BODY\): name "b", value "--"/s, 1 ], + -debug => [ qr/Multipart error:/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + --test + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + -- + -----------------------------69343412719991675451336310646--), + ), + ), +}, + +# We should emit warnings for parsing errors +{ + type => "misc", + comment => "multipart parser error (no final boundary)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + audit => [ qr/Final boundary missing/, 1 ], + debug => [ qr/Final boundary missing/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + 2 + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser error (no disposition)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + -debug => [ qr/Multipart error:/, 1 ], + audit => [ qr/Part missing Content-Disposition header/, 1 ], + debug => [ qr/Part missing Content-Disposition header/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + + 1 + -----------------------------69343412719991675451336310646 + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser error (bad disposition)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + audit => [ qr/Invalid Content-Disposition header/, 1 ], + debug => [ qr/Invalid Content-Disposition header/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data name="a" + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data name="b" + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "misc", + comment => "multipart parser error (no disposition name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecAuditLog "$ENV{AUDIT_LOG}" + SecAuditEngine RelevantOnly + ), + match_log => { + -debug => [ qr/Multipart error:/, 1 ], + audit => [ qr/Content-Disposition header missing name field/, 1 ], + debug => [ qr/Content-Disposition header missing name field/, 1 ], + + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + + 1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; + + 2 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, diff --git a/apache2/t/regression/misc/00-phases.t b/apache2/t/regression/misc/00-phases.t new file mode 100644 index 00000000..97a40a52 --- /dev/null +++ b/apache2/t/regression/misc/00-phases.t @@ -0,0 +1,151 @@ +### Test the phases + +# Phase 1 (request headers) +{ + type => "misc", + comment => "phase 1", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:1,pass,log,auditlog" + SecRule ARGS "val1" "phase:1,pass,log,auditlog" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:1,pass,log,auditlog" + SecRule RESPONSE_BODY "TEST" "phase:1,pass,log,auditlog" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE/, 1 ], + -error => [ qr/Pattern match .* (ARGS|RESPONSE)/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 2 (request body) +{ + type => "misc", + comment => "phase 2", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:2,pass,log,auditlog" + SecRule ARGS "val1" "phase:2,pass,log,auditlog" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:2,pass,log,auditlog" + SecRule RESPONSE_BODY "TEST" "phase:2,pass,log,auditlog" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS/s, 1 ], + -error => [ qr/Pattern match .* RESPONSE/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 3 (response headers) +{ + type => "misc", + comment => "phase 3", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:3,pass,log,auditlog" + SecRule ARGS "val1" "phase:3,pass,log,auditlog" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:3,pass,log,auditlog" + SecRule RESPONSE_BODY "TEST" "phase:3,pass,log,auditlog" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS.*Pattern match "\." at RESPONSE_HEADERS/s, 1 ], + -error => [ qr/Pattern match .* RESPONSE_BODY/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 4 (response body) +{ + type => "misc", + comment => "phase 4", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecDebugLog "$ENV{DEBUG_LOG}" + SecDebugLogLevel 9 + SecRule REQUEST_LINE "^POST" "phase:4,pass,log,auditlog" + SecRule ARGS "val1" "phase:4,pass,log,auditlog" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:4,pass,log,auditlog" + SecRule RESPONSE_BODY "TEST" "phase:4,pass,log,auditlog" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS.*Pattern match "\." at RESPONSE_HEADERS.*Pattern match "TEST" at RESPONSE_BODY/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# Phase 5 (logging) +{ + type => "misc", + comment => "phase 5", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType text/plain null + SecRule REQUEST_LINE "^POST" "phase:5,pass,log,auditlog" + SecRule ARGS "val1" "phase:5,pass,log,auditlog" + SecRule RESPONSE_HEADERS:Last-Modified "." "phase:5,pass,log,auditlog" + SecRule RESPONSE_BODY "TEST" "phase:5,pass,log,auditlog" + ), + match_log => { + error => [ qr/Pattern match "\^POST" at REQUEST_LINE.*Pattern match "val1" at ARGS.*Pattern match "\." at RESPONSE_HEADERS.*Pattern match "TEST" at RESPONSE_BODY/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, diff --git a/apache2/t/regression/rule/00-basics.t b/apache2/t/regression/rule/00-basics.t new file mode 100644 index 00000000..396d0ea1 --- /dev/null +++ b/apache2/t/regression/rule/00-basics.t @@ -0,0 +1,24 @@ +### Tests for basic rule components + +# SecAction +{ + type => "rule", + comment => "SecAction (override default)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecAction "nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Warning\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, diff --git a/apache2/t/regression/rule/00-inheritance.t b/apache2/t/regression/rule/00-inheritance.t new file mode 100644 index 00000000..c661c28e --- /dev/null +++ b/apache2/t/regression/rule/00-inheritance.t @@ -0,0 +1,4 @@ +### Tests for rule inheritance + +### TODO: +# SecRuleInheritance diff --git a/apache2/t/regression/rule/00-script.t b/apache2/t/regression/rule/00-script.t new file mode 100644 index 00000000..8af6d7bb --- /dev/null +++ b/apache2/t/regression/rule/00-script.t @@ -0,0 +1,63 @@ +### Test for SecRuleScript + +# Lua +{ + type => "rule", + comment => "SecRuleScript (lua absolute nomatch)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 1 + SecRuleScript "$ENV{CONF_DIR}/test.lua" "phase:2,deny" + ), + match_log => { + -error => [ qr/Lua script matched\./, 1 ], + debug => [ qr/Test message\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "rule", + comment => "SecRuleScript (lua relative nomatch)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 1 + SecRuleScript "test.lua" "phase:2,deny" + ), + match_log => { + -error => [ qr/Lua script matched\./, 1 ], + debug => [ qr/Test message\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "rule", + comment => "SecRuleScript (lua relative match)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 1 + SecRuleScript "match.lua" "phase:2,deny" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 403 \(phase 2\)\. Lua script matched\./, 1 ], + debug => [ qr/Test message\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, diff --git a/apache2/t/regression/rule/20-exceptions.t b/apache2/t/regression/rule/20-exceptions.t new file mode 100644 index 00000000..43ccf215 --- /dev/null +++ b/apache2/t/regression/rule/20-exceptions.t @@ -0,0 +1,129 @@ +### Tests for rule exceptions + +# SecRuleRemoveByMsg + +# SecRuleRemoveById +{ + type => "config", + comment => "SecRuleRemoveById (single)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRuleRemoveById 1 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecRuleRemoveById (multiple)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:2" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:3" + SecRuleRemoveById 1 2 3 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecRuleRemoveById (range)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:2" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:3" + SecRuleRemoveById 1-3 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecRuleRemoveById (multiple + range)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:2" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:3" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:4" + SecRuleRemoveById 1 2-4 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecRuleRemoveByMsg +{ + type => "config", + comment => "SecRuleRemoveByMsg", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1,msg:'testing rule'" + SecRuleRemoveByMsg "testing rule" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, diff --git a/apache2/t/regression/server_root/conf/httpd.conf.in b/apache2/t/regression/server_root/conf/httpd.conf.in new file mode 100644 index 00000000..7d45cdbf --- /dev/null +++ b/apache2/t/regression/server_root/conf/httpd.conf.in @@ -0,0 +1,35 @@ +### Base configuration for starting Apache httpd + + + # File locations + PidFile @MSC_REGRESSION_LOGS_DIR@/httpd.pid + ScoreBoardFile @MSC_REGRESSION_LOGS_DIR@/httpd.scoreboard + + + + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + + + LoadModule unique_id_module modules/mod_unique_id.so + + + + # TODO: Need to have these configurable + LoadFile /usr/lib/libxml2.so + LoadFile /usr/lib/liblua5.1.so + LoadModule security2_module modules/mod_security2.so + + +ServerName localhost + +LogLevel debug +ErrorLog @MSC_REGRESSION_LOGS_DIR@/error.log + + + DocumentRoot @MSC_REGRESSION_DOCROOT_DIR@ + + Options Indexes FollowSymLinks + AllowOverride None + + diff --git a/apache2/t/regression/server_root/conf/match.lua b/apache2/t/regression/server_root/conf/match.lua new file mode 100644 index 00000000..fafd39b1 --- /dev/null +++ b/apache2/t/regression/server_root/conf/match.lua @@ -0,0 +1,14 @@ +-- Test matching Lua Script to just print debug messages +function main() + m.log(1, "Test message."); + m.log(2, "Test message."); + m.log(3, "Test message."); + m.log(4, "Test message."); + m.log(5, "Test message."); + m.log(6, "Test message."); + m.log(7, "Test message."); + m.log(8, "Test message."); + m.log(9, "Test message."); + + return "Lua script matched."; +end diff --git a/apache2/t/regression/server_root/conf/test.lua b/apache2/t/regression/server_root/conf/test.lua new file mode 100644 index 00000000..1cff076d --- /dev/null +++ b/apache2/t/regression/server_root/conf/test.lua @@ -0,0 +1,14 @@ +-- Test Lua Script to just print debug messages +function main() + m.log(1, "Test message."); + m.log(2, "Test message."); + m.log(3, "Test message."); + m.log(4, "Test message."); + m.log(5, "Test message."); + m.log(6, "Test message."); + m.log(7, "Test message."); + m.log(8, "Test message."); + m.log(9, "Test message."); + + return nil; +end diff --git a/apache2/t/regression/server_root/htdocs/8k.txt b/apache2/t/regression/server_root/htdocs/8k.txt new file mode 100644 index 00000000..6d17cf9d Binary files /dev/null and b/apache2/t/regression/server_root/htdocs/8k.txt differ diff --git a/apache2/t/regression/server_root/htdocs/index.html b/apache2/t/regression/server_root/htdocs/index.html new file mode 100644 index 00000000..16c51c1e --- /dev/null +++ b/apache2/t/regression/server_root/htdocs/index.html @@ -0,0 +1 @@ +INDEX diff --git a/apache2/t/regression/server_root/htdocs/test.txt b/apache2/t/regression/server_root/htdocs/test.txt new file mode 100644 index 00000000..2a02d41c --- /dev/null +++ b/apache2/t/regression/server_root/htdocs/test.txt @@ -0,0 +1 @@ +TEST diff --git a/apache2/t/regression/server_root/htdocs/test2.txt b/apache2/t/regression/server_root/htdocs/test2.txt new file mode 100644 index 00000000..55d8fa4b --- /dev/null +++ b/apache2/t/regression/server_root/htdocs/test2.txt @@ -0,0 +1 @@ +TEST 2 diff --git a/apache2/t/regression/target/00-targets.t b/apache2/t/regression/target/00-targets.t new file mode 100644 index 00000000..9a89f3b5 --- /dev/null +++ b/apache2/t/regression/target/00-targets.t @@ -0,0 +1,487 @@ +### Test basic targets + +# ARGS +{ + type => "target", + comment => "ARGS (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS "val1" "phase:2,log,pass" + SecRule ARGS "val2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS.*Pattern match "val2" at ARGS/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS "val1" "phase:2,log,pass" + SecRule ARGS "val2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS.*Pattern match "val2" at ARGS/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_COMBINED_SIZE +{ + type => "target", + comment => "ARGS_COMBINED_SIZE (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule ARGS_COMBINED_SIZE "\@eq 16" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Operator EQ matched 16 at ARGS_COMBINED_SIZE\./s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_COMBINED_SIZE (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecRule ARGS_COMBINED_SIZE "\@eq 16" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Operator EQ matched 16 at ARGS_COMBINED_SIZE\./s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_NAMES +{ + type => "target", + comment => "ARGS_NAMES (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_NAMES "arg1" "phase:2,log,pass" + SecRule ARGS_NAMES "arg2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS.*Pattern match "arg2" at ARGS/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_NAMES (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_NAMES "arg1" "phase:2,log,pass" + SecRule ARGS_NAMES "arg2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS_NAMES.*Pattern match "arg2" at ARGS_NAMES/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_GET +{ + type => "target", + comment => "ARGS_GET (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET "val1" "phase:2,log,pass" + SecRule ARGS_GET "val2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS_GET.*Pattern match "val2" at ARGS_GET/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_GET (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET "val1" "phase:2,log,pass" + SecRule ARGS_GET "val2" "phase:2,log,pass" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_GET_NAMES +{ + type => "target", + comment => "ARGS_GET_NAMES (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET_NAMES "arg1" "phase:2,log,pass" + SecRule ARGS_GET_NAMES "arg2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS_GET.*Pattern match "arg2" at ARGS_GET/s, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_GET_NAMES (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_GET_NAMES "arg1" "phase:2,log,pass" + SecRule ARGS_GET_NAMES "arg2" "phase:2,log,pass" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_POST +{ + type => "target", + comment => "ARGS_POST (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST "val1" "phase:2,log,pass" + SecRule ARGS_POST "val2" "phase:2,log,pass" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_POST (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST "val1" "phase:2,log,pass" + SecRule ARGS_POST "val2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "val1" at ARGS_POST.*Pattern match "val2" at ARGS_POST/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# ARGS_POST_NAMES +{ + type => "target", + comment => "ARGS_POST_NAMES (get)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST_NAMES "arg1" "phase:2,log,pass" + SecRule ARGS_POST_NAMES "arg2" "phase:2,log,pass" + ), + match_log => { + -error => [ qr/Pattern match/, 1 ], + debug => [ qr/Adding request argument \(QUERY_STRING\): name "arg1", value "val1".*Adding request argument \(QUERY_STRING\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt?arg1=val1&arg2=val2", + ), +}, +{ + type => "target", + comment => "ARGS_POST_NAMES (post)", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType null + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule ARGS_POST_NAMES "arg1" "phase:2,log,pass" + SecRule ARGS_POST_NAMES "arg2" "phase:2,log,pass" + ), + match_log => { + error => [ qr/Pattern match "arg1" at ARGS_POST.*Pattern match "arg2" at ARGS_POST/s, 1 ], + debug => [ qr/Adding request argument \(BODY\): name "arg1", value "val1".*Adding request argument \(BODY\): name "arg2", value "val2"/s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + "arg1=val1&arg2=val2", + ), +}, + +# AUTH_TYPE +#{ +# type => "target", +# comment => "AUTH_TYPE", +# conf => qq( +# = 2.2> +# +# LoadModule authn_file_module modules/mod_authn_file.so +# +# +## +## +## LoadModule auth_module modules/mod_auth.so +## +## +# +# AuthType Basic +# AuthName Test +# AuthUserFile "$ENV{CONF_DIR}/htpasswd" +# Require user nobody +# +# SecRuleEngine On +# SecRequestBodyAccess On +# SecResponseBodyAccess On +# SecResponseBodyMimeType null +## SecDebugLog $ENV{DEBUG_LOG} +## SecDebugLogLevel 9 +# SecRule REQUEST_HEADERS:Authorization "Basic (.*)" "phase:2,log,pass,capture,chain" +# SecRule TX:1 "nobody:test" "t:none,t:base64Decode,chain" +# SecRule AUTH_TYPE "Basic" +# ), +# match_log => { +# error => [ qr/Pattern match "Basic" at AUTH_TYPE/s, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", +# [ +# "Authorization" => "Basic bm9ib2R5OnRlc3Q=" +# ], +# ), +#}, + +# TODO: ENV +# TODO: FILES +# TODO: FILES_COMBINED_SIZE +# TODO: FILES_NAMES +# TODO: FILES_SIZES +# TODO: FILES_TMPNAMES +# TODO: GEO +# TODO: HIGHEST_SEVERITY +# TODO: MATCHED_VAR +# TODO: MATCHED_VAR_NAME +# TODO: MODSEC_BUILD +# TODO: MULTIPART_CRLF_LF_LINES +# TODO: MULTIPART_STRICT_ERROR +# TODO: MULTIPART_UNMATCHED_BOUNDARY +# TODO: PATH_INFO +# TODO: QUERY_STRING +# TODO: REMOTE_ADDR +# TODO: REMOTE_HOST +# TODO: REMOTE_PORT +# TODO: REMOTE_USER +# TODO: REQBODY_PROCESSOR +# TODO: REQBODY_PROCESSOR_ERROR +# TODO: REQBODY_PROCESSOR_ERROR_MSG +# TODO: REQUEST_BASENAME +# TODO: REQUEST_BODY +# TODO: REQUEST_COOKIES +# TODO: REQUEST_COOKIES_NAMES +# TODO: REQUEST_FILENAME +# TODO: REQUEST_HEADERS +# TODO: REQUEST_HEADERS_NAMES +# TODO: REQUEST_LINE +# TODO: REQUEST_METHOD +# TODO: REQUEST_PROTOCOL +# TODO: REQUEST_URI +# TODO: REQUEST_URI_RAW +# TODO: RESPONSE_BODY +# TODO: RESPONSE_CONTENT_LENGTH +# TODO: RESPONSE_CONTENT_TYPE +# TODO: RESPONSE_HEADERS +# TODO: RESPONSE_HEADERS_NAMES +# TODO: RESPONSE_PROTOCOL +# TODO: RESPONSE_STATUS +# TODO: RULE +# TODO: SCRIPT_BASENAME +# TODO: SCRIPT_FILENAME +# TODO: SCRIPT_GID +# TODO: SCRIPT_GROUPNAME +# TODO: SCRIPT_MODE +# TODO: SCRIPT_UID +# TODO: SCRIPT_USERNAME +# TODO: SERVER_ADDR +# TODO: SERVER_NAME +# TODO: SERVER_PORT +# TODO: SESSION +# TODO: SESSIONID +# TODO: TIME +# TODO: TIME_DAY +# TODO: TIME_EPOCH +# TODO: TIME_HOUR +# TODO: TIME_MIN +# TODO: TIME_MON +# TODO: TIME_SEC +# TODO: TIME_WDAY +# TODO: TIME_YEAR +# TODO: TX +# TODO: USERID +# TODO: WEBAPPID +# TODO: WEBSERVER_ERROR_LOG +# TODO: XML + diff --git a/apache2/t/run-regression-tests.pl.in b/apache2/t/run-regression-tests.pl.in new file mode 100755 index 00000000..032c5b41 --- /dev/null +++ b/apache2/t/run-regression-tests.pl.in @@ -0,0 +1,738 @@ +#!@PERL@ +# +# Run regression tests. +# +# Syntax: run-regression-tests.pl [options] [file [N]] +# +# All: run-regression-tests.pl +# All in file: run-regression-tests.pl file +# Nth in file: run-regression-tests.pl file N +# +use strict; +use Time::HiRes qw(gettimeofday sleep); +use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG); +use File::Spec qw(rel2abs); +use File::Basename qw(basename dirname); +use FileHandle; +use IPC::Open2 qw(open2); +use IPC::Open3 qw(open3); +use Getopt::Std; +use Data::Dumper; +use IO::Socket; +use LWP::UserAgent; + +my @TYPES = qw(config misc action target rule); +my $SCRIPT = basename($0); +my $SCRIPT_DIR = File::Spec->rel2abs(dirname($0)); +my $REG_DIR = "$SCRIPT_DIR/regression"; +my $SROOT_DIR = "$REG_DIR/server_root"; +my $DATA_DIR = "$SROOT_DIR/data"; +my $TEMP_DIR = "$SROOT_DIR/tmp"; +my $UPLOAD_DIR = "$SROOT_DIR/upload"; +my $CONF_DIR = "$SROOT_DIR/conf"; +my $FILES_DIR = "$SROOT_DIR/logs"; +my $PID_FILE = "$FILES_DIR/httpd.pid"; +my $HTTPD = q(@APXS_HTTPD@); +my $PASSED = 0; +my $TOTAL = 0; +my %C = (); +my %FILE = (); +my $UA_NAME = "ModSecurity Regression Tests/1.2.3"; +my $UA = LWP::UserAgent->new; +$UA->agent($UA_NAME); + +# Hack for testing the script w/o configure +if ($HTTPD eq "\@APXS_HTTPD\@") { + $HTTPD = "/usr/local/apache2/bin/httpd"; +} + +$SIG{TERM} = $SIG{INT} = \&handle_interrupt; + +my %opt; +getopts('A:E:D:C:T:H:a:p:dh', \%opt); + +if ($opt{D}) { + $Data::Dumper::Indent = 1; + $Data::Dumper::Terse = 1; + $Data::Dumper::Pad = ""; + $Data::Dumper::Quotekeys = 0; +} + +sub usage { + print stderr <<"EOT"; +@_ +Usage: $SCRIPT [options] [file [N]] + + Options: + -A file Specify ModSecurity audit log to read. + -D file Specify ModSecurity debug log to read. + -E file Specify Apache httpd error log to read. + -C file Specify Apache httpd base conf file to generate/reload. + -H path Specify Apache httpd htdocs path. + -S path Specify Apache httpd server root path. + -a file Specify Apache httpd binary (default: httpd) + -p port Specify Apache httpd port (default: 8088) + -d Enable debugging. + -h This help. + +EOT + + exit(1); +} + +usage() if ($opt{h}); + +### Check httpd binary +if (defined $opt{a}) { + $HTTPD = $opt{a}; +} +else { + $opt{a} = $HTTPD; +} +usage("Invalid Apache startup script: $HTTPD\n") unless (-e $HTTPD); + +### Defaults +$opt{A} = "$FILES_DIR/modsec_audit.log" unless (defined $opt{A}); +$opt{D} = "$FILES_DIR/modsec_debug.log" unless (defined $opt{D}); +$opt{E} = "$FILES_DIR/error.log" unless (defined $opt{E}); +$opt{C} = "$CONF_DIR/httpd.conf" unless (defined $opt{C}); +$opt{H} = "$SROOT_DIR/htdocs" unless (defined $opt{H}); +$opt{p} = 8088 unless (defined $opt{p}); + +unless (defined $opt{S}) { + my $httpd_root = `$HTTPD -V`; + ($opt{S} = $httpd_root) =~ s/.*-D HTTPD_ROOT="([^"]*)".*/$1/sm; +} + +%ENV = ( + %ENV, + SERVER_ROOT => $opt{S}, + SERVER_PORT => $opt{p}, + SERVER_NAME => "localhost", + TEST_SERVER_ROOT => $SROOT_DIR, + DATA_DIR => $DATA_DIR, + TEMP_DIR => $TEMP_DIR, + UPLOAD_DIR => $UPLOAD_DIR, + CONF_DIR => $CONF_DIR, + LOGS_DIR => $FILES_DIR, + SCRIPT_DIR => $SCRIPT_DIR, + REGRESSION_DIR => $REG_DIR, + DIST_ROOT => File::Spec->rel2abs(dirname("$SCRIPT_DIR/../../..")), + AUDIT_LOG => $opt{A}, + DEBUG_LOG => $opt{D}, + ERROR_LOG => $opt{E}, + HTTPD_CONF => $opt{C}, + HTDOCS => $opt{H}, + USER_AGENT => $UA_NAME, +); + +#dbg("OPTIONS: ", \%opt); + +if (-e "$PID_FILE") { + msg("Shutting down previous instance: $PID_FILE"); + httpd_stop(); +} + +if (defined $ARGV[0]) { + runfile(dirname($ARGV[0]), basename($ARGV[0]), $ARGV[1]); + done(); +} + +for my $type (@TYPES) { + my $dir = "$SCRIPT_DIR/regression/$type"; + my @cfg = (); + + # Get test names + opendir(DIR, "$dir") or quit(1, "Failed to open \"$dir\": $!"); + @cfg = grep { /\.t$/ && -f "$dir/$_" } readdir(DIR); + closedir(DIR); + + for my $cfg (sort @cfg) { + runfile($dir, $cfg); + } + +} +done(); + + +sub runfile { + my($dir, $cfg, $testnum) = @_; + my $fn = "$dir/$cfg"; + my @data = (); + my $edata; + my @C = (); + my @test = (); + my $teststr; + my $n = 0; + my $pass = 0; + + open(CFG, "<$fn") or quit(1, "Failed to open \"$fn\": $!"); + @data = ; + + $edata = q/@C = (/ . join("", @data) . q/)/; + eval $edata; + quit(1, "Failed to read test data \"$cfg\": $@") if ($@); + + unless (@C) { + msg("\nNo tests defined for $fn"); + return; + } + + msg("\nLoaded ".@C." tests from $fn"); + for my $t (@C) { + $n++; + next if (defined $testnum and $n != $testnum); + + my $httpd_up = 0; + my %t = %{$t || {}}; + my $id = sprintf("%3d", $n); + my $out = ""; + my $rc = 0; + my $conf_fn; + + # Startup httpd with optionally included conf. + if (exists $t{conf} and defined $t{conf}) { + $conf_fn = sprintf "%s/%s_%s_%06d.conf", + $CONF_DIR, $t{type}, $cfg, $n; +# dbg("Writing test config to: $conf_fn"); + open(CONF, ">$conf_fn") or die "Failed to open conf \"$conf_fn\": $!\n"; + print CONF (ref $t{conf} eq "CODE" ? eval { &{$t{conf}} } : $t{conf}); + msg("$@") if ($@); + close CONF; + $httpd_up = httpd_start(\%t, "Include $conf_fn") ? 0 : 1; + } + else { + $httpd_up = httpd_start(\%t) ? 0 : 1; + } + + # Run any prerun setup + if ($rc == 0 and exists $t{prerun} and defined $t{prerun}) { + dbg("Executing perl prerun..."); + $rc = &{$t{prerun}}; + dbg("Perl prerun returned: $rc"); + } + + if ($httpd_up) { + # Perform the request and check response + if (exists $t{request}) { + my $resp = do_request($t{request}); + if (!$resp) { + msg("invalid response"); + dbg("RESPONSE: ", $resp); + $rc = 1; + } + else { + for my $key (keys %{ $t{match_response} || {}}) { + my($neg,$mtype) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_response}{$key}; + my $match = match_response($mtype, $resp, $m); + if ($neg and defined $match) { + $rc = 1; + msg("response $mtype matched: $m"); + dbg($resp); + + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("response $mtype failed to match: $m"); + dbg($resp); + last; + } + } + } + } + + # Run any arbitrary perl tests + if ($rc == 0 and exists $t{test} and defined $t{test}) { + #dbg("Executing perl test(s)..."); + $rc = eval { &{$t{test}} }; + if (! defined $rc) { + msg("Error running test: $@"); + $rc = -1; + } + #dbg("Perl tests returned: $rc"); + } + + # Search for all log matches + if ($rc == 0 and exists $t{match_log} and defined $t{match_log}) { + for my $key (keys %{ $t{match_log} || {}}) { + my($neg,$mtype) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_log}{$key}; + my $match = match_log($mtype, @{$m || []}); + if ($neg and defined $match) { + $rc = 1; + msg("$mtype log matched: $m->[0]"); + msg("Log: $FILE{$mtype}{fn}"); + dbg(escape("$FILE{$mtype}{buf}")); + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("$mtype log failed to match: $m->[0]"); + msg("Log: $FILE{$mtype}{fn}"); + dbg(escape("$FILE{$mtype}{buf}")); + last; + } + } + } + + # Search for all file matches + if ($rc == 0 and exists $t{match_file} and defined $t{match_file}) { + for my $key (keys %{ $t{match_file} || {}}) { + my($neg,$fn) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_file}{$key}; + my $match = match_file($fn, $m); + if ($neg and defined $match) { + $rc = 1; + msg("$fn file matched: $m"); + dbg(escape("$FILE{$fn}{buf}")); + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("$fn file failed match: $m"); + dbg(escape("$FILE{$fn}{buf}")); + last; + } + } + } + } + else { + msg("Failed to start httpd."); + $rc = 1; + } + + if ($rc == 0) { + $pass++; + } + else { + dbg("Test config: $conf_fn"); + } + + msg(sprintf("%s) %s%s: %s%s", $id, $t{type}, (exists($t{comment}) ? " - $t{comment}" : ""), ($rc ? "failed" : "passed"), ((defined($out) && $out ne "")? " ($out)" : ""))); + + if ($httpd_up) { + $httpd_up = httpd_stop(\%t) ? 0 : 1; + } + + } + + $TOTAL += $testnum ? 1 : $n; + $PASSED += $pass; + + msg(sprintf("Passed: %2d; Failed: %2d", $pass, $testnum ? (1 - $pass) : ($n - $pass))); +} + +# Take out any indenting and translate LF -> CRLF +sub normalize_raw_request_data { + my $r = $_[0]; + + # Allow for indenting in test file + $r =~ s/^[ \t]*\x0d?\x0a//s; + my($indention) = ($r =~ m/^([ \t]*)/s); # indention taken from first line + $r =~ s/^$indention//mg; + $r =~ s/(\x0d?\x0a)[ \t]+$/$1/s; + + # Translate LF to CRLF + $r =~ s/^\x0a/\x0d\x0a/mg; + $r =~ s/([^\x0d])\x0a/$1\x0d\x0a/mg; + + return $r; +} + +sub do_raw_request { + my $sock = new IO::Socket::INET( + Proto => "tcp", + PeerAddr => "localhost", + PeerPort => $opt{p}, + ) or msg("Failed to connect to localhost:$opt{p}: $@"); + return unless ($sock); + + # Join togeather the request + my $r = join("", @_); + dbg($r); + + # Write to socket + print $sock "$r"; + $sock->shutdown(1); + + # Read from socket + my @resp = <$sock>; + $sock->close(); + + return HTTP::Response->parse(join("", @resp)); +} + +sub do_request { + my $r = $_[0]; + + # Allow test to execute code + if (ref $r eq "CODE") { + $r = eval { &$r }; + msg("$@") unless (defined $r); + } + + if (ref $r eq "HTTP::Request") { +# dbg("REQUEST: ", $r); + my $resp = $UA->request($r); + if ($opt{d}) { + dbg($resp->request()->as_string()); + } + return $resp + } + else { +# dbg("REQUEST:\n", $r); + return do_raw_request($r); + } + + return; +} + + +sub match_response { + my($name, $resp, $re) = @_; + + msg("Warning: Empty regular expression.") if (!defined $re or $re eq ""); + + if ($name eq "status") { + return $& if ($resp->code =~ m/$re/); + } + elsif ($name eq "content") { + return $& if ($resp->content =~ m/$re/m); + } + elsif ($name eq "raw") { + return $& if ($resp->as_string =~ m/$re/m); + } + + return; +} + +sub match_log { + my($name, $re, $timeout) = @_; + my $t0 = gettimeofday; + my($fh,$rbuf) = ($FILE{$name}{fd}, \$FILE{$name}{buf}); + my $n = length($$rbuf); + + msg("Warning: Empty regular expression.") if (!defined $re or $re eq ""); + + unless (defined $fh) { + msg("Error: File \"$name\" is not opened for matching."); + return; + } + + $timeout = 0 unless (defined $timeout); + + do { + $n += $fh->sysread($$rbuf, 1024, $n); +# dbg("Match \"$re\" in $name \"$$rbuf\" ($n)"); + return $& if ($$rbuf =~ m/$re/m); + # TODO: Use select()/poll() + sleep 0.1; + } while (gettimeofday - $t0 < $timeout); + + return; +} + +sub match_file { + my($neg,$fn) = ($_[0] =~ m/^(-?)(.*)$/); + unless (exists $FILE{$fn}) { + eval { + $FILE{$fn}{fn} = $fn; + $FILE{$fn}{fd} = new FileHandle($fn, O_RDONLY) or die "$!\n"; + $FILE{$fn}{fd}->blocking(0); + $FILE{$fn}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$fn\": $@"); + return; + } + } + return match_log($_[0], $_[1]); # timeout makes no sense +} + +sub quote_shell { + my($s) = @_; + return $s unless ($s =~ m|[^\w!%+,\-./:@^]|); + $s =~ s/(['\\])/\\$1/g; + return "'$s'"; +} + +sub escape { + my @new = (); + for my $c (split(//, $_[0])) { + my $oc = ord($c); + push @new, ((($oc >= 0x20 and $oc <= 0x7e) or $oc == 0x0a or $oc == 0x0d) ? $c : sprintf("\\x%02x", ord($c))); + } + join('', @new); +} + +sub dbg { + return unless(@_ and $opt{d}); + my $out = join "", map { + (ref $_ ne "" ? Dumper($_) : $_) + } @_; + $out =~ s/^/DBG: /mg; + print STDOUT "$out\n"; +} + +sub msg { + return unless(@_); + my $out = join "", map { + (ref $_ ne "" ? Dumper($_) : $_) + } @_; + print STDOUT "$out\n"; +} + +sub handle_interrupt { + $SIG{TERM} = $SIG{INT} = \&handle_interrupt; + + msg("Interrupted via SIG$_[0]. Shutting down tests..."); + httpd_stop(); + + quit(1); +} + +sub quit { + my($ec,$msg) = @_; + $ec = 0 unless (defined $_[0]); + + msg("$msg") if (defined $msg); + + exit $ec; +} + +sub done { + if ($PASSED != $TOTAL) { + quit(1, "\n$PASSED/$TOTAL tests passed."); + } + + quit(0, "\nAll tests passed ($TOTAL)."); +} + +sub httpd_start { + my $t = shift; + httpd_reset_fd($t); + my @p = ( + $HTTPD, + -d => $opt{S}, + -f => $opt{C}, + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), + -k => "start", + ); + + my $httpd_out; + my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); + my $out = join("\\n", split(/\n/, <$httpd_out>)); + close $httpd_out; + waitpid($httpd_pid, 0); + + my $rc = $?; + if ( WIFEXITED($rc) ) { + $rc = WEXITSTATUS($rc); + dbg("Httpd start returned with $rc.") if ($rc); + } + elsif( WIFSIGNALED($rc) ) { + msg("Httpd start failed with signal " . WTERMSIG($rc) . "."); + $rc = -1; + } + else { + msg("Httpd start failed with unknown error."); + $rc = -1; + } + + if (defined $out and $out ne "") { + dbg(join(" ", map { quote_shell($_) } @p)); + msg("Httpd start failed with error messages:\n$out"); + return -1 + } + + # Look for startup msg + unless (defined match_log("error", qr/resuming normal operations/, 10)) { + dbg(join(" ", map { quote_shell($_) } @p)); + dbg(match_log("error", qr/(^.*ModSecurity: .*)/sm, 10)); + msg("Httpd server failed to start."); + return -1; + } + + return $rc; +} + +sub httpd_stop { + my $t = shift; + my @p = ( + $HTTPD, + -d => $opt{S}, + -f => $opt{C}, + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), + -k => "stop", + ); + + my $httpd_out; + my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); + my $out = join("\\n", split(/\n/, <$httpd_out>)); + close $httpd_out; + waitpid($httpd_pid, 0); + + if (defined $out and $out ne "") { + msg("Httpd stop failed with error messages:\n$out"); + return -1 + } + + my $rc = $?; + if ( WIFEXITED($rc) ) { + $rc = WEXITSTATUS($rc); + dbg("Httpd stop returned with $rc.") if ($rc); + } + elsif( WIFSIGNALED($rc) ) { + msg("Httpd stop failed with signal " . WTERMSIG($rc) . "."); + $rc = -1; + } + else { + msg("Httpd stop failed with unknown error."); + $rc = -1; + } + + # Look for startup msg + unless (defined match_log("error", qr/caught SIG[A-Z]+, shutting down/, 10)) { + dbg(join(" ", map { quote_shell($_) } @p)); + msg("Httpd server failed to shutdown."); + return -1; + } + + return $rc; +} + +sub httpd_reload { + my $t = shift; + httpd_reset_fd($t); + my @p = ( + $HTTPD, + -d => $opt{S}, + -f => $opt{C}, + (map { (-c => $_) } ("Listen localhost:$opt{p}", @_)), + -k => "graceful", + ); + + my $httpd_out; + my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); + my $out = join("\\n", split(/\n/, <$httpd_out>)); + close $httpd_out; + waitpid($httpd_pid, 0); + + if (defined $out and $out ne "") { + msg("Httpd reload failed with error messages:\n$out"); + return -1 + } + + my $rc = $?; + if ( WIFEXITED($rc) ) { + $rc = WEXITSTATUS($rc); + dbg("Httpd reload returned with $rc.") if ($rc); + } + elsif( WIFSIGNALED($rc) ) { + msg("Httpd reload failed with signal " . WTERMSIG($rc) . "."); + $rc = -1; + } + else { + msg("Httpd reload failed with unknown error."); + $rc = -1; + } + + # Look for startup msg + unless (defined match_log("error", qr/resuming normal operations/, 10)) { + dbg(join(" ", map { quote_shell($_) } @p)); + msg("Httpd server failed to reload."); + return -1; + } + + return $rc; +} + +sub httpd_reset_fd { + my($t) = @_; + + # Cleanup + for my $key (keys %FILE) { + if (exists $FILE{$key}{fd} and defined $FILE{$key}{fd}) { + $FILE{$key}{fd}->close(); + } + delete $FILE{$key}; + } + + # Error + eval { + $FILE{error}{fn} = $opt{E}; + $FILE{error}{fd} = new FileHandle($opt{E}, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{error}{fd}->blocking(0); + $FILE{error}{fd}->sysseek(0, 2); + $FILE{error}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$opt{E}\": $@"); + return undef; + } + + # Audit + eval { + $FILE{audit}{fn} = $opt{A}; + $FILE{audit}{fd} = new FileHandle($opt{A}, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{audit}{fd}->blocking(0); + $FILE{audit}{fd}->sysseek(0, 2); + $FILE{audit}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$opt{A}\": $@"); + return undef; + } + + # Debug + eval { + $FILE{debug}{fn} = $opt{D}; + $FILE{debug}{fd} = new FileHandle($opt{D}, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{debug}{fd}->blocking(0); + $FILE{debug}{fd}->sysseek(0, 2); + $FILE{debug}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$opt{D}\": $@"); + return undef; + } + + # Any extras listed in "match_log" + if ($t and exists $t->{match_log}) { + for my $k (keys %{ $t->{match_log} || {} }) { + my($neg,$fn) = ($k =~ m/^(-?)(.*)$/); + next if (!$fn or exists $FILE{$fn}); + eval { + $FILE{$fn}{fn} = $fn; + $FILE{$fn}{fd} = new FileHandle($fn, O_RDWR|O_CREAT) or die "$!\n"; + $FILE{$fn}{fd}->blocking(0); + $FILE{$fn}{fd}->sysseek(0, 2); + $FILE{$fn}{buf} = ""; + }; + if ($@) { + msg("Warning: Failed to open file \"$fn\": $@"); + return undef; + } + } + } +} + +sub encode_chunked { + my($data, $size) = @_; + $size = 128 unless ($size); + my $chunked = ""; + + my $n = 0; + my $bytes = length($data); + while ($bytes >= $size) { + $chunked .= sprintf "%x\x0d\x0a%s\x0d\x0a", $size, substr($data, $n, $size); + $n += $size; + $bytes -= $size; + } + if ($bytes) { + $chunked .= sprintf "%x\x0d\x0a%s\x0d\x0a", $bytes, substr($data, $n, $bytes); + } + $chunked .= "0\x0d\x0a\x0d\x0a" +} diff --git a/apache2/t/run-unit-tests.pl.in b/apache2/t/run-unit-tests.pl.in new file mode 100755 index 00000000..845061fc --- /dev/null +++ b/apache2/t/run-unit-tests.pl.in @@ -0,0 +1,161 @@ +#!@PERL@ +# +# Run unit tests. +# +# Syntax: +# All: run-tests.pl +# All in file: run-tests.pl file +# Nth in file: run-tests.pl file N +# +use strict; +use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG); +use File::Basename qw(basename dirname); +use FileHandle; +use IPC::Open2 qw(open2); + +my @TYPES = qw(tfn op action); +my $TEST = "./msc_test"; +my $SCRIPT = basename($0); +my $SCRIPTDIR = dirname($0); +my $PASSED = 0; +my $TOTAL = 0; +my $DEBUG = $ENV{MSC_TEST_DEBUG} || 0; + +if (defined $ARGV[0]) { + runfile(dirname($ARGV[0]), basename($ARGV[0]), $ARGV[1]); + done(); +} + +for my $type (sort @TYPES) { + my $dir = "$SCRIPTDIR/$type"; + my @cfg = (); + + # Get test names + opendir(DIR, "$dir") or quit(1, "Failed to open \"$dir\": $!"); + @cfg = grep { /\.t$/ && -f "$dir/$_" } readdir(DIR); + closedir(DIR); + + for my $cfg (sort @cfg) { + runfile($dir, $cfg); + } + +} +done(); + + +sub runfile { + my($dir, $cfg, $testnum) = @_; + my $fn = "$dir/$cfg"; + my @data = (); + my $edata; + my @C = (); + my @test = (); + my $teststr; + my $n = 0; + my $pass = 0; + + open(CFG, "<$fn") or quit(1, "Failed to open \"$fn\": $!"); + @data = ; + + $edata = q/@C = (/ . join("", @data) . q/)/; + eval $edata; + quit(1, "Failed to read test data \"$cfg\": $@") if ($@); + + unless (@C) { + msg("\nNo tests defined for $fn"); + return; + } + + msg("\nLoaded ".@C." tests from $fn"); + for my $t (@C) { + $n++; + next if (defined $testnum and $n != $testnum); + + my %t = %{$t || {}}; + my $id = sprintf("%6d", $n); + my $in = (exists($t{input}) and defined($t{input})) ? $t{input} : ""; + my $out; + my $test_in = new FileHandle(); + my $test_out = new FileHandle(); + my $test_pid; + my $rc = 0; + my $param; + + if ($t{type} eq "tfn") { + $param = escape($t{output}); + } + elsif ($t{type} eq "op") { + $param = escape($t{param}); + } + elsif ($t{type} eq "action") { + $param = escape($t{param}); + } + else { + quit(1, "Unknown type \"$t{type}\" - should be one of: " . join(",",@TYPES)); + } + + @test = ("-t", $t{type}, "-n", $t{name}, "-p", $param, "-D", "$DEBUG", (exists($t{ret}) ? ("-r", $t{ret}) : ()), (exists($t{iterations}) ? ("-I", $t{iterations}) : ()), (exists($t{prerun}) ? ("-P", $t{prerun}) : ())); + $teststr = "$TEST " . join(" ", map { "\"$_\"" } @test); + $test_pid = open2($test_out, $test_in, $TEST, @test) or quit(1, "Failed to execute test: $teststr\": $!"); + print $test_in "$in"; + close $test_in; + $out = join("\\n", split(/\n/, <$test_out>)); + close $test_out; + waitpid($test_pid, 0); + + $rc = $?; + if ( WIFEXITED($rc) ) { + $rc = WEXITSTATUS($rc); + } + elsif( WIFSIGNALED($rc) ) { + msg("Test exited with signal " . WTERMSIG($rc) . "."); + msg("Executed: $teststr"); + $rc = -1; + } + else { + msg("Test exited with unknown error."); + $rc = -1; + } + + if ($rc == 0) { + $pass++; + } + + msg(sprintf("%s) %s \"%s\"%s: %s%s", $id, $t{type}, $t{name}, (exists($t{comment}) ? " $t{comment}" : ""), ($rc ? "failed" : "passed"), ((defined($out) && $out ne "")? " ($out)" : ""))); + + } + + $TOTAL += $testnum ? 1 : $n; + $PASSED += $pass; + + msg(sprintf("Passed: %2d; Failed: %2d", $pass, $testnum ? (1 - $pass) : ($n - $pass))); +} + +sub escape { + my @new = (); + for my $c (split(//, $_[0])) { + push @new, ((ord($c) >= 0x20 and ord($c) <= 0x7e) ? $c : sprintf("\\x%02x", ord($c))); + } + join('', @new); +} + +sub msg { + print STDOUT "@_\n" if (@_); +} + +sub quit { + my($ec,$msg) = @_; + $ec = 0 unless (defined $_[0]); + + msg("$msg") if (defined $msg); + + exit $ec; +} + +sub done { + if ($PASSED != $TOTAL) { + quit(1, "\n$PASSED/$TOTAL tests passed."); + } + + quit(0, "\nAll tests passed ($TOTAL)."); +}