From 926426db4a0809dc9d2e07aad6810fe78113e95e Mon Sep 17 00:00:00 2001 From: ADD-SP Date: Mon, 5 Jul 2021 00:28:12 +0800 Subject: [PATCH] :sparkles: Advanced rules. --- .github/workflows/test.yml | 5 +- .gitignore | 6 +- Makefile | 3 + README-ZH-CN.md | 2 + README.md | 2 + assets/rules/advanced | 0 bison/parser.yacc | 589 ++++++++++++++++++++++++++++++ config | 89 ++++- docker/Dockerfile.alpine | 7 +- docker/Dockerfile.debian | 8 +- docs/.vuepress/config.js | 7 +- docs/advance/changes.md | 16 + docs/advance/directive.md | 9 + docs/advance/priority.md | 3 +- docs/advance/rule.md | 209 ++++++++++- docs/advance/upgrade.md | 15 + docs/guide/compatibility.md | 6 +- docs/guide/overview.md | 2 + docs/todo/advanced-rule.md | 116 ------ docs/todo/overview.md | 2 - docs/zh-cn/advance/changes.md | 18 +- docs/zh-cn/advance/directive.md | 8 + docs/zh-cn/advance/priority.md | 3 +- docs/zh-cn/advance/rule.md | 216 ++++++++++- docs/zh-cn/advance/upgrade.md | 13 + docs/zh-cn/guide/compatibility.md | 6 +- docs/zh-cn/guide/overview.md | 2 + docs/zh-cn/todo/advanced-rule.md | 116 ------ docs/zh-cn/todo/overview.md | 2 - flex/lexer.lex | 284 ++++++++++++++ inc/ngx_http_waf_module_check.h | 28 +- inc/ngx_http_waf_module_config.h | 224 +++++++----- inc/ngx_http_waf_module_ip_trie.h | 12 +- inc/ngx_http_waf_module_macro.h | 21 +- inc/ngx_http_waf_module_type.h | 79 +++- inc/ngx_http_waf_module_util.h | 209 ++++++++++- inc/ngx_http_waf_module_vm.h | 531 +++++++++++++++++++++++++++ src/ngx_http_waf_module_core.c | 8 +- 38 files changed, 2452 insertions(+), 424 deletions(-) create mode 100644 Makefile create mode 100644 assets/rules/advanced create mode 100644 bison/parser.yacc create mode 100644 docs/advance/upgrade.md delete mode 100644 docs/todo/advanced-rule.md create mode 100644 docs/zh-cn/advance/upgrade.md delete mode 100644 docs/zh-cn/todo/advanced-rule.md create mode 100644 flex/lexer.lex create mode 100644 inc/ngx_http_waf_module_vm.h diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b50c6000..e5533f99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,11 +48,12 @@ jobs: - name: Install dependencies run: | sudo apt-get --yes update - sudo apt-get install --yes libsodium23 libsodium-dev build-essential zlib1g-dev libpcre3 libpcre3-dev libssl-dev libxslt1-dev libxml2-dev libgeoip-dev libgd-dev libperl-dev uthash-dev + sudo apt-get install --yes libsodium23 libsodium-dev build-essential zlib1g-dev libpcre3 libpcre3-dev libssl-dev libxslt1-dev libxml2-dev libgeoip-dev libgd-dev libperl-dev uthash-dev flex bison sudo pip install lastversion - name: Download ${{ matrix.nginx-version }} run: | chmod 777 -R ${{ github.workspace }} + sudo make parser sudo git clone https://github.com/libinjection/libinjection.git inc/libinjection if [ ${{ matrix.nginx-version }} = 'stable nginx' ] ; then \ version='stable' ;\ @@ -70,7 +71,7 @@ jobs: else \ opt='--add-dynamic-module' ;\ fi - ./configure ${opt}=.. --with-cc-opt='-fstack-protector' + ./configure ${opt}=.. --with-cc-opt='-Wno-unused-but-set-variable -Wno-unused-function -fstack-protector-strong' - name: Install ${{ matrix.nginx-version }} run: | cd nginx-src diff --git a/.gitignore b/.gitignore index bdbe8fa5..7855244a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ docs/.vuepress/dist yarn.lock package-lock.json inc/libinjection -inc/libsodium \ No newline at end of file +inc/libsodium +inc/ngx_http_waf_module_lexer.h +inc/ngx_http_waf_module_parser.tab.h +src/ngx_http_waf_module_lexer.c +src/ngx_http_waf_module_parser.tab.c \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..439c2c74 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +parser: flex/lexer.lex bison/parser.yacc + @flex flex/lexer.lex + @bison --defines=inc/ngx_http_waf_module_parser.tab.h -L C -o src/ngx_http_waf_module_parser.tab.c bison/parser.yacc \ No newline at end of file diff --git a/README-ZH-CN.md b/README-ZH-CN.md index db48ea01..6f53f592 100644 --- a/README-ZH-CN.md +++ b/README-ZH-CN.md @@ -25,6 +25,7 @@ * 功能齐全:「网络应用防火墙」的基本功能都有。 * 安装方便:缺少依赖项时会自动提供解决方法。 * 使用方便:配置指令简单易懂,不用看文档都能猜到大概是什么意思。 +* 规则灵活:提供高级规则,将动作(如拦截或放行)和多个条件表达式组合起来。 * 高性能:经过较为极限的测试,启动本模块后 RPS(每秒请求数) 降低约 4%。测试说明和结果见使用文档。 ## 功能 @@ -40,6 +41,7 @@ * UserAgent 黑名单。 * Cookie 黑名单。 * Referer 黑白名单。 +* 高级规则,将动作(如拦截或放行)和多个条件表达式组合起来。 ## 使用文档 diff --git a/README.md b/README.md index b4e45ac0..ff91ade6 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Handy, High performance Nginx firewall module. * Full-featured: The basic functions of the web application firewall are available. * Easy to install: The solution is automatically provided when a dependency is missing. * Easy to use: directives are easy to understand and you can probably guess what they mean without reading the documentation. +* Flexible rules: Provide advanced rules that combine actions (such as block or allow) with multiple conditional expressions. * High performance: In more extreme tests, QPS(Queries Per Second) is reduced by about 4% after starting this module. See the documentation for details of the tests. ## Function @@ -41,6 +42,7 @@ Handy, High performance Nginx firewall module. * Block the specified Cookie. * Exceptional allow on specific Referer. * Block the specified Referer. +* Advanced rules that combine actions (such as block or allow) with multiple conditional expressions. ## Docs diff --git a/assets/rules/advanced b/assets/rules/advanced new file mode 100644 index 00000000..e69de29b diff --git a/bison/parser.yacc b/bison/parser.yacc new file mode 100644 index 00000000..55122de5 --- /dev/null +++ b/bison/parser.yacc @@ -0,0 +1,589 @@ +%{ + #include + #include + #include + #include + #include + #include + #include + int ngx_http_waf_lex (void); + void ngx_http_waf_error (UT_array* array, ngx_pool_t* pool, const char* msg); + void ngx_http_waf_gen_push_str_code(UT_array* array, char* str); + // int ngx_http_waf_gen_push_regexp_code(UT_array* array, char* str, ngx_pool_t* pool); + int ngx_http_waf_gen_push_ip_code(UT_array* array, char* str); + void ngx_http_waf_gen_push_client_ip_code(UT_array* array); + void ngx_http_waf_gen_push_url_code(UT_array* array); + void ngx_http_waf_gen_push_user_agent_code(UT_array* array); + void ngx_http_waf_gen_push_referer_code(UT_array* array); + void ngx_http_waf_gen_push_query_string_code(UT_array* array, char* index); + void ngx_http_waf_gen_push_header_in_code(UT_array* array, char* index); + void ngx_http_waf_gen_push_cookie_code(UT_array* array, char* index); + void ngx_http_waf_gen_int_code(UT_array* array, int num); + void ngx_http_waf_gen_push_op_not_code(UT_array* array); + void ngx_http_waf_gen_op_and_code(UT_array* array); + void ngx_http_waf_gen_op_or_code(UT_array* array); + void ngx_http_waf_gen_op_matches_code(UT_array* array); + void ngx_http_waf_gen_op_contains_code(UT_array* array); + void ngx_http_waf_gen_op_equals_code(UT_array* array); + void ngx_http_waf_gen_op_belong_to_code(UT_array* array); + void ngx_http_waf_gen_op_sqli_detn_code(UT_array* array); + void ngx_http_waf_gen_op_xss_detn_code(UT_array* array); + void ngx_http_waf_gen_act_ret_code(UT_array* array, int http_status); + void ngx_http_waf_gen_act_allow_code(UT_array* array); +%} + +%define api.prefix {ngx_http_waf_} +%parse-param {UT_array* array} {ngx_pool_t* pool} + +// %nterm id_rule if_rule do_rule conditon conditon_ex primary rule + +%nterm str_operand +%nterm str_op ip_op + +%token token_id +%token token_str token_index +%token token_int + +%token keyword_id keyword_if keyword_do keyword_or keyword_allow +%token keyword_and keyword_matches keyword_equals +%token token_break_line token_blank keyword_not +%token keyword_url keyword_contains keyword_return +%token keyword_query_string keyword_user_agent keyword_belong_to +%token keyword_referer keyword_client_ip keyword_header_in +%token keyword_sqli_detn keyword_xss_detn keyword_cookie + +%union { + int int_val; + unsigned int uint_val; + char id_val[256]; + char str_val[256]; + void (*push_op_code_pt)(UT_array* array); + struct { + int argc; + void (*no_str_pt)(UT_array* array); + void (*one_str_pt)(UT_array* array, char* str); + char* argv[4]; + } push_str_code_info; +} + + +%% + + + +base: + rule end + {} + + | %empty + {} + ; + +rule: + id_rule if_rule do_rule + {} + ; +end: + token_break_line token_break_line rule { } + | token_break_line token_break_line { } + | %empty { } + ; + + +id_rule: + keyword_id ':' token_blank token_id token_break_line + { ngx_http_waf_gen_push_str_code(array, $4); } + ; + +if_rule: + keyword_if ':' token_blank conditon token_break_line + { } + ; + +do_rule: + keyword_do ':' token_blank keyword_return '(' token_int ')' + { + ngx_http_waf_gen_act_ret_code(array, $6); + } + + | keyword_do ':' token_blank keyword_allow + { + ngx_http_waf_gen_act_allow_code(array); + } + ; + +conditon: + conditon keyword_and conditon_ex + { + ngx_http_waf_gen_op_and_code(array); + } + + | conditon_ex + { } + ; + +conditon_ex: + conditon_ex keyword_or primary + { + ngx_http_waf_gen_op_or_code(array); + } + | primary + { } + ; +primary: + '(' conditon ')' { } + + | keyword_not token_blank '(' conditon ')' + { + ngx_http_waf_gen_push_op_not_code(array); + } + + | str_operand str_op str_operand + { + switch ($3.argc) { + case 0: + $3.no_str_pt(array); + break; + case 1: + $3.one_str_pt(array, $3.argv[0]); + break; + default: + YYABORT; + } + + switch ($1.argc) { + case 0: + $1.no_str_pt(array); + break; + case 1: + $1.one_str_pt(array, $1.argv[0]); + break; + default: + YYABORT; + } + $2(array); + } + + | str_operand token_blank keyword_not str_op str_operand + { + switch ($5.argc) { + case 0: + $5.no_str_pt(array); + break; + case 1: + $5.one_str_pt(array, $5.argv[0]); + break; + default: + YYABORT; + } + + switch ($1.argc) { + case 0: + $1.no_str_pt(array); + break; + case 1: + $1.one_str_pt(array, $1.argv[0]); + break; + default: + YYABORT; + } + $4(array); + ngx_http_waf_gen_push_op_not_code(array); + } + + | keyword_sqli_detn token_blank str_operand + { + switch ($3.argc) { + case 0: + $3.no_str_pt(array); + break; + case 1: + $3.one_str_pt(array, $3.argv[0]); + break; + default: + YYABORT; + } + ngx_http_waf_gen_op_sqli_detn_code(array); + } + + | keyword_xss_detn token_blank str_operand + { + switch ($3.argc) { + case 0: + $3.no_str_pt(array); + break; + case 1: + $3.one_str_pt(array, $3.argv[0]); + break; + default: + YYABORT; + } + ngx_http_waf_gen_op_xss_detn_code(array); + } + + | keyword_client_ip ip_op str_operand + { + switch ($3.argc) { + case 0: + $3.no_str_pt(array); + break; + case 1: + $3.one_str_pt(array, $3.argv[0]); + break; + default: + YYABORT; + } + ngx_http_waf_gen_push_client_ip_code(array); + $2(array); + } + + | keyword_client_ip token_blank keyword_not ip_op str_operand + { + switch ($5.argc) { + case 0: + $5.no_str_pt(array); + break; + case 1: + $5.one_str_pt(array, $5.argv[0]); + break; + default: + YYABORT; + } + ngx_http_waf_gen_push_client_ip_code(array); + $4(array); + ngx_http_waf_gen_push_op_not_code(array); + } + + ; + +str_operand: + keyword_url + { + $$.argc = 0; + $$.no_str_pt = ngx_http_waf_gen_push_url_code; + } + + | keyword_user_agent + { + $$.argc = 0; + $$.no_str_pt = ngx_http_waf_gen_push_user_agent_code; + } + + | keyword_referer + { + $$.argc = 0; + $$.no_str_pt = ngx_http_waf_gen_push_referer_code; + } + + | token_str + { + $$.argc = 1; + $$.one_str_pt = ngx_http_waf_gen_push_str_code; + $$.argv[0] = strdup($1); + } + + | keyword_header_in token_index + { + $$.argc = 1; + $$.one_str_pt = ngx_http_waf_gen_push_header_in_code; + $$.argv[0] = strdup($2); + } + + | keyword_query_string token_index + { + $$.argc = 1; + $$.one_str_pt = ngx_http_waf_gen_push_query_string_code; + $$.argv[0] = strdup($2); + } + + | keyword_cookie token_index + { + $$.argc = 1; + $$.one_str_pt = ngx_http_waf_gen_push_cookie_code; + $$.argv[0] = strdup($2); + } + ; + +str_op: + keyword_contains + { + $$ = ngx_http_waf_gen_op_contains_code; + } + + | keyword_matches + { + $$ = ngx_http_waf_gen_op_matches_code; + } + + | keyword_equals + { + $$ = ngx_http_waf_gen_op_equals_code; + } + ; + +ip_op: + keyword_equals + { + $$ = ngx_http_waf_gen_op_equals_code; + } + + | keyword_belong_to + { + $$ = ngx_http_waf_gen_op_belong_to_code; + } + ; +%% + + +void +ngx_http_waf_gen_push_str_code(UT_array* array, char* str) { + vm_code_t code; + + code.type = VM_CODE_PUSH_STR; + code.argv.argc = 1; + code.argv.type[0] = VM_DATA_STR; + code.argv.value[0].str_val.data = (u_char*)strdup(str); + code.argv.value[0].str_val.len = strlen(str); + + utarray_push_back(array, &code); + free(code.argv.value[0].str_val.data); +} + + +void ngx_http_waf_gen_push_query_string_code(UT_array* array, char* index) { + vm_code_t code; + size_t len = strlen(index); + code.type = VM_CODE_PUSH_QUERY_STRING; + code.argv.argc = 1; + code.argv.type[0] = VM_DATA_STR; + code.argv.value[0].str_val.data = (u_char*)strdup(index); + code.argv.value[0].str_val.len = len; + + utarray_push_back(array, &code); + free(code.argv.value[0].str_val.data); +} + + +void ngx_http_waf_gen_push_header_in_code(UT_array* array, char* index) { + vm_code_t code; + size_t len = strlen(index); + code.type = VM_CODE_PUSH_HEADER_IN; + code.argv.argc = 1; + code.argv.type[0] = VM_DATA_STR; + code.argv.value[0].str_val.data = (u_char*)strdup(index); + code.argv.value[0].str_val.len = len; + + utarray_push_back(array, &code); + free(code.argv.value[0].str_val.data); +} + + +void ngx_http_waf_gen_push_cookie_code(UT_array* array, char* index) { + vm_code_t code; + size_t len = strlen(index); + code.type = VM_CODE_PUSH_COOKIE; + code.argv.argc = 1; + code.argv.type[0] = VM_DATA_STR; + code.argv.value[0].str_val.data = (u_char*)strdup(index); + code.argv.value[0].str_val.len = len; + + utarray_push_back(array, &code); + free(code.argv.value[0].str_val.data); +} + + +// int ngx_http_waf_gen_push_regexp_code(UT_array* array, char* str, ngx_pool_t* pool) { +// vm_code_t code; +// code.type = VM_CODE_PUSH_REGEXP; +// code.argv.argc = 1; +// code.argv.type[0] = VM_DATA_REGEXP; +// ngx_regex_compile_t regex_compile; +// u_char errstr[NGX_MAX_CONF_ERRSTR]; +// ngx_regex_elt_t* ngx_regex_elt = &(code.argv.value[0].regexp_val); +// ngx_memzero(®ex_compile, sizeof(ngx_regex_compile_t)); +// regex_compile.pattern.len = strlen(str); +// regex_compile.pattern.data = (u_char*)str; +// regex_compile.pool = pool; +// regex_compile.err.len = NGX_MAX_CONF_ERRSTR; +// regex_compile.err.data = errstr; +// if (ngx_regex_compile(®ex_compile) != NGX_OK) { +// return 0; +// } + +// ngx_regex_elt->name = (u_char*)strdup(str); +// ngx_regex_elt->regex = regex_compile.regex; + +// utarray_push_back(array, &code); +// return 1; +// } + + +int +ngx_http_waf_gen_push_ip_code(UT_array* array, char* str) { + vm_code_t code; + + code.type = VM_CODE_PUSH_IP; + code.argv.argc = 1; + + ngx_str_t ip_str; + ip_str.data = (u_char*)str; + ip_str.len = strlen(str); + if (parse_ipv4(ip_str, &(code.argv.value[0].ipv4_val)) == NGX_HTTP_WAF_SUCCESS) { + code.argv.type[0] = VM_DATA_IPV4; + } else if (parse_ipv6(ip_str, &(code.argv.value[0].ipv6_val)) == NGX_HTTP_WAF_SUCCESS) { + code.argv.type[0] = VM_DATA_IPV6; + } else { + return 0; + } + + utarray_push_back(array, &code); + return 1; +} + + +void +ngx_http_waf_gen_push_client_ip_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_PUSH_CLIENT_IP; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_push_url_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_PUSH_URL; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_push_user_agent_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_PUSH_USER_AGENT; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_push_referer_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_PUSH_REFERER; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_int_code(UT_array* array, int num) { + vm_code_t code; + code.type = VM_CODE_PUSH_INT; + code.argv.argc = 1; + code.argv.type[0] = VM_DATA_INT; + code.argv.value[0].int_val = num; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_push_op_not_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_NOT; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_op_and_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_AND; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_op_or_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_OR;; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + +void +ngx_http_waf_gen_op_matches_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_MATCHES; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + + +void +ngx_http_waf_gen_op_contains_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_CONTAINS; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_op_equals_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_EQUALS; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_op_belong_to_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_BELONG_TO; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_op_sqli_detn_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_SQLI_DETN; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_op_xss_detn_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_OP_XSS_DETN; + code.argv.argc = 0; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_act_ret_code(UT_array* array, int http_status) { + vm_code_t code; + code.type = VM_CODE_ACT_RETURN; + code.argv.argc = 1; + code.argv.type[0] = VM_DATA_INT; + code.argv.value[0].int_val = http_status; + utarray_push_back(array, &code); +} + + +void +ngx_http_waf_gen_act_allow_code(UT_array* array) { + vm_code_t code; + code.type = VM_CODE_ACT_ALLOW; + code.argv.argc = 0; + utarray_push_back(array, &code); +} \ No newline at end of file diff --git a/config b/config index f3b62212..b591c679 100644 --- a/config +++ b/config @@ -10,9 +10,14 @@ deps="$ngx_addon_dir/inc/ngx_http_waf_module_check.h \ $ngx_addon_dir/inc/ngx_http_waf_module_token_bucket_set.h \ $ngx_addon_dir/inc/ngx_http_waf_module_mem_pool.h \ $ngx_addon_dir/inc/ngx_http_waf_module_lru_cache.h \ - $ngx_addon_dir/inc/ngx_http_waf_module_under_attack.h" + $ngx_addon_dir/inc/ngx_http_waf_module_under_attack.h \ + $ngx_addon_dir/inc/ngx_http_waf_module_vm.h \ + $ngx_addon_dir/inc/ngx_http_waf_module_lexer.h \ + $ngx_addon_dir/inc/ngx_http_waf_module_parser.tab.h" srcs="$ngx_addon_dir/src/ngx_http_waf_module_core.c \ + $ngx_addon_dir/src/ngx_http_waf_module_lexer.c \ + $ngx_addon_dir/src/ngx_http_waf_module_parser.tab.c \ $ngx_addon_dir/inc/libinjection/src/libinjection_html5.c \ $ngx_addon_dir/inc/libinjection/src/libinjection_sqli.c \ $ngx_addon_dir/inc/libinjection/src/libinjection_xss.c" @@ -26,6 +31,88 @@ if [ -n "$LIB_UTHASH" ] ; then ngx_http_waf_module_inc_path="${ngx_http_waf_module_inc_path} ${LIB_UTHASH}/include" fi +which flex +if [ $? -ne 0 ] ; then + cat << END + +$0: error: the $ngx_addon_name module requires the flex. + +Please run: + On Ubuntu or Debian: + apt-get update && apt-get install --yes flex + On CentOS 7: + yum -y install flex + On Centos 8 or Fedora 33 or Fedora 34: + dnf install flex + On Alpine: + apk update && apk add --upgrade flex + On Arch: + 1. Enable the core repository on /etc/pacman.conf: + [core] + Include = /etc/pacman.d/mirrorlist + 2. Install flex xz package: + pacman -Syu flex + On FreeBSD 12 or FreeBSD 13: + pkg install flex +END + exit 1 + +fi + +which bison +if [ $? -ne 0 ] ; then + cat << END + +$0: error: the $ngx_addon_name module requires the bison. + +Please run: + On Ubuntu or Debian: + apt-get update && apt-get install --yes bison + On CentOS 7: + yum -y install bison + On Centos 8 or Fedora 33 or Fedora 34: + dnf install bison + On Alpine: + apk update && apk add --upgrade bison + On Arch: + 1. Enable the core repository on /etc/pacman.conf: + [core] + Include = /etc/pacman.d/mirrorlist + 2. Install flex xz package: + pacman -Syu bison + On FreeBSD 12 or FreeBSD 13: + pkg install bison + +END + exit 1 + +fi + + +if [ ! -e "${ngx_addon_dir}/inc/ngx_http_waf_module_lexer.h" -o ! -e "${ngx_addon_dir}/src/ngx_http_waf_module_lexer.c" ] ; then + cat << END + +$0: error: the $ngx_addon_name module requires the following command to be run to generate the necessary files. + + cd $ngx_addon_dir && flex flex/lexer.lex && cd $(pwd) + +END + exit 1 +fi + + +if [ ! -e "${ngx_addon_dir}/inc/ngx_http_waf_module_parser.tab.h" -o ! -e "${ngx_addon_dir}/src/ngx_http_waf_module_parser.tab.c" ] ; then + cat << END + +$0: error: the $ngx_addon_name module requires the following command to be run to generate the necessary files. + + cd $ngx_addon_dir \\ +&& bison --defines=inc/ngx_http_waf_module_parser.tab.h -L C -o src/ngx_http_waf_module_parser.tab.c bison/parser.yacc \\ +&& cd $(pwd) + +END + exit 1 +fi # Check if the uthash library is installed. diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 64ab0e2e..6855fb32 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -30,6 +30,8 @@ RUN set -xe \ libsodium \ libmaxminddb \ libmaxminddb-dev \ + flex \ + bison \ && if [ ${CHANGE_SOURCE} = true ]; then \ pip3 config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple ; \ fi \ @@ -37,6 +39,9 @@ RUN set -xe \ RUN set -xe \ && git clone https://github.com/leev/ngx_http_geoip2_module.git \ && (cd ngx_waf && git clone https://github.com/libinjection/libinjection.git inc/libinjection) \ + && (cd ngx_waf && make parser) \ + && git clone https://github.com/troydhanson/uthash.git \ + && export LIB_UTHASH=/usr/local/src/uthash \ && (git clone https://github.com/google/ngx_brotli.git && cd ngx_brotli && git submodule update --init) \ # && lastversion --download="openssl.tar.gz" --at=github openssl \ # && mkdir openssl && tar -zxf "openssl.tar.gz" -C openssl --strip-components=1 \ @@ -44,7 +49,7 @@ RUN set -xe \ && mkdir nginx && tar -zxf "nginx.tar.gz" -C nginx --strip-components=1 \ && cd nginx \ && echo "#!/bin/sh" > /env.sh \ - && export EXTRA_CFLAGS="$(python3 /usr/local/src/ngx_waf/docker/arguments.py OUTPUT_EXTRA_CFLAGS)" \ + && export EXTRA_CFLAGS="$(python3 /usr/local/src/ngx_waf/docker/arguments.py OUTPUT_EXTRA_CFLAGS) -Wno-unused-but-set-variable -Wno-unused-function -fstack-protector-strong" \ && echo "export EXTRA_CFLAGS='${EXTRA_CFLAGS}'" >> /env.sh \ && export EXTRA_LFLAGS="$(python3 /usr/local/src/ngx_waf/docker/arguments.py OUTPUT_EXTRA_LFLAGS)" \ && echo "export EXTRA_LFLAGS='${EXTRA_LFLAGS}'" >> /env.sh \ diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 4ca38f89..292cde7f 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -25,13 +25,14 @@ RUN set -xe \ libgeoip-dev \ libgd-dev \ libperl-dev \ - uthash-dev \ python3 \ python3-pip \ libsodium23 \ libsodium-dev \ libmaxminddb0 \ libmaxminddb-dev \ + flex \ + bison \ && if [ ${CHANGE_SOURCE} == true ] ; then \ pip3 config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple ; \ fi \ @@ -39,6 +40,9 @@ RUN set -xe \ RUN set -xe \ && git clone https://github.com/leev/ngx_http_geoip2_module.git \ && (cd ngx_waf && git clone https://github.com/libinjection/libinjection.git inc/libinjection) \ + && (cd ngx_waf && make parser) \ + && git clone https://github.com/troydhanson/uthash.git \ + && export LIB_UTHASH=/usr/local/src/uthash \ && (git clone https://github.com/google/ngx_brotli.git && cd ngx_brotli && git submodule update --init) \ # && lastversion --download="openssl.tar.gz" --at=github openssl \ # && mkdir openssl && tar -zxf "openssl.tar.gz" -C openssl --strip-components=1 \ @@ -46,7 +50,7 @@ RUN set -xe \ && mkdir nginx && tar -zxf "nginx.tar.gz" -C nginx --strip-components=1 \ && cd nginx \ && echo "#!/bin/bash" > /env.sh \ - && export EXTRA_CFLAGS="$(python3 /usr/local/src/ngx_waf/docker/arguments.py OUTPUT_EXTRA_CFLAGS)" \ + && export EXTRA_CFLAGS="$(python3 /usr/local/src/ngx_waf/docker/arguments.py OUTPUT_EXTRA_CFLAGS) -Wno-unused-but-set-variable -Wno-unused-function -fstack-protector-strong" \ && echo "export EXTRA_CFLAGS='${EXTRA_CFLAGS}'" >> /env.sh \ && export EXTRA_LFLAGS="$(python3 /usr/local/src/ngx_waf/docker/arguments.py OUTPUT_EXTRA_LFLAGS)" \ && echo "export EXTRA_LFLAGS='${EXTRA_LFLAGS}'" >> /env.sh \ diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 3f748acd..0fd6c32c 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,6 +1,9 @@ module.exports = { base: process.env.docsBaseUrl == undefined ? "/" : process.env.docsBaseUrl, plugins: ['fulltext-search'], + head: [ + ['link', { rel: 'icon', href: 'https://cdn.jsdelivr.net/gh/ADD-SP/ngx_waf@master/assets/logo.png' }] + ], locales: { '/': { lang: "en", @@ -49,6 +52,7 @@ module.exports = { "/advance/priority.md", "/advance/variable.md", "/advance/log.md", + "/advance/upgrade.md", "/advance/issue.md", "/advance/changes.md" ] @@ -57,7 +61,6 @@ module.exports = { title: "TODO (Advice Needed)", path: "/todo/overview.html", children: [ - "/todo/advanced-rule.md", ] } ] @@ -90,6 +93,7 @@ module.exports = { "/zh-cn/advance/priority.md", "/zh-cn/advance/variable.md", "/zh-cn/advance/log.md", + "/zh-cn/advance/upgrade.md", "/zh-cn/advance/issue.md", "/zh-cn/advance/changes.md" ] @@ -98,7 +102,6 @@ module.exports = { title: "开发计划(建议征集)", path: "/zh-cn/todo/overview.html", children: [ - "/zh-cn/todo/advanced-rule.md", ] } ] diff --git a/docs/advance/changes.md b/docs/advance/changes.md index 504a8951..20a0bfba 100644 --- a/docs/advance/changes.md +++ b/docs/advance/changes.md @@ -7,12 +7,28 @@ lang: en ## [Unreleased] +### NOTE + +* **This release contains some breaking changes.** + +* Advanced rules have a high performance cost because the principle is to compile the rules into a series of instructions that are then executed by the VM. + +### Upgrade from 5.x.x to 6.x.x + +1. Create a new empty file named `advanced` in the rules directory. + +2. If the directive `waf_priority` is used, you can delete it or modify it according to the directive in the documentation. + ### Added +* Advanced rules are supported, see the documentation for details. + ### Removed ### Changed +* Updated the directive `waf_priority`, see the documentation for details. + ### Fixed *** diff --git a/docs/advance/directive.md b/docs/advance/directive.md index c6d8725a..01979eea 100644 --- a/docs/advance/directive.md +++ b/docs/advance/directive.md @@ -180,6 +180,15 @@ Set the priority of each inspection process, except for POST detection, which al ::: +::: tip CHANGES IN THE DEVELOPMENT VERSION + +The default value is changed to W-IP IP CC UNDER-ATTACK W-URL URL ARGS UA W-REFERER REFERER COOKIE ADV" + +`ADV` is advanced rules. + +::: + + ## `waf_http_status` * syntax: waf_http_status \[general=*http_status_code*\] \[cc_deny=*http_status_code*] diff --git a/docs/advance/priority.md b/docs/advance/priority.md index 548cedd1..9c89bf3c 100644 --- a/docs/advance/priority.md +++ b/docs/advance/priority.md @@ -20,7 +20,8 @@ The following is a list of all the tests in order of priority, from top to botto 9. Referer whitelist inspection 10. Referer blacklist inspection 11. Cookie blacklist inspection -12. Post request body blacklist +12. Advanced rules (development version only) +13. Post request body blacklist ::: tip Change priority diff --git a/docs/advance/rule.md b/docs/advance/rule.md index 98ab80b1..e3f2ecd4 100644 --- a/docs/advance/rule.md +++ b/docs/advance/rule.md @@ -18,6 +18,7 @@ all of which must be in the same directory and to which nginx has read access. * Cookie blacklist, with the filename `cookie`. * Referer whitelist with the filename `white-referer`. * Referer blacklist with the filename `referer`. +* Advanced rules with the file name `advaced`. ::: tip NOTE @@ -27,7 +28,9 @@ Regular expressions follow the [PCRE standard](http://www.pcre.org/current/doc/h ::: -## IP Whitelist +## Basic Rules + +### IP Whitelist The ip whitelist consists of the following two files. @@ -62,7 +65,7 @@ Specifies an ipv6 address block. FE80::/10 ``` -## IP Blacklist +### IP Blacklist The IP blacklist consists of the following two files. @@ -71,22 +74,22 @@ The IP blacklist consists of the following two files. Write the same as [IP Whitelist](#ip-whitelist). -## Url Whitelist +### Url Whitelist The Url whitelist file name is `white-url`, and the rule is written with one regular expression per line. Url matched by any of the regular expressions will be released directly without subsequent checks. -## Url Blacklist +### Url Blacklist The Url blacklist file name is `url`, and the rule is written with one regular expression per line. Url will be blocked if it is matched by any of the regular expressions, and a 403 status code will be returned. -## Get Parameter Blacklist +### Get Parameter Blacklist The Get parameter is blacklisted with the file name `args`, and the rule is written with one regular expression per line. If the Get parameter is matched by any of the regular expressions, it will be intercepted and a 403 status code will be returned. -## Post body Blacklist +### Post body Blacklist The file name of the Post body blacklist is `post`, and the rule is written with one regular expression per line. The content of the Post body will be blocked if any of the regular expressions match it, and a 403 status code will be returned. @@ -97,22 +100,206 @@ Sometimes this module does not perform Post body inspection, see [FAQ](/guide/fa ::: -## User-Agent Blacklist +### User-Agent Blacklist The file name of the UserAgent blacklist is `user-agent`, and the rule is written with one regular expression per line. UserAgent will be blocked if any of the regular expressions match it, and a 403 status code will be returned. -## Cookie Blacklist +### Cookie Blacklist The file name of the cookie blacklist is `cookie` and the rule is written with one regular expression per line. If a cookie is matched by any of the regular expressions, it will be blocked and a 403 status code will be returned. -## Referer Whitelist +### Referer Whitelist The referer whitelist is named `white-referer`, and the rule is written with one regular expression per line. The referer will be released if it is matched by any of the regular expressions, and no subsequent checks will be performed. -## Referer Blacklist +### Referer Blacklist The Referer blacklist file is named `referer` and the rule is written with one regular expression per line. -Referer will be blocked if it is matched by any of the regular expressions, and a 403 status code will be returned. \ No newline at end of file +Referer will be blocked if it is matched by any of the regular expressions, and a 403 status code will be returned. + + +## Advanced Rules + +### Overview + +An advanced rule is a rule that combines conditions and actions; it is more flexible, but slow to execute. + +::: tip NOTE + +Advanced rules are slow to execute because the principle is to compile the rules into a series of instructions that are then executed by the VM. + +::: + +### Example + +The following example shows that if `url` contains `/install` then an HTTP 403 status code is returned. + +``` +id: example +if: url contains '/install' +do: return(403) +``` + +*** + +The following example returns an HTTP 403 status code if `user-agent` does not contain `secret`. + +``` +id: example +if: user-agent not equals 'secret' +do: return(403) +``` + +*** + +The following example shows that if `url` matches the regular expression `^/admin` or `user-agent` equals `secret` then immediately stop all detections and let this request go. + + +``` +id: example +if: url matches '^/admin' or user-agent equals 'secret' +do: allow +``` + +*** + +The following example indicates that if the content of the parameter `user_id` in the query string is detected as an SQL injection, an HTTP 403 status code is returned. + +``` +id: example +if: sqli_detn(query_string[user_id]) +do: return(403) +``` + +*** + +The following example indicates that if the client sends a request header with a value of `X-Passwod` that is not equal to `password` then an HTTP 403 status code is returned. + +``` +id: example +if: header_in[X-Passwod] not equals 'password' +do: return(403) +``` + +### Syntax + +``` +id: example +if: condition +do: action + +id: example +if: condition +do: action +``` + +Multiple rules must be separated by one line, and only one line. +The last rule must not have any characters at the end of it. + +* id: Each rule has a unique ID, which is recorded in the log when the rule takes effect. Each rule can only have one ID, different rules can have the same ID. +* if: Execute `action` if `condition` is true. +* do: Execute `action` when `condition` is true. + + +::: tip NOTE + +All keywords are case-insensitive. + +::: + +### Condition + +`condition` is a set of conditional expressions consisting of an operator and an operator number. + +* string operators + * equals + * Format: `left equals right`. + * Function: True if the left and right strings are equal, false if the opposite is true. + * contains + * Format: `left contains right`. + * Function: True if `right` is a substring of `left`, false if not. + * matches + * Format: `str matches regexp`. + * Function: True if `str` can be matched by the regular expression `regexp`, false if not. + * Note: False if `regexp` is not a legal regular expression. + * sqli_detn + * Format: `sqli_detn(str)`. + * Function: True if SQL injection is present in `str`, false if not. + * xss_detn + * Format: `xss_detn(str)`. + * Function: True if there is an XSS attack in `str`, false if not. + +::: tip NOTE + +* `detn` is short for `detection`. +* `sqli` stands for `SQL injection`. + +::: + +* IP operators. + * equals + * Format: `client_ip equals str`. + * Function: True if the IP represented by `str` is the same as `client_ip`, false if not. + * Note + * `str` is a dotted decimal or colon hexadecimal representation of the IP string, and is false if it is incorrectly formatted. + * False when the IP types of the left and right operators do not match. + * `client_ip` is a keyword that indicates the IP address of the client. + * belong_to + * Format: `client_ip belong_to str`. + * Function: True if the IP address block represented by `str` contains `client_ip`, false if the opposite is true. + * Note + * `str` is a dotted decimal or colon hexadecimal representation of the IP string, or false if it is incorrectly formatted. + * False when the IP types of the left and right operators do not match. + * `client_ip` is a keyword indicating the IP address of the client. + +* Logical operators + * and + * Format: `condition and condition`. + * Function: Logical with. + * or + * Format: `condition or condition`. + * Function: Logical or. + * not + * Format + * `not operator`. + * `not (condition)`. + * Function: logical non. + * Example + * `not equals`. + * `not belong_to`. + +* Other operators + * () + * Format: `(condition)` + * Function: Parenthesis operator, used to change the priority, functions like parentheses in math. + +### Action + +`Action` is the action executed after the `if` condition is met. + +* return + * Format: `return(http_status)`. + * Function: Stops all tests immediately and returns the specified HTTP status code. + * Example: `return(403)`. +* allow + * Format: `allow`. + * Function: Immediately stop all detections and let this request go. + + +### Other keywords + +#### String type + +* url: If the user requests `http(s)://localhost/index.html?smth=smth`, then the value is `index.html`. +* query_string\[*key*\]: If the user requests `http(s)://localhost/index.html?key=one&ex=two`, then the value is `one`. +* user-agent: You know, the `user-agent`. +* referer: You know, that is `referer`. +* cookie\[*key*\]: If the cookie is `key=one&ex=two` then the value is `one`. +* header_in\[*key*\]: indicates the value of the corresponding field in the request header. + +#### IP type + +* client_ip: Indicates the IP address of the client. \ No newline at end of file diff --git a/docs/advance/upgrade.md b/docs/advance/upgrade.md new file mode 100644 index 00000000..acad737e --- /dev/null +++ b/docs/advance/upgrade.md @@ -0,0 +1,15 @@ +--- +title: Upgrade +lang: en +--- + +# Upgrade + +The upgrade module needs to be reinstalled according to the instructions in [Installation Guide](/guide/installation.md). + +## Upgrade from 5.x.x to 6.x.x + +1. Create a new empty file named `advanced` in the rules directory. + +2. If the directive `waf_priority` is used, you can delete it or modify it according to the directive in the documentation. + diff --git a/docs/guide/compatibility.md b/docs/guide/compatibility.md index 6f569a83..8dd4b133 100644 --- a/docs/guide/compatibility.md +++ b/docs/guide/compatibility.md @@ -6,13 +6,13 @@ lang: en # Compatibility Statement -## Platform Compatibility +## OS Compatibility -This module does not provide compatibility support for Windows platforms. +Compatibility with operating systems other than Linux is not guaranteed. ## Nginx Compatibility -This module currently supports `nginx-1.18.0` or newer. +This module only guarantees compatibility with `nginx-1.18.0` or newer versions. ## Module Compatibility diff --git a/docs/guide/overview.md b/docs/guide/overview.md index 5ca7b22e..d893de84 100644 --- a/docs/guide/overview.md +++ b/docs/guide/overview.md @@ -12,6 +12,7 @@ Handy, High performance Nginx firewall module. * Full-featured: The basic functions of the web application firewall are available. * Easy to install. The solution is automatically provided when a dependency is missing. * Easy to use: directives are easy to understand and you can probably guess what they mean without reading the documentation. +* Flexible rules: Provide advanced rules that combine actions (such as block or allow) with multiple conditional expressions * High performance: In more extreme tests, QPS (Queries Per Second) is reduced by about 4% after starting this module. See the documentation for details of the tests. ## Function @@ -30,6 +31,7 @@ Handy, High performance Nginx firewall module. * Block the specified Cookie. * Exceptional allow on specific Referer. * Block the specified Referer. +* Advanced rules that combine actions (such as block or allow) with multiple conditional expressions. ## Contact diff --git a/docs/todo/advanced-rule.md b/docs/todo/advanced-rule.md deleted file mode 100644 index ffb4ae44..00000000 --- a/docs/todo/advanced-rule.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: Advanced Rules -lang: en ---- - -# Advanced Rules - -## Overview - -An advanced rule is a rule that contains both a condition and an action, and the corresponding action will be executed only when the specified condition is met. Advanced rules improve flexibility at the cost of performance. - -## Status - -The relevant syntax is being designed, and we are looking forward to your suggestions. - -## Example - -The following example returns an HTTP status code 403 if the `url` contains `/install`. - -``` -id: 'example' -if: url contains '/install' -do: return -status: 403 -``` - -*** - -The following example indicates that if `user-agent` does not contain `secret` then the HTTP status code 403 is returned. - -``` -id: 'example' -if: user-agent not equals 'secret' -do: return -status: 403 -``` - -*** - -The following example shows that if `url` matches the regular expression `^/admin`, or `user-agent` is equal to `secret`, then all subsequent inspections will be stopped and let the request go. - -``` -id: 'example' -if: url matches '^/admin' || user-agent equals 'secret' -do: allow -``` - -## Syntax - -### General format - -``` -id: 'value' -if: condition -do: action -action_paramter: value - -id: 'value' -if: condition -do: action -action_paramter: value -``` - -Multiple rules are separated by at least one blank line. - -* id: identifier of the rule, which will be written to the log when triggered. Each rule can only have one ID, and one ID can be owned by multiple rules. - -### Condition - -Here is the general format of condition. - -```bison -condition -> field comparison_operator 'value' -condition -> field logical_operator comparison_operator 'value' -condition -> condition && condition -condition -> condition || condition -condition -> (condition) -``` - -* field: currently contains only the following values. - * url: The request path, without the query string. - * user-agent: HTTP.Header.User-Agent. -* comparison_operator: currently contains only the following values. - * equals: equals. - * contains: contains. - * matches: Can be matched by regular expressions. -* logical_operator: currently contains only the following values. - * not: logical not. -* &&: logical and. -* ||: logical or. - -::: tip NOTE - -String operations are case-sensitive unless otherwise specified. - -::: - - -### Action - -The following is the general format of an action. - -```bison -action -> name -``` - -* name: currently contains only the following values. - * return: Returns the specified http status code. - * allow: stop all subsequent inspections and let the request go. - - -### Action Parameters - -When the action is `return`, you need to specify the following parameters. - -* status: An integer indicating the http status code to be returned. \ No newline at end of file diff --git a/docs/todo/overview.md b/docs/todo/overview.md index 5a32941a..7231837a 100644 --- a/docs/todo/overview.md +++ b/docs/todo/overview.md @@ -10,5 +10,3 @@ This page will list the development plan of the project, including but not limit **Look forward to your feedbacks and suggestions soon!** ## List - -* [Advanced rule](advanced-rule.md) \ No newline at end of file diff --git a/docs/zh-cn/advance/changes.md b/docs/zh-cn/advance/changes.md index d94e07a6..b550ec25 100644 --- a/docs/zh-cn/advance/changes.md +++ b/docs/zh-cn/advance/changes.md @@ -7,13 +7,29 @@ lang: zh-CN ## [未发布] +### 注意 + +* **本次更新有一些不向下兼容的改动。** + +* 高级规则的执行速度较慢,因为其原理是将规则编译成一系列指令,然后由虚拟机执行。 + +### 从 5.x.x 升级到 6.x.x + +1. 在规则目录下新建一个名为 `advanced` 的空文件。 +2. 如果使用了配置项 `waf_priority`,可以将其删除或者按照文档中对该配置项的说明进行修改。 + ### 新增 +* 支持了高级规则,详情见文档。 + ### 移除 ### 变动 -### 修复 +* 更新了配置项 `waf_priority`,详情见文档。 + +## 修复 + *** diff --git a/docs/zh-cn/advance/directive.md b/docs/zh-cn/advance/directive.md index 523cd274..84f425d6 100644 --- a/docs/zh-cn/advance/directive.md +++ b/docs/zh-cn/advance/directive.md @@ -177,6 +177,14 @@ waf_mode !UA STD; ::: +::: tip 开发版中的变动 + +默认值被修改为 W-IP IP CC UNDER-ATTACK W-URL URL ARGS UA W-REFERER REFERER COOKIE ADV" + +`ADV` 表示高级规则。 + +::: + ## `waf_http_status` diff --git a/docs/zh-cn/advance/priority.md b/docs/zh-cn/advance/priority.md index dda2c57f..548aad9e 100644 --- a/docs/zh-cn/advance/priority.md +++ b/docs/zh-cn/advance/priority.md @@ -20,7 +20,8 @@ lang: zh-CN 9. Referer 白名单检测 10. Referer 黑名单检测 11. Cookie 黑名单检测 -12. Post 请求体黑名单 +12. 高级规则(仅开发版) +13. Post 请求体黑名单 ::: tip 修改优先级 diff --git a/docs/zh-cn/advance/rule.md b/docs/zh-cn/advance/rule.md index 47bd7121..99e9b96c 100644 --- a/docs/zh-cn/advance/rule.md +++ b/docs/zh-cn/advance/rule.md @@ -17,6 +17,7 @@ lang: zh-CN * Cookie 黑名单,文件名为 `cookie`。 * Referer 白名单,文件名为 `white-referer`。 * Referer 黑名单,文件名为 `referer`。 +* 高级规则,文件名为 `advaced`。 ::: tip 注意 @@ -25,7 +26,16 @@ lang: zh-CN ::: -## ip白名单 + +::: tip 注意 + +高级规则仅在开发版中可用。 + +::: + +## 基础规则 + +### ip白名单 ip 白名单包括下面两个文件。 @@ -59,7 +69,7 @@ FE80::1000 FE80::/10 ``` -## IP黑名单 +### IP黑名单 IP 黑名单包括下面两个文件。 @@ -68,22 +78,22 @@ IP 黑名单包括下面两个文件。 写法同 [ip 白名单](#ip白名单)。 -## url白名单 +### url白名单 Url 白名单的文件名为 `white-url`,书写规则时每行指定一个正则表达式, Url 被任何一个正则表达式匹配到就会直接放行,不进行后续的检查。 -## url黑名单 +### url黑名单 Url 黑名单的文件名为 `url`,书写规则时每行指定一个正则表达式, Url 被任何一个正则表达式匹配到就会被拦截,返回 403 状态码。 -## get参数黑名单 +### get参数黑名单 Get 参数黑名单的文件名为 `args`,书写规则时每行指定一个正则表达式, Get 参数被任何一个正则表达式匹配到就会被拦截,返回 403 状态码。 -## post请求体黑名单 +### post请求体黑名单 Post 请求体黑名单的文件名为 `post`,书写规则时每行指定一个正则表达式, Post 请求体内的内容被任何一个正则表达式匹配到就会被拦截,返回 403 状态码。 @@ -94,22 +104,206 @@ Post 请求体内的内容被任何一个正则表达式匹配到就会被拦截 ::: -## user-agent黑名单 +### user-agent黑名单 UserAgent 黑名单的文件名为 `user-agent`,书写规则时每行指定一个正则表达式, UserAgent 被任何一个正则表达式匹配到就会被拦截,返回 403 状态码。 -## cookie黑名单 +### cookie黑名单 Cookie 黑名单的文件名为 `cookie`,书写规则时每行指定一个正则表达式, Cookie 被任何一个正则表达式匹配到就会被拦截,返回 403 状态码。 -## referer白名单 +### referer白名单 Referer 白名单的文件名为 `white-referer`,书写规则时每行指定一个正则表达式, Referer 被任何一个正则表达式匹配到就会直接放行,不进行后续的检查。 -## referer黑名单 +### referer黑名单 Referer 黑名单的文件名为 `referer`,书写规则时每行指定一个正则表达式, -Referer 被任何一个正则表达式匹配到就会被拦截,返回 403 状态码。 \ No newline at end of file +Referer 被任何一个正则表达式匹配到就会被拦截,返回 403 状态码。 + +## 高级规则 + +### 概述 + +高级规则是一种将条件表达式和动作组合起来的规则,只有满足指定的条件时才会执行对应的动作。高级规则更加灵活,但是也更加消耗性能。 + + +::: tip 注意 + +高级规则的性能较慢,因为其原理是将规则编译成一系列指令,然后由虚拟机执行。 + +::: + +### 示例 + +下面的例子表示如果 `url` 中包含 `/install` 则返回 HTTP 403 状态码。 + +``` +id: example +if: url contains '/install' +do: return(403) +``` + +*** + +下面的例子表示如果 `user-agent` 中不包含 `secret` 则返回 HTTP 403 状态码。 + +``` +id: example +if: user-agent not equals 'secret' +do: return(403) +``` + +*** + +下面的例子表示如果 `url` 能够正则表达式 `^/admin` 所匹配,或者 `user-agent` 等于 `secret` 则停止后续的所有检测并放行本次请求。 + +``` +id: example +if: url matches '^/admin' or user-agent equals 'secret' +do: allow +``` + +*** + +下面的例子表示如果查询字符串中的参数 `user_id` 的内容被检测出 SQL 注入则返回 HTTP 403 状态码。 + +``` +id: example +if: sqli_detn(query_string[user_id]) +do: return(403) +``` + +*** + +下面的例子表示如果客户端发送的请求头中 `X-Passwod` 的值不等于 `password` 则返回 HTTP 403 状态码。 + +``` +id: example +if: header_in[X-Passwod] not equals 'password' +do: return(403) +``` + +### 语法 + +``` +id: example +if: condition +do: action + +id: example +if: condition +do: action +``` + +多条规则之间必须间隔一行,并且只能间隔一行。 +最后一条规则的末尾不能有任何字符。 + +* id:每条规则都有一个唯一的 ID,这个 ID 会在规则生效时记录在日志中。每条规则只能有一个 ID,不同规则可以有相同的 ID。 +* if:如果 `condition` 为真则执行 `action`。 +* do:当 `condition` 为真则时执行 `action`。 + + +::: tip 注意 + +所有的关键字均大小写不敏感。 + +::: + +### Condition + +`condition`是一系列条件表达式的组合,条件表达式由运算符符和运算数组成。 + +* 字符串运算符 + * equals + * 格式:`left equals right`。 + * 功能:如果左右两个字符串相等则为真,反之为假。 + * contains + * 格式:`left contains right`。 + * 功能:如果 `right` 是 `left` 的一个子串则为真,反之为假。 + * matches + * 格式:`str matches regexp`。 + * 功能:如果 `str` 能被正则表达式 `regexp` 所匹配则为真,反之为假。 + * 注意:如果 `regexp` 不是合法的正则表达式则为假。 + * sqli_detn + * 格式:`sqli_detn(str)`。 + * 功能:如果 `str` 中是否存在 SQL 注入则为真,反之为假。 + * xss_detn + * 格式:`xss_detn(str)`。 + * 功能:如果 `str` 中存在 XSS 攻击则为真,反之为假。 + +::: tip 注意 + +* `detn` 是 `detection` 的缩写。 +* `sqli` 是 `SQL injection` 的缩写。 + +::: + +* IP 运算符: + * equals + * 格式:`client_ip equals str`。 + * 功能:如果 `str` 所表示的 IP 与 `client_ip` 相同则为真,反之为假。 + * 注意 + * `str` 是一个点分十进制或冒号十六进制表示的 IP 字符串,如果格式错误则为假。 + * 当左右两个运算数的 IP 类型不一致时为假。 + * `client_ip` 是关键字,表示客户端的 IP 地址。 + * belong_to + * 格式:`client_ip belong_to str`。 + * 功能:如果 `str` 所表示的 IP 地址块包含 `client_ip` 则为真,反之为假。 + * 注意 + * `str` 是一个点分十进制或冒号十六进制表示的 IP 字符串,如果格式错误则为假。 + * 当左右两个运算数的 IP 类型不一致时为假。 + * `client_ip` 是关键字,表示客户端的 IP 地址。 + +* 逻辑运算符 + * and + * 格式:`condition and condition`。 + * 功能:逻辑与。 + * or + * 格式:`condition or condition`。 + * 功能:逻辑或。 + * not + * 格式 + * `not operator`。 + * `not (condition)`。 + * 功能:逻辑非。 + * 示例 + * `not equals`。 + * `not belong_to`。 + +* 其它运算符 + * () + * 格式:`(condition)` + * 功能:括号运算符,用来改变优先级,功能类似数学中的括号。 + +### Action + +`Action` 是在 `if` 条件满足后执行的动作。 + +* return + * 格式:`return(http_status)`。 + * 功能:立即停止所有的检测并返回指定的 HTTP 状态码。 + * 示例:`return(403)`。 +* allow + * 格式:`allow` + * 功能:立即停止所有的检测并放行本次请求。 + + +### 其它关键字 + +#### 字符串类型 + +* url:如果用户请求 `http(s)://localhost/index.html?smth=smth`,则值为`index.html`。 +* query_string\[*key*\]:如果用户请求 `http(s)://localhost/index.html?key=one&ex=two` ,则值为 `one`。 +* user-agent: 你知道的,就是 `user-agent`。 +* referer:你知道的,就是 `referer`。 +* cookie\[*key*\]:如果 Cookie 为 `key=one&ex=two` 则值为 `one`。 +* header_in\[*key*\]:表示请求头中对应字段的值。 + +#### IP 类型 + +* client_ip:表示客户端的 IP 地址。 + diff --git a/docs/zh-cn/advance/upgrade.md b/docs/zh-cn/advance/upgrade.md new file mode 100644 index 00000000..4cb88fc0 --- /dev/null +++ b/docs/zh-cn/advance/upgrade.md @@ -0,0 +1,13 @@ +--- +title: 升级 +lang: zh-CN +--- + +# 升级 + +升级模块均需要按照[安装指南](/zh-cn/guide/installation.md)中的说明重新安装。 + +## 从 5.x.x 升级到 6.x.x + +1. 在规则目录下新建一个名为 `advanced` 的空文件。 +2. 如果使用了配置项 `waf_priority`,可以将其删除或者按照文档中对该配置项的说明进行修改。 \ No newline at end of file diff --git a/docs/zh-cn/guide/compatibility.md b/docs/zh-cn/guide/compatibility.md index 31404af8..97a53312 100644 --- a/docs/zh-cn/guide/compatibility.md +++ b/docs/zh-cn/guide/compatibility.md @@ -6,13 +6,13 @@ lang: zh-CN # 兼容性说明 -## 平台兼容性 +## 操作系统兼容性 -本模块不向 Windows 平台提供兼容性支持。 +不保证与Linux以外的操作系统的兼容性。 ## nginx 兼容性 -本模块目前支持 `nginx-1.18.0` 或更新的版本。 +本模块只保证对 `nginx-1.18.0` 或更新的版本的兼容性。 ## 模块兼容性 diff --git a/docs/zh-cn/guide/overview.md b/docs/zh-cn/guide/overview.md index 99588ce7..16210d8e 100644 --- a/docs/zh-cn/guide/overview.md +++ b/docs/zh-cn/guide/overview.md @@ -12,6 +12,7 @@ lang: zh-CN * 功能齐全:「网络应用防火墙」的基本功能都有。 * 安装方便:缺少依赖项时会自动提供解决方法。 * 使用方便:配置指令简单易懂,不用看文档都能猜到大概是什么意思。 +* 规则灵活:提供高级规则,将动作(如拦截或放行)和多个条件表达式组合起来。 * 高性能:经过较为极限的测试,启动本模块后 RPS(每秒请求数) 降低约 4%。测试说明和结果见使用文档。 ## 功能 @@ -27,6 +28,7 @@ lang: zh-CN * UserAgent 黑名单。 * Cookie 黑名单。 * Referer 黑白名单。 +* 高级规则,将动作(如拦截或放行)和多个条件表达式组合起来。 ## 联系方式 diff --git a/docs/zh-cn/todo/advanced-rule.md b/docs/zh-cn/todo/advanced-rule.md deleted file mode 100644 index 5c448498..00000000 --- a/docs/zh-cn/todo/advanced-rule.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: 高级规则 -lang: zh-CN ---- - -# 高级规则 - -## 概述 - -高级规则是一种将条件表达式和动作组合起来的规则,只有满足指定的条件时才会执行对应的动作。高级规则更加灵活,但是也更加消耗性能。 - -## 状态 - -此功能处在纸面设计阶段,期待您的建议。 - -## 示例 - -下面的例子表示如果 `url` 中包含 `/install` 则返回 HTTP 403 状态码。 - -``` -id: 'example' -if: url contains '/install' -do: return -status: 403 -``` - -*** - -下面的例子表示如果 `user-agent` 中不包含 `secret` 则返回 HTTP 403 状态码。 - -``` -id: 'example' -if: user-agent not equals 'secret' -do: return -status: 403 -``` - -*** - -下面的例子表示如果 `url` 能够正则表达式 `^/admin` 所匹配,或者 `user-agent` 等于 `secret` 则停止后续的所有检测并直接放行本次请求。 - -``` -id: 'example' -if: url matches '^/admin' || user-agent equals 'secret' -do: allow -``` - -## 语法 - -### 一般格式 - -``` -id: 'value' -if: condition -do: action -action_paramter: value - -id: 'value' -if: condition -do: action -action_paramter: value -``` - -多条规则之间至少用一个空行分隔。 - -* id:规则的标识符,触发时会被写入日志。每条规则只能有一个 ID,一个 ID 可以被多个规则所拥有。 - -### Condition - -下面是 condition 的一般格式。 - -```bison -condition -> field comparison_operator 'value' -condition -> field logical_operator comparison_operator 'value' -condition -> condition && condition -condition -> condition || condition -condition -> (condition) -``` - -* field:目前仅包含下列取值。 - * url:请求路径,不含查询字符串。 - * user-agent: HTTP.Header.User-Agent。 -* comparison_operator:目前仅包含下列取值。 - * equals:等于。 - * contains:包含。 - * matches:能够被正则表达式匹配。 -* logical_operator:目前仅包含下列取值。 - * not:逻辑非。 -* &&:逻辑与。 -* ||:逻辑或。 - -::: tip 注意 - -涉及字符串操作除非特别说明,否则均大小写敏感。 - -::: - - -### Action - -下面是 action 的一般格式。 - -```bison -action -> name -``` - -* name:目前仅包含下列取值。 - * return:返回指定的 http 状态码。 - * allow:停止后续的一切检测并放行本次请求。 - - -### Action Parameters - -当 action 为 `return` 时,您需要指定下列参数。 - -* status:一个整数,表示要返回的 http 状态码。 diff --git a/docs/zh-cn/todo/overview.md b/docs/zh-cn/todo/overview.md index f3d4ae0c..5aa3a164 100644 --- a/docs/zh-cn/todo/overview.md +++ b/docs/zh-cn/todo/overview.md @@ -12,6 +12,4 @@ lang: zh-CN ## 列表 -* [高级规则](advanced-rule.md) - diff --git a/flex/lexer.lex b/flex/lexer.lex new file mode 100644 index 00000000..c896bb21 --- /dev/null +++ b/flex/lexer.lex @@ -0,0 +1,284 @@ +%option noyywrap +%option yylineno +%option outfile="src/ngx_http_waf_module_lexer.c" header-file="inc/ngx_http_waf_module_lexer.h" +%option prefix="ngx_http_waf_" + +%{ + #include + #include + #include + #include + #include + // #define VM_DEBUG + void yyerror (UT_array* array, const char* msg); +%} + + +KEYWORD_ID ^(?i:id) + +KEYWORD_IF ^(?i:if) + +KEYWORD_DO ^(?i:do) + +KEYWORD_URL (?i:url) + +KEYWORD_QUERY_STRING (?i:query_string) + +KEYWORD_USER_AGENT (?i:user_agent) + +KEYWORD_REFERER (?i:referer) + +KEYWORD_CLIENT_IP (?i:client_ip) + +KEYWORD_HEADER_IN (?i:header_in) + +KEYWORD_COOKIE (?i:cookie) + +KEYWORD_CONTAINS [[:blank:]]+(?i:contains)[[:blank:]]+ + +KEYWORD_MATCHES [[:blank:]]+(?i:matches)[[:blank:]]+ + +KEYWORD_EQUALS [[:blank:]]+(?i:equals)[[:blank:]]+ + +KEYWORD_BELONG_TO [[:blank:]]+(?i:belong_to)[[:blank:]]+ + +KEYWORD_SQLI_DETN (?i:sqli_detn) + +KEYWORD_XSS_DETN (?i:xss_detn) + +KEYWORD_RETURN (?i:return) + +KEYWORD_ALLOW (?i:allow) + +KEYWORD_NOT (?i:not) + +KEYWORD_OR [[:blank:]]+(?i:or)[[:blank:]]+ + +KEYWORD_AND [[:blank:]]+(?i:and)[[:blank:]]+ + +INDEX \[((-|_|[[:alnum:]])+)\] + +ID (_|[[:alpha:]])((_|[[:alnum:]]){1,49}) + +STRING (\"[^\"]*\")|('[^']*') + +INTEGER (-)?[[:digit:]]+ + +BREAK_LINE (\r)?\n + + +%% + + +{KEYWORD_ID} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_ID\n"); + #endif + return keyword_id; + } + +{KEYWORD_IF} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_IF\n"); + #endif + return keyword_if; + } + +{KEYWORD_DO} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_DO\n"); + #endif + return keyword_do; + } + + +{KEYWORD_URL} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_URL\n"); + #endif + return keyword_url; + } + +{KEYWORD_QUERY_STRING} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_QUERY_STRING\n"); + #endif + return keyword_query_string; + } + + +{KEYWORD_USER_AGENT} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_USER_AGENT\n"); + #endif + return keyword_user_agent; + } + +{KEYWORD_REFERER} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_REFERER\n"); + #endif + return keyword_referer; + } + + +{KEYWORD_CLIENT_IP} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_CLIENT_IP\n"); + #endif + return keyword_client_ip; + } + + +{KEYWORD_HEADER_IN} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_HEADER_IN\n"); + #endif + return keyword_header_in; + } + +{KEYWORD_COOKIE} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_COOKIE\n"); + #endif + return keyword_cookie; + } + +{KEYWORD_CONTAINS} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_CONTAINS\n"); + #endif + return keyword_contains; + } + +{KEYWORD_MATCHES} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_MATCHES\n"); + #endif + return keyword_matches; + } + +{KEYWORD_EQUALS} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_EQUALS\n"); + #endif + return keyword_equals; + } + +{KEYWORD_BELONG_TO} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_BELONG_TO"); + #endif + return keyword_belong_to; + } + +{KEYWORD_SQLI_DETN} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_SQLI_DETN\n"); + #endif + return keyword_sqli_detn; + } + +{KEYWORD_XSS_DETN} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_XSS_DETN\n"); + #endif + return keyword_xss_detn; + } + +{KEYWORD_RETURN} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_RETURN\n"); + #endif + return keyword_return; + } + +{KEYWORD_ALLOW} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_ALLOW\n"); + #endif + return keyword_allow; + } + +{INDEX} { + strcpy(ngx_http_waf_lval.str_val, yytext + 1); + ngx_http_waf_lval.str_val[yyleng - 2] = '\0'; + #ifdef VM_DEBUG + printf("Lexer - INDEX: %s\n", yytext); + #endif + return token_index; + } + + +{BREAK_LINE} { + #ifdef VM_DEBUG + printf("Lexer - BREAK_LINE\n"); + #endif + return token_break_line; + } + +{KEYWORD_NOT} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_NOT\n"); + #endif + return keyword_not; + } + +{KEYWORD_OR} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_or\n"); + #endif + return keyword_or; + } + +{KEYWORD_AND} { + #ifdef VM_DEBUG + printf("Lexer - KEYWORD_and\n"); + #endif + return keyword_and; + } + +{STRING} { + #ifdef VM_DEBUG + printf("Lexer - STRING: %s\n", yytext); + #endif + strcpy(ngx_http_waf_lval.str_val, yytext + 1); + ngx_http_waf_lval.str_val[yyleng - 2] = '\0'; + return token_str; + } + +{ID} { + #ifdef VM_DEBUG + printf("Lexer - ID: %s\n", yytext); + #endif + strcpy(ngx_http_waf_lval.id_val, yytext); + return token_id; + } + +{INTEGER} { + #ifdef VM_DEBUG + printf("Lexer - INTEGER: %s\n", yytext); + #endif + ngx_http_waf_lval.int_val = atoi(yytext); + return token_int; + } + +[[:blank:]]+ { + #ifdef VM_DEBUG + printf("Lexer - [[:blank:]]+\n"); + #endif + return token_blank; + } + +. { + #ifdef VM_DEBUG + printf("Lexer - Other: %s\n", yytext); + #endif + return *yytext; + } + +%% + +void ngx_http_waf_error(UT_array* array, ngx_pool_t* pool, const char* msg) { + printf("error: %s in line %d\n", msg, yylineno); +} \ No newline at end of file diff --git a/inc/ngx_http_waf_module_check.h b/inc/ngx_http_waf_module_check.h index e4ca1739..ff77815b 100644 --- a/inc/ngx_http_waf_module_check.h +++ b/inc/ngx_http_waf_module_check.h @@ -179,7 +179,7 @@ static ngx_int_t ngx_http_waf_handler_check_white_ip(ngx_http_request_t* r, ngx_ ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_IP) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_IP) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -233,7 +233,7 @@ static ngx_int_t ngx_http_waf_handler_check_black_ip(ngx_http_request_t* r, ngx_ ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_IP) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_IP) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -289,7 +289,7 @@ static ngx_int_t ngx_http_waf_handler_check_cc(ngx_http_request_t* r, ngx_int_t* ngx_int_t ip_type = r->connection->sockaddr->sa_family; time_t now = time(NULL); - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_CC) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_CC) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this detection is disabled in the configuration, no detection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -450,7 +450,7 @@ static ngx_int_t ngx_http_waf_handler_check_white_url(ngx_http_request_t* r, ngx ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_URL | r->method) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_URL | r->method) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this detection is disabled in the configuration, no detection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -495,7 +495,7 @@ static ngx_int_t ngx_http_waf_handler_check_black_url(ngx_http_request_t* r, ngx ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_URL | r->method) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_URL | r->method) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -540,7 +540,7 @@ static ngx_int_t ngx_http_waf_handler_check_black_args(ngx_http_request_t* r, ng ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_ARGS | r->method) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_ARGS | r->method) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -625,7 +625,7 @@ static ngx_int_t ngx_http_waf_handler_check_black_user_agent(ngx_http_request_t* ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_UA | r->method) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_UA | r->method) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -674,7 +674,7 @@ static ngx_int_t ngx_http_waf_handler_check_white_referer(ngx_http_request_t* r, ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_REFERER | r->method) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_REFERER | r->method) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -724,7 +724,7 @@ static ngx_int_t ngx_http_waf_handler_check_black_referer(ngx_http_request_t* r, ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_REFERER | r->method) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_REFERER | r->method) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -774,7 +774,7 @@ static ngx_int_t ngx_http_waf_handler_check_black_cookie(ngx_http_request_t* r, ngx_int_t ret_value = NGX_HTTP_WAF_NOT_MATCHED; - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_COOKIE | r->method) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_COOKIE | r->method) == NGX_HTTP_WAF_FALSE) { ngx_log_debug(NGX_LOG_DEBUG_CORE, r->connection->log, 0, "ngx_waf_debug: Because this Inspection is disabled in the configuration, no Inspection is performed."); ret_value = NGX_HTTP_WAF_NOT_MATCHED; @@ -973,7 +973,7 @@ static ngx_int_t ngx_http_waf_regex_exec_arrray_sqli_xss(ngx_http_request_t* r, return NGX_HTTP_WAF_NOT_MATCHED; } - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_CACHE) == NGX_HTTP_WAF_TRUE + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_CACHE) == NGX_HTTP_WAF_TRUE && srv_conf->waf_inspection_capacity != NGX_CONF_UNSET && cache != NULL) { cache_hit = lru_cache_manager_find(cache, str->data, str->len * sizeof(u_char), &is_matched, &rule_detail); @@ -981,7 +981,7 @@ static ngx_int_t ngx_http_waf_regex_exec_arrray_sqli_xss(ngx_http_request_t* r, if (cache_hit != NGX_HTTP_WAF_SUCCESS) { if (check_sql_injection == NGX_HTTP_WAF_TRUE - && NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_LIB_INJECTION_SQLI) == NGX_HTTP_WAF_TRUE) { + && ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_LIB_INJECTION_SQLI) == NGX_HTTP_WAF_TRUE) { sfilter sf; libinjection_sqli_init(&sf, (char*)(str->data), @@ -999,7 +999,7 @@ static ngx_int_t ngx_http_waf_regex_exec_arrray_sqli_xss(ngx_http_request_t* r, } if (check_xss - && NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_LIB_INJECTION_XSS) == NGX_HTTP_WAF_TRUE) { + && ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_LIB_INJECTION_XSS) == NGX_HTTP_WAF_TRUE) { if (libinjection_xss((char*)(str->data), str->len) == 1) { is_matched = NGX_HTTP_WAF_MATCHED; rule_detail = ngx_pnalloc(r->pool, sizeof(u_char) * 64); @@ -1024,7 +1024,7 @@ static ngx_int_t ngx_http_waf_regex_exec_arrray_sqli_xss(ngx_http_request_t* r, } } - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_CACHE) == NGX_HTTP_WAF_TRUE + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_CACHE) == NGX_HTTP_WAF_TRUE && srv_conf->waf_inspection_capacity != NGX_CONF_UNSET && cache != NULL) { lru_cache_manager_add(cache, str->data, str->len * sizeof(u_char), is_matched, rule_detail); diff --git a/inc/ngx_http_waf_module_config.h b/inc/ngx_http_waf_module_config.h index 106746fc..d04d6adf 100644 --- a/inc/ngx_http_waf_module_config.h +++ b/inc/ngx_http_waf_module_config.h @@ -18,6 +18,9 @@ #include #include #include +#include +#include +#include #ifndef NGX_HTTP_WAF_MODULE_CONFIG_H @@ -25,6 +28,8 @@ extern ngx_module_t ngx_http_waf_module; +extern FILE* ngx_http_waf_in; + static ngx_int_t ngx_http_waf_handler_server_rewrite_phase(ngx_http_request_t* r); @@ -200,18 +205,20 @@ static char* ngx_http_waf_rule_path_conf(ngx_conf_t* cf, ngx_command_t* cmd, voi char* full_path = ngx_palloc(cf->pool, sizeof(char) * NGX_HTTP_WAF_RULE_MAX_LEN); char* end = to_c_str((u_char*)full_path, srv_conf->waf_rule_path); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_IPV4_FILE, &srv_conf->black_ipv4, 1); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_IPV6_FILE, &srv_conf->black_ipv6, 2); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_URL_FILE, srv_conf->black_url, 0); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_ARGS_FILE, srv_conf->black_args, 0); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_UA_FILE, srv_conf->black_ua, 0); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_REFERER_FILE, srv_conf->black_referer, 0); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_COOKIE_FILE, srv_conf->black_cookie, 0); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_POST_FILE, srv_conf->black_post, 0); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_WHITE_IPV4_FILE, &srv_conf->white_ipv4, 1); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_WHITE_IPV6_FILE, &srv_conf->white_ipv6, 2); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_WHITE_URL_FILE, srv_conf->white_url, 0); - NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, full_path, end, NGX_HTTP_WAF_WHITE_REFERER_FILE, srv_conf->white_referer, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_IPV4_FILE, &srv_conf->black_ipv4, 1); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_IPV6_FILE, &srv_conf->black_ipv6, 2); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_URL_FILE, srv_conf->black_url, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_ARGS_FILE, srv_conf->black_args, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_UA_FILE, srv_conf->black_ua, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_REFERER_FILE, srv_conf->black_referer, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_COOKIE_FILE, srv_conf->black_cookie, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_POST_FILE, srv_conf->black_post, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_WHITE_IPV4_FILE, &srv_conf->white_ipv4, 1); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_WHITE_IPV6_FILE, &srv_conf->white_ipv6, 2); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_WHITE_URL_FILE, srv_conf->white_url, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_WHITE_REFERER_FILE, srv_conf->white_referer, 0); + ngx_http_waf_check_and_load_conf(cf, full_path, end, NGX_HTTP_WAF_ADVANCED_FILE, &(srv_conf->advanced_rule), 3); + ngx_pfree(cf->pool, full_path); return NGX_CONF_OK; @@ -826,7 +833,7 @@ static char* ngx_http_waf_priority_conf(ngx_conf_t* cf, ngx_command_t* cmd, void } - if (utarray_len(array) != 11) { + if (utarray_len(array) != 12) { ngx_conf_log_error(NGX_LOG_EMERG, cf, NGX_EINVAL, "ngx_waf: you must specify the priority of all inspections except for POST inspections"); return NGX_CONF_ERROR; @@ -889,6 +896,11 @@ static char* ngx_http_waf_priority_conf(ngx_conf_t* cf, ngx_command_t* cmd, void srv_conf->check_proc_no_cc[proc_no_cc_index++] = ngx_http_waf_check_under_attack; } + else if (strcasecmp("ADV", (char*)(p->data)) == 0) { + srv_conf->check_proc[proc_index++] = ngx_http_waf_vm_exec; + srv_conf->check_proc_no_cc[proc_no_cc_index++] = ngx_http_waf_vm_exec; + } + else { ngx_conf_log_error(NGX_LOG_EMERG, cf, NGX_EINVAL, "ngx_waf: ngx_waf: invalid value [%s]", p->data); @@ -984,6 +996,8 @@ static void* ngx_http_waf_create_srv_conf(ngx_conf_t* cf) { srv_conf->black_post = ngx_array_create(cf->pool, 10, sizeof(ngx_regex_elt_t)); srv_conf->white_url = ngx_array_create(cf->pool, 10, sizeof(ngx_regex_elt_t)); srv_conf->white_referer = ngx_array_create(cf->pool, 10, sizeof(ngx_regex_elt_t)); + UT_icd icd = ngx_http_waf_make_utarray_vm_code_icd(); + utarray_init(&(srv_conf->advanced_rule), &icd); srv_conf->shm_zone_cc_deny = NULL; srv_conf->ipv4_access_statistics = NULL; srv_conf->ipv6_access_statistics = NULL; @@ -1002,6 +1016,7 @@ static void* ngx_http_waf_create_srv_conf(ngx_conf_t* cf) { srv_conf->check_proc[8] = ngx_http_waf_handler_check_white_referer; srv_conf->check_proc[9] = ngx_http_waf_handler_check_black_referer; srv_conf->check_proc[10] = ngx_http_waf_handler_check_black_cookie; + srv_conf->check_proc[11] = ngx_http_waf_vm_exec; ngx_memzero(srv_conf->check_proc_no_cc, sizeof(srv_conf->check_proc_no_cc)); @@ -1015,6 +1030,8 @@ static void* ngx_http_waf_create_srv_conf(ngx_conf_t* cf) { srv_conf->check_proc_no_cc[7] = ngx_http_waf_handler_check_white_referer; srv_conf->check_proc_no_cc[8] = ngx_http_waf_handler_check_black_referer; srv_conf->check_proc_no_cc[9] = ngx_http_waf_handler_check_black_cookie; + srv_conf->check_proc_no_cc[10] = ngx_http_waf_vm_exec; + if (ip_trie_init(&(srv_conf->white_ipv4), std, NULL, AF_INET) != NGX_HTTP_WAF_SUCCESS) { @@ -1338,107 +1355,118 @@ static ngx_int_t load_into_container(ngx_conf_t* cf, const char* file_name, void if (fp == NULL) { return NGX_HTTP_WAF_FAIL; } - while (fgets(str, NGX_HTTP_WAF_RULE_MAX_LEN - 16, fp) != NULL) { - ngx_regex_compile_t regex_compile; - u_char errstr[NGX_MAX_CONF_ERRSTR]; - ngx_regex_elt_t* ngx_regex_elt; - ipv4_t ipv4; - inx_addr_t inx_addr; - ipv6_t ipv6; - ip_trie_node_t* ip_trie_node = NULL; - ++line_number; - line.data = (u_char*)str; - #ifdef __STDC_LIB_EXT1__ - line.len = strnlen_s((char*)str. sizeof(char) * NGX_HTTP_WAF_RULE_MAX_LEN); - #else - line.len = strlen((char*)str); - #endif - - memset(&ipv4, 0, sizeof(ipv4_t)); - memset(&inx_addr, 0, sizeof(inx_addr_t)); - memset(&ipv6, 0, sizeof(ipv6_t)); - - if (line.len <= 0) { - continue; - } - - if (line.data[line.len - 1] == '\n') { - line.data[line.len - 1] = '\0'; - --(line.len); + + if (mode == 3) { + ngx_http_waf_in = fp; + if (ngx_http_waf_parse(container, cf->pool) != 0) { + return NGX_HTTP_WAF_FAIL; + } + print_code(container); + } else { + while (fgets(str, NGX_HTTP_WAF_RULE_MAX_LEN - 16, fp) != NULL) { + ngx_regex_compile_t regex_compile; + u_char errstr[NGX_MAX_CONF_ERRSTR]; + ngx_regex_elt_t* ngx_regex_elt; + ipv4_t ipv4; + inx_addr_t inx_addr; + ipv6_t ipv6; + ip_trie_node_t* ip_trie_node = NULL; + ++line_number; + line.data = (u_char*)str; + #ifdef __STDC_LIB_EXT1__ + line.len = strnlen_s((char*)str. sizeof(char) * NGX_HTTP_WAF_RULE_MAX_LEN); + #else + line.len = strlen((char*)str); + #endif + + memset(&ipv4, 0, sizeof(ipv4_t)); + memset(&inx_addr, 0, sizeof(inx_addr_t)); + memset(&ipv6, 0, sizeof(ipv6_t)); + if (line.len <= 0) { continue; } - if (line.data[line.len - 1] == '\r') { + + if (line.data[line.len - 1] == '\n') { line.data[line.len - 1] = '\0'; --(line.len); + if (line.len <= 0) { + continue; + } + if (line.data[line.len - 1] == '\r') { + line.data[line.len - 1] = '\0'; + --(line.len); + } } - } - - if (line.len <= 0) { - continue; - } - switch (mode) { - case 0: - ngx_memzero(®ex_compile, sizeof(ngx_regex_compile_t)); - regex_compile.pattern = line; - regex_compile.pool = cf->pool; - regex_compile.err.len = NGX_MAX_CONF_ERRSTR; - regex_compile.err.data = errstr; - if (ngx_regex_compile(®ex_compile) != NGX_OK) { - char temp[NGX_HTTP_WAF_RULE_MAX_LEN] = { 0 }; - to_c_str((u_char*)temp, line); - ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, - "ngx_waf: In %s:%d, [%s] is not a valid regex string.", file_name, line_number, temp); - return NGX_HTTP_WAF_FAIL; - } - ngx_regex_elt = ngx_array_push((ngx_array_t*)container); - ngx_regex_elt->name = ngx_palloc(cf->pool, sizeof(u_char) * NGX_HTTP_WAF_RULE_MAX_LEN); - to_c_str(ngx_regex_elt->name, line); - ngx_regex_elt->regex = regex_compile.regex; - break; - case 1: - if (parse_ipv4(line, &ipv4) != NGX_HTTP_WAF_SUCCESS) { - ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, - "ngx_waf: In %s:%d, [%s] is not a valid IPV4 string.", file_name, line_number, ipv4.text); - return NGX_HTTP_WAF_FAIL; + if (line.len <= 0) { + continue; } - inx_addr.ipv4.s_addr = ipv4.prefix; - if (ip_trie_add((ip_trie_t*)container, &inx_addr, ipv4.suffix_num, ipv4.text, 32) != NGX_HTTP_WAF_SUCCESS) { - if (ip_trie_find((ip_trie_t*)container, &inx_addr, &ip_trie_node) == NGX_HTTP_WAF_SUCCESS) { - ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, - "ngx_waf: In %s:%d, the two address blocks [%s] and [%s] have overlapping parts.", - file_name, line_number, ipv4.text, ip_trie_node->data); - } else { + + switch (mode) { + case 0: + ngx_memzero(®ex_compile, sizeof(ngx_regex_compile_t)); + regex_compile.pattern = line; + regex_compile.pool = cf->pool; + regex_compile.err.len = NGX_MAX_CONF_ERRSTR; + regex_compile.err.data = errstr; + if (ngx_regex_compile(®ex_compile) != NGX_OK) { + char temp[NGX_HTTP_WAF_RULE_MAX_LEN] = { 0 }; + to_c_str((u_char*)temp, line); ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, - "ngx_waf: In %s:%d, [%s] cannot be stored because the memory allocation failed.", - file_name, line_number, ipv4.text); - return NGX_HTTP_WAF_FAIL; + "ngx_waf: In %s:%d, [%s] is not a valid regex string.", file_name, line_number, temp); + return NGX_HTTP_WAF_FAIL; } - } - break; - case 2: - if (parse_ipv6(line, &ipv6) != NGX_HTTP_WAF_SUCCESS) { - ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, - "ngx_waf: In %s:%d, [%s] is not a valid IPV6 string.", file_name, line_number, ipv6.text); - return NGX_HTTP_WAF_FAIL; - } - ngx_memcpy(inx_addr.ipv6.s6_addr, ipv6.prefix, 16); - if (ip_trie_add((ip_trie_t*)container, &inx_addr, ipv6.suffix_num, ipv6.text, 64) != NGX_HTTP_WAF_SUCCESS) { - if (ip_trie_find((ip_trie_t*)container, &inx_addr, &ip_trie_node) == NGX_HTTP_WAF_SUCCESS) { + ngx_regex_elt = ngx_array_push((ngx_array_t*)container); + ngx_regex_elt->name = ngx_palloc(cf->pool, sizeof(u_char) * NGX_HTTP_WAF_RULE_MAX_LEN); + to_c_str(ngx_regex_elt->name, line); + ngx_regex_elt->regex = regex_compile.regex; + break; + case 1: + if (parse_ipv4(line, &ipv4) != NGX_HTTP_WAF_SUCCESS) { ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, - "ngx_waf: In %s:%d, the two address blocks [%s] and [%s] have overlapping parts.", - file_name, line_number, ipv6.text, ip_trie_node->data); - } else { + "ngx_waf: In %s:%d, [%s] is not a valid IPV4 string.", file_name, line_number, ipv4.text); + return NGX_HTTP_WAF_FAIL; + } + inx_addr.ipv4.s_addr = ipv4.prefix; + if (ip_trie_add((ip_trie_t*)container, &inx_addr, ipv4.suffix_num, ipv4.text, 32) != NGX_HTTP_WAF_SUCCESS) { + if (ip_trie_find((ip_trie_t*)container, &inx_addr, &ip_trie_node) == NGX_HTTP_WAF_SUCCESS) { + ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, + "ngx_waf: In %s:%d, the two address blocks [%s] and [%s] have overlapping parts.", + file_name, line_number, ipv4.text, ip_trie_node->data); + } else { + ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, + "ngx_waf: In %s:%d, [%s] cannot be stored because the memory allocation failed.", + file_name, line_number, ipv4.text); + return NGX_HTTP_WAF_FAIL; + } + } + break; + case 2: + if (parse_ipv6(line, &ipv6) != NGX_HTTP_WAF_SUCCESS) { ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, - "ngx_waf: In %s:%d, [%s] cannot be stored because the memory allocation failed.", - file_name, line_number, ipv6.text); - return NGX_HTTP_WAF_FAIL; + "ngx_waf: In %s:%d, [%s] is not a valid IPV6 string.", file_name, line_number, ipv6.text); + return NGX_HTTP_WAF_FAIL; } + ngx_memcpy(inx_addr.ipv6.s6_addr, ipv6.prefix, 16); + if (ip_trie_add((ip_trie_t*)container, &inx_addr, ipv6.suffix_num, ipv6.text, 64) != NGX_HTTP_WAF_SUCCESS) { + if (ip_trie_find((ip_trie_t*)container, &inx_addr, &ip_trie_node) == NGX_HTTP_WAF_SUCCESS) { + ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, + "ngx_waf: In %s:%d, the two address blocks [%s] and [%s] have overlapping parts.", + file_name, line_number, ipv6.text, ip_trie_node->data); + } else { + ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, + "ngx_waf: In %s:%d, [%s] cannot be stored because the memory allocation failed.", + file_name, line_number, ipv6.text); + return NGX_HTTP_WAF_FAIL; + } + } + break; } - break; } } + + fclose(fp); ngx_pfree(cf->pool, str); return NGX_HTTP_WAF_SUCCESS; diff --git a/inc/ngx_http_waf_module_ip_trie.h b/inc/ngx_http_waf_module_ip_trie.h index db184f39..4aa06241 100644 --- a/inc/ngx_http_waf_module_ip_trie.h +++ b/inc/ngx_http_waf_module_ip_trie.h @@ -146,7 +146,7 @@ static ngx_int_t ip_trie_add(ip_trie_t* trie, inx_addr_t* inx_addr, uint32_t suf } } prev_node = cur_node; - if (NGX_HTTP_WAF_CHECK_BIT(u8_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { + if (ngx_http_waf_check_bit(u8_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { prev_bit = 0; cur_node = cur_node->left; } else { @@ -167,7 +167,7 @@ static ngx_int_t ip_trie_add(ip_trie_t* trie, inx_addr_t* inx_addr, uint32_t suf } } uint8_index = bit_index / 8; - if (NGX_HTTP_WAF_CHECK_BIT(u8_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { + if (ngx_http_waf_check_bit(u8_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { cur_node->left = new_node; } else { cur_node->right = new_node; @@ -188,7 +188,7 @@ static ngx_int_t ip_trie_add(ip_trie_t* trie, inx_addr_t* inx_addr, uint32_t suf } } prev_node = cur_node; - if (NGX_HTTP_WAF_CHECK_BIT(inx_addr->ipv6.s6_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { + if (ngx_http_waf_check_bit(inx_addr->ipv6.s6_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { cur_node = cur_node->left; prev_bit = 0; } else { @@ -209,7 +209,7 @@ static ngx_int_t ip_trie_add(ip_trie_t* trie, inx_addr_t* inx_addr, uint32_t suf } } uint8_index = bit_index / 8; - if (NGX_HTTP_WAF_CHECK_BIT(inx_addr->ipv6.s6_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { + if (ngx_http_waf_check_bit(inx_addr->ipv6.s6_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { cur_node->left = new_node; } else { cur_node->right = new_node; @@ -240,7 +240,7 @@ static ngx_int_t ip_trie_find(ip_trie_t* trie, inx_addr_t* inx_addr, ip_trie_nod while (bit_index < 32 && cur_node != NULL && cur_node->is_ip != NGX_HTTP_WAF_TRUE) { int uint8_index = bit_index / 8; - if (NGX_HTTP_WAF_CHECK_BIT(u8_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { + if (ngx_http_waf_check_bit(u8_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { cur_node = cur_node->left; } else { cur_node = cur_node->right; @@ -251,7 +251,7 @@ static ngx_int_t ip_trie_find(ip_trie_t* trie, inx_addr_t* inx_addr, ip_trie_nod } else if (trie->ip_type == AF_INET6) { while (bit_index < 128 && cur_node != NULL && cur_node->is_ip != NGX_HTTP_WAF_TRUE) { int uint8_index = bit_index / 8; - if (NGX_HTTP_WAF_CHECK_BIT(inx_addr->ipv6.s6_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { + if (ngx_http_waf_check_bit(inx_addr->ipv6.s6_addr[uint8_index], 7 - (bit_index % 8)) != NGX_HTTP_WAF_TRUE) { cur_node = cur_node->left; } else { cur_node = cur_node->right; diff --git a/inc/ngx_http_waf_module_macro.h b/inc/ngx_http_waf_module_macro.h index f1b72f37..537856cd 100644 --- a/inc/ngx_http_waf_module_macro.h +++ b/inc/ngx_http_waf_module_macro.h @@ -19,6 +19,7 @@ #define NGX_HTTP_WAF_WHITE_IPV6_FILE ("white-ipv6") #define NGX_HTTP_WAF_WHITE_URL_FILE ("white-url") #define NGX_HTTP_WAF_WHITE_REFERER_FILE ("white-referer") +#define NGX_HTTP_WAF_ADVANCED_FILE ("advanced") #define NGX_HTTP_WAF_FALSE (0) @@ -341,7 +342,7 @@ /* 检查对应文件是否存在,如果存在则根据 mode 的值将数据处理后存入容器中 */ /** - * @def NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, folder, end, filename, container, mode) + * @def ngx_http_waf_check_and_load_conf(cf, folder, end, filename, container, mode) * @brief 检查对应文件是否存在,如果存在则根据 mode 的值将数据处理后存入数组中。 * @param[in] folder 配置文件所在文件夹的绝对路径。 * @param[in] end folder 字符数组的 '\0' 的地址。 @@ -350,7 +351,7 @@ * @param[in] mode 配置读取模式。 * @warning 当文件不存在的时候会直接执行 @code return NGX_CONF_ERROR; @endcode 语句。 */ -#define NGX_HTTP_WAF_CHECK_AND_LOAD_CONF(cf, folder, end, filename, container, mode) { \ +#define ngx_http_waf_check_and_load_conf(cf, folder, end, filename, container, mode) { \ strcat((folder), (filename)); \ if (access((folder), R_OK) != 0) { \ ngx_conf_log_error(NGX_LOG_ERR, (cf), 0, "ngx_waf: %s: %s", (folder), "No such file or directory"); \ @@ -364,27 +365,33 @@ } /** - * @def NGX_HTTP_WAF_CHECK_FLAG(origin, flag) + * @def ngx_http_waf_check_flag(origin, flag) * @brief 检查 flag 是否存在于 origin 中,即位操作。 * @return 存在则返回 NGX_HTTP_WAF_TRUE,反之返回 NGX_HTTP_WAF_FALSE。 * @retval NGX_HTTP_WAF_TRUE 存在。 * @retval NGX_HTTP_WAF_FALSE 不存在。 */ -#define NGX_HTTP_WAF_CHECK_FLAG(origin, flag) (((origin) & (flag)) == (flag) ? NGX_HTTP_WAF_TRUE : NGX_HTTP_WAF_FALSE) +#define ngx_http_waf_check_flag(origin, flag) (((origin) & (flag)) == (flag) ? NGX_HTTP_WAF_TRUE : NGX_HTTP_WAF_FALSE) /** - * @def NGX_HTTP_WAF_CHECK_BIT(origin, bit_index) + * @def ngx_http_waf_check_bit(origin, bit_index) * @brief 检查 origin 的某一位是否为 1。 * @return 如果为一则返回 NGX_HTTP_WAF_TRUE,反之返回 NGX_HTTP_WAF_FALSE。 * @retval NGX_HTTP_WAF_TRUE 被测试的位为一。 * @retval NGX_HTTP_WAF_FALSE 被测试的位为零。 * @note bit_index 从 0 开始计数,其中 0 代表最低位。 */ -#define NGX_HTTP_WAF_CHECK_BIT(origin, bit_index) (NGX_HTTP_WAF_CHECK_FLAG((origin), 1 << (bit_index))) +#define ngx_http_waf_check_bit(origin, bit_index) (ngx_http_waf_check_flag((origin), 1 << (bit_index))) -#define NGX_HTTP_WAF_MAKE_UTARRAY_NGX_STR_ICD() { sizeof(ngx_str_t), NULL, utarray_ngx_str_ctor, utarray_ngx_str_dtor } +#define ngx_http_waf_make_utarray_ngx_str_icd() { sizeof(ngx_str_t), NULL, utarray_ngx_str_ctor, utarray_ngx_str_dtor } + +#define ngx_http_waf_make_utarray_vm_code_icd() { sizeof(vm_code_t), NULL, utarray_vm_code_ctor, utarray_vm_code_dtor } + +#define ngx_strdup(s) ((u_char*)strdup((char*)(s))); + +#define ngx_strcpy(d, s) (strcpy((char*)d, (const char*)s)) #endif // !NGX_HTTP_WAF_MODULE_MACRO_H diff --git a/inc/ngx_http_waf_module_type.h b/inc/ngx_http_waf_module_type.h index e7e10f42..b0e4cd01 100644 --- a/inc/ngx_http_waf_module_type.h +++ b/inc/ngx_http_waf_module_type.h @@ -4,6 +4,7 @@ */ #include +#include #include #include #include @@ -75,6 +76,53 @@ typedef enum { } mem_pool_type_e; +typedef enum { + VM_CODE_NOP, + VM_CODE_PUSH_IP, + VM_CODE_PUSH_INT, + VM_CODE_PUSH_STR, + VM_CODE_PUSH_REGEXP, + VM_CODE_PUSH_CLIENT_IP, + VM_CODE_PUSH_URL, + VM_CODE_PUSH_QUERY_STRING, + VM_CODE_PUSH_REFERER, + VM_CODE_PUSH_USER_AGENT, + VM_CODE_PUSH_HEADER_IN, + VM_CODE_PUSH_COOKIE, + VM_CODE_POP, + VM_CODE_TOP, + VM_CODE_OP_NOT, + VM_CODE_OP_AND, + VM_CODE_OP_OR, + VM_CODE_OP_CONTAINS, + VM_CODE_OP_MATCHES, + VM_CODE_OP_EQUALS, + VM_CODE_OP_BELONG_TO, + VM_CODE_OP_SQLI_DETN, + VM_CODE_OP_XSS_DETN, + VM_CODE_ACT_RETURN, + VM_CODE_ACT_ALLOW +} vm_code_type_e; + + +typedef enum { + VM_DATA_VOID, + VM_DATA_STR, + VM_DATA_INT, + VM_DATA_BOOL, + VM_DATA_REGEXP, + VM_DATA_IPV4, + VM_DATA_IPV6 +} vm_data_type_e; + + +typedef struct key_value_s { + ngx_str_t key; + ngx_str_t value; + UT_hash_handle hh; +} key_value_t; + + /** * @struct mem_pool_t * @brief 包含常规内存池或 slab 内存池 @@ -223,6 +271,7 @@ typedef struct ngx_http_waf_srv_conf_s { ip_trie_t white_ipv6; /**< IPV6 白名单 */ ngx_array_t *white_url; /**< URL 白名单 */ ngx_array_t *white_referer; /**< Referer 白名单 */ + UT_array advanced_rule; ngx_shm_zone_t *shm_zone_cc_deny; /**< 共享内存 */ ip_trie_t *ipv4_access_statistics; /**< IP 访问频率统计表 */ ip_trie_t *ipv6_access_statistics; /**< IP 访问频率统计表 */ @@ -247,8 +296,8 @@ typedef struct ngx_http_waf_srv_conf_s { typedef struct ipv4_s { u_char text[32]; /**< 点分十进制表示法 */ uint32_t prefix; /**< 相当于 192.168.1.0/24 中的 192.168.1.0 的整数形式 */ - uint32_t suffix; /**< 相当于 192.168.1.0/24 中的 24 */ - uint32_t suffix_num; + uint32_t suffix; /**< 相当于 192.168.1.0/24 中的 24 的位表示(网络字节序) */ + uint32_t suffix_num; /**< 相当于 192.168.1.0/24 中的 24 */ } ipv4_t; @@ -261,8 +310,30 @@ typedef struct ipv4_s { typedef struct ipv6_s { u_char text[64]; /**< 冒号十六进制表示法 */ uint8_t prefix[16]; /**< 相当于 ffff::ffff/64 中的 ffff::ffff 的整数形式 */ - uint8_t suffix[16]; /**< 相当于 ffff::ffff/64 中的 64 */ - uint32_t suffix_num; + uint8_t suffix[16]; /**< 相当于 ffff::ffff/64 中的 64 的位表示(网络字节序) */ + uint32_t suffix_num; /**< 相当于 ffff::ffff/64 中的 64 */ } ipv6_t; + +typedef struct vm_stack_item_s { + vm_data_type_e type[4]; + size_t argc; + union { + int int_val; + ngx_str_t str_val; + uint8_t bool_val; + ngx_regex_elt_t regexp_val; + ipv4_t ipv4_val; + ipv6_t ipv6_val; + inx_addr_t inx_addr_val; + } value[4]; + struct vm_stack_item_s *utstack_handle; +} vm_stack_item_t; + + +typedef struct vm_code_s { + vm_code_type_e type; + struct vm_stack_item_s argv; +} vm_code_t; + #endif // !NGX_HTTP_WAF_MODULE_TYPE_H \ No newline at end of file diff --git a/inc/ngx_http_waf_module_util.h b/inc/ngx_http_waf_module_util.h index 9cb14ba9..de41ffb3 100644 --- a/inc/ngx_http_waf_module_util.h +++ b/inc/ngx_http_waf_module_util.h @@ -67,6 +67,26 @@ static ngx_int_t parse_size(u_char* str); static ngx_int_t parse_cookie(ngx_str_t* native_cookie, UT_array** array); +/** + * @brief 将一个 Query String 字符串解析为哈希表 + * @param[in] native_query_string 字符串形式的 Cookie + * @param[out] hash_head 保存解析结果的哈希表 + * @return 成功则返回 SUCCESS,反之则不是。 + * @warning 使用完毕后请自行释放数组所占用内存。 +*/ +static ngx_int_t parse_query_string(ngx_str_t* native_query_string, key_value_t** hash_head); + + +/** + * @brief 将一个 Header 列表解析为哈希表 + * @param[in] native_header Header 列表 + * @param[out] hash_head 保存解析结果的哈希表 + * @return 成功则返回 SUCCESS,反之则不是。 + * @warning 使用完毕后请自行释放数组所占用内存。 +*/ +static ngx_int_t parse_header(ngx_list_t* native_header, key_value_t** hash_head); + + /** * @brief 字符串分割 * @param[in] str 要分割的字符串 @@ -79,6 +99,12 @@ static ngx_int_t parse_cookie(ngx_str_t* native_cookie, UT_array** array); static ngx_int_t ngx_str_split(ngx_str_t* str, u_char sep, size_t max_len, UT_array** array); +static ngx_int_t ipv4_netcmp(uint32_t ip, const ipv4_t* ipv4); + + +static ngx_int_t ipv6_netcmp(uint8_t ip[16], const ipv6_t* ipv6); + + /** * @brief 字符串分割 * @param[in] str 要分割的字符串 @@ -122,10 +148,16 @@ static ngx_int_t rand_str(u_char* dest, size_t len); static ngx_int_t sha256(u_char* dst, size_t dst_len, const u_char* buf, size_t buf_len); -void utarray_ngx_str_ctor(void *dst, const void *src); +static void utarray_ngx_str_ctor(void *dst, const void *src); + + +static void utarray_ngx_str_dtor(void* elt); + +static void utarray_vm_code_ctor(void *dst, const void *src); -void utarray_ngx_str_dtor(void* elt); + +static void utarray_vm_code_dtor(void* elt); /** @@ -185,16 +217,14 @@ static ngx_int_t parse_ipv4(ngx_str_t text, ipv4_t* ipv4) { suffix_num = suffix; uint8_t temp_suffix[4] = { 0 }; - int i; - for (i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { uint8_t temp = 0; if (suffix >= 8) { suffix -=8; temp = ~0; } else { - uint32_t j; - for (j = 0; j < suffix; j++) { + for (uint32_t j = 0; j < suffix; j++) { temp |= 0x80 >> j; } suffix = 0; @@ -203,7 +233,7 @@ static ngx_int_t parse_ipv4(ngx_str_t text, ipv4_t* ipv4) { } suffix = 0; - for (i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { suffix |= ((uint32_t)temp_suffix[i]) << (i * 8); } @@ -268,16 +298,14 @@ static ngx_int_t parse_ipv6(ngx_str_t text, ipv6_t* ipv6) { } suffix_num = temp_suffix; - int i; - for (i = 0; i < 16; i++) { + for (int i = 0; i < 16; i++) { uint8_t temp = 0; if (temp_suffix >= 8) { temp_suffix -=8; temp = ~0; } else { - uint32_t j; - for (j = 0; j < temp_suffix; j++) { + for (uint32_t j = 0; j < temp_suffix; j++) { temp |= 0x80 >> j; } temp_suffix = 0; @@ -285,7 +313,7 @@ static ngx_int_t parse_ipv6(ngx_str_t text, ipv6_t* ipv6) { suffix[i] = temp; } - for (i = 0; i < 16; i++) { + for (int i = 0; i < 16; i++) { prefix[i] &= suffix[i]; } @@ -349,7 +377,7 @@ static ngx_int_t parse_cookie(ngx_str_t* native_cookie, UT_array** array) { return NGX_HTTP_WAF_FAIL; } - UT_icd icd = NGX_HTTP_WAF_MAKE_UTARRAY_NGX_STR_ICD(); + UT_icd icd = ngx_http_waf_make_utarray_ngx_str_icd(); utarray_new(*array, &icd); if (native_cookie == NULL) { @@ -398,12 +426,124 @@ static ngx_int_t parse_cookie(ngx_str_t* native_cookie, UT_array** array) { } +static ngx_int_t parse_query_string(ngx_str_t* native_query_string, key_value_t** hash_head) { + if (hash_head == NULL) { + return NGX_HTTP_WAF_FAIL; + } + + if (native_query_string == NULL) { + return NGX_HTTP_WAF_FAIL; + } + + + UT_array* kvs = NULL; + + ngx_str_split(native_query_string, '&', native_query_string->len, &kvs); + ngx_str_t* p = NULL; + + while (p = (ngx_str_t*)utarray_next(kvs, p), p != NULL) { + UT_array* key_and_value = NULL; + ngx_str_t temp; + temp.data = p->data; + temp.len = p->len; + + ngx_str_split(&temp, '=', native_query_string->len, &key_and_value); + + if (utarray_len(key_and_value) != 2) { + return NGX_HTTP_WAF_FAIL; + } + + ngx_str_t* key = NULL; + ngx_str_t* value = NULL; + + + key = (ngx_str_t*)utarray_next(key_and_value, NULL); + value = (ngx_str_t*)utarray_next(key_and_value, key); + + key_value_t* qs = malloc(sizeof(key_value_t)); + ngx_memzero(qs, sizeof(key_value_t)); + qs->key.data = ngx_strdup(key->data); + qs->key.len = key->len; + qs->value.data = ngx_strdup(value->data); + qs->value.len = value->len; + + HASH_ADD_KEYPTR(hh, *hash_head, qs->key.data, qs->key.len * sizeof(u_char), qs); + + utarray_free(key_and_value); + } + + utarray_free(kvs); + return NGX_HTTP_WAF_SUCCESS; +} + + +static ngx_int_t parse_header(ngx_list_t* native_header, key_value_t** hash_head) { + if (native_header == NULL || hash_head == NULL) { + return NGX_HTTP_WAF_FALSE; + } + + ngx_list_part_t* part = &(native_header->part); + ngx_table_elt_t* value = part->elts; + + for (size_t i = 0; ; i++) { + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + value = part->elts; + i = 0; + } + + key_value_t* temp = malloc(sizeof(key_value_t)); + ngx_memzero(temp, sizeof(key_value_t)); + temp->key.data = ngx_strdup(value[i].key.data); + temp->key.len = value[i].key.len; + ngx_strlow(temp->key.data, temp->key.data, temp->key.len); + temp->value.data = ngx_strdup(value[i].value.data); + temp->value.len = value[i].value.len; + HASH_ADD_KEYPTR(hh, *hash_head, temp->key.data, temp->key.len * sizeof(u_char), temp); + + } + + return NGX_HTTP_WAF_TRUE; +} + + +static ngx_int_t ipv4_netcmp(uint32_t ip, const ipv4_t* ipv4) { + size_t prefix = ip & ipv4->suffix; + + if (prefix == ipv4->prefix) { + return NGX_HTTP_WAF_MATCHED; + } + + return NGX_HTTP_WAF_NOT_MATCHED; +} + +static ngx_int_t ipv6_netcmp(uint8_t ip[16], const ipv6_t* ipv6) { + uint8_t temp_ip[16]; + + memcpy(temp_ip, ip, 16); + + for (int i = 0; i < 16; i++) { + temp_ip[i] &= ipv6->suffix[i]; + } + + if (memcmp(temp_ip, ipv6->prefix, sizeof(uint8_t) * 16) != 0) { + return NGX_HTTP_WAF_NOT_MATCHED; + } + + return NGX_HTTP_WAF_MATCHED; +} + + static ngx_int_t ngx_str_split(ngx_str_t* str, u_char sep, size_t max_len, UT_array** array) { if (array == NULL) { return NGX_HTTP_WAF_FAIL; } - UT_icd icd = NGX_HTTP_WAF_MAKE_UTARRAY_NGX_STR_ICD(); + UT_icd icd = ngx_http_waf_make_utarray_ngx_str_icd(); utarray_new(*array,&icd); if (str == NULL) { @@ -449,7 +589,7 @@ static ngx_int_t ngx_str_split(ngx_str_t* str, u_char sep, size_t max_len, UT_ar // return NGX_HTTP_WAF_FAIL; // } -// UT_icd icd = NGX_HTTP_WAF_MAKE_UTARRAY_NGX_STR_ICD(); +// UT_icd icd = ngx_http_waf_make_utarray_ngx_str_icd(); // utarray_new(*array,&icd); // ngx_str_t temp_str; @@ -549,4 +689,43 @@ void utarray_ngx_str_dtor(void* elt) { } +void utarray_vm_code_ctor(void *dst, const void *src) { + vm_code_t* _dst = (vm_code_t*)dst; + const vm_code_t* _src = (const vm_code_t*)src; + _dst->type = _src->type; + _dst->argv.argc = _src->argv.argc; + + + for (size_t i = 0; i < _src->argv.argc; i++) { + _dst->argv.type[i] = _src->argv.type[i]; + + if (_src->argv.type[i] == VM_DATA_STR) { + size_t len = _src->argv.value[i].str_val.len; + _dst->argv.value[i].str_val.len = len; + _dst->argv.value[i].str_val.data = (u_char*)malloc(sizeof(u_char) * (len + 1)); + ngx_memcpy(_dst->argv.value[i].str_val.data, _src->argv.value[i].str_val.data, sizeof(u_char) * len); + _dst->argv.value[i].str_val.data[len] = '\0'; + _dst->argv.value[i].str_val.len = len; + } else { + ngx_memcpy(&(_dst->argv.value[i]), &(_src->argv.value[i]), sizeof(_src->argv.value[i])); + } + } +} + + +void utarray_vm_code_dtor(void* elt) { + vm_code_t* _elt = (vm_code_t*)elt; + + for (size_t i = 0; i < _elt->argv.argc; i++) { + switch (_elt->argv.type[i]) { + case VM_DATA_STR: + free(_elt->argv.value[i].str_val.data); + break; + default: + break; + } + } +} + + #endif \ No newline at end of file diff --git a/inc/ngx_http_waf_module_vm.h b/inc/ngx_http_waf_module_vm.h new file mode 100644 index 00000000..4626bc26 --- /dev/null +++ b/inc/ngx_http_waf_module_vm.h @@ -0,0 +1,531 @@ +#ifndef __NGX_HTTP_WAF_MODULE_VM_H__ +#define __NGX_HTTP_WAF_MODULE_VM_H__ + +#include +#include +#include +#include + +static void print_code(UT_array* array); + + +static ngx_int_t ngx_http_waf_vm_exec(ngx_http_request_t* r, ngx_int_t* out_http_status) { + static ngx_str_t s_empty_str = ngx_string(""); + ngx_http_waf_srv_conf_t* srv_conf = NULL; + ngx_http_waf_ctx_t* ctx = NULL; + ngx_http_waf_get_ctx_and_conf(r, &srv_conf, &ctx); + + ngx_int_t ret = NGX_HTTP_WAF_NOT_MATCHED; + + // ipv4_t client_ipv4; + + ngx_str_t* url = &(r->uri); + if (url->len == 0 || url->data == NULL) { + url = &s_empty_str; + } + + ngx_str_t* user_agent = &s_empty_str; + if (r->headers_in.user_agent != NULL && user_agent->len != 0 && user_agent->data != NULL) { + user_agent = &(r->headers_in.user_agent->value); + } + + ngx_str_t* referer = &s_empty_str; + if (r->headers_in.referer != NULL && referer->len == 0 && referer->data == NULL) { + referer = &(r->headers_in.referer->value); + } + + key_value_t* query_string = NULL; + parse_query_string(&(r->args), &query_string); + + key_value_t* header_in = NULL; + parse_header(&(r->headers_in.headers), &header_in); + + + + vm_stack_item_t* stack = NULL; + vm_code_t* code = NULL; + + while (code = (vm_code_t*)utarray_next(&(srv_conf->advanced_rule), code), code != NULL) { + vm_stack_item_t* argv = &(code->argv); + switch (code->type) { + case VM_CODE_PUSH_INT: + break; + + case VM_CODE_PUSH_STR: + { + vm_stack_item_t* temp = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + temp->type[0] = VM_DATA_STR; + temp->argc = 1; + temp->value[0].str_val.data = ngx_pcalloc(r->pool, sizeof(u_char) * (argv->value[0].str_val.len + 1)); + temp->value[0].str_val.len = argv->value[0].str_val.len; + ngx_memcpy(temp->value[0].str_val.data, argv->value[0].str_val.data, sizeof(u_char) * argv->value[0].str_val.len); + STACK_PUSH2(stack, temp, utstack_handle); + break; + } + + case VM_CODE_PUSH_CLIENT_IP: + { + vm_stack_item_t* temp = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + + if (r->connection->sockaddr->sa_family == AF_INET) { + struct sockaddr_in* sin = (struct sockaddr_in*)r->connection->sockaddr; + temp->type[0] = VM_DATA_IPV4; + ngx_memcpy(&(temp->value[0].inx_addr_val.ipv4), &(sin->sin_addr), sizeof(struct in_addr)); + } else if (r->connection->sockaddr->sa_family == AF_INET6) { + struct sockaddr_in6* sin6 = (struct sockaddr_in6*)r->connection->sockaddr; + temp->type[0] = VM_DATA_IPV6; + ngx_memcpy(&(temp->value[0].inx_addr_val.ipv6), &(sin6->sin6_addr), sizeof(struct in_addr)); + } + + temp->argc = 1; + STACK_PUSH2(stack, temp, utstack_handle); + break; + } + + case VM_CODE_PUSH_URL: + { + vm_stack_item_t* temp = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + temp->type[0] = VM_DATA_STR; + temp->argc = 1; + temp->value[0].str_val.data = ngx_pcalloc(r->pool, sizeof(u_char) * (url->len + 1)); + temp->value[0].str_val.len = url->len; + ngx_memcpy(temp->value[0].str_val.data, url->data, sizeof(u_char) * url->len); + STACK_PUSH2(stack, temp, utstack_handle); + break; + } + + case VM_CODE_PUSH_USER_AGENT: + { + vm_stack_item_t* temp = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + temp->type[0] = VM_DATA_STR; + temp->argc = 1; + temp->value[0].str_val.data = ngx_pcalloc(r->pool, sizeof(u_char) * (user_agent->len + 1)); + temp->value[0].str_val.len = user_agent->len; + ngx_memcpy(temp->value[0].str_val.data, user_agent->data, sizeof(u_char) * user_agent->len); + STACK_PUSH2(stack, temp, utstack_handle); + break; + } + + case VM_CODE_PUSH_REFERER: + { + vm_stack_item_t* temp = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + temp->type[0] = VM_DATA_STR; + temp->argc = 1; + temp->value[0].str_val.data = ngx_pcalloc(r->pool, sizeof(u_char) * (referer->len + 1)); + temp->value[0].str_val.len = referer->len; + ngx_memcpy(temp->value[0].str_val.data, referer->data, sizeof(u_char) * referer->len); + STACK_PUSH2(stack, temp, utstack_handle); + break; + } + + case VM_CODE_PUSH_QUERY_STRING: + { + vm_stack_item_t* result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_STR; + result->argc = 1; + key_value_t* temp = NULL; + HASH_FIND(hh, query_string, argv->value[0].str_val.data, argv->value[0].str_val.len * sizeof(u_char), temp); + if (temp != NULL) { + result->value[0].str_val.data = ngx_pcalloc(r->pool, sizeof(u_char) * (temp->value.len + 1)); + ngx_memcpy(result->value[0].str_val.data, temp->value.data, sizeof(u_char) * temp->value.len); + result->value[0].str_val.len = temp->value.len; + } else { + result->value[0].str_val.data = ngx_pcalloc(r->pool, 1); + result->value[0].str_val.len = 0; + } + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_PUSH_HEADER_IN: + { + ngx_str_t header_key; + header_key.data = ngx_strdup(argv->value[0].str_val.data); + header_key.len = argv->value[0].str_val.len; + ngx_strlow(header_key.data, header_key.data, header_key.len); + + key_value_t* temp = NULL; + HASH_FIND(hh, header_in, header_key.data, header_key.len * sizeof(u_char), temp); + + free(header_key.data); + header_key.len = 0; + + vm_stack_item_t* result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_STR; + result->argc = 1; + + if (temp != NULL) { + result->value[0].str_val.data = ngx_pcalloc(r->pool, sizeof(u_char) * (temp->value.len + 1)); + ngx_memcpy(result->value[0].str_val.data, temp->value.data, sizeof(u_char) * temp->value.len); + result->value[0].str_val.len = temp->value.len; + } else { + result->value[0].str_val.data = ngx_pcalloc(r->pool, sizeof(u_char)); + result->value[0].str_val.len = 0; + } + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_PUSH_IP: + case VM_CODE_PUSH_REGEXP: + STACK_PUSH2(stack, argv, utstack_handle); + break; + + case VM_CODE_OP_NOT: + { + vm_stack_item_t* temp = NULL; + STACK_POP2(stack, temp, utstack_handle); + temp->value[0].bool_val = !temp->value[0].bool_val; + STACK_PUSH2(stack, temp, utstack_handle); + break; + } + + case VM_CODE_OP_AND: + case VM_CODE_OP_OR: + { + vm_stack_item_t *left = NULL, *right = NULL, *result = NULL; + STACK_POP2(stack, left, utstack_handle); + STACK_POP2(stack, right, utstack_handle); + result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_BOOL; + result->argc = 1; + if (code->type == VM_CODE_OP_AND) { + result->value[0].bool_val = left->value[0].bool_val && right->value[0].bool_val; + } else { + result->value[0].bool_val = left->value[0].bool_val || right->value[0].bool_val; + } + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_OP_BELONG_TO: + { + vm_stack_item_t *left = NULL, *right = NULL, *result = NULL; + STACK_POP2(stack, left, utstack_handle); + STACK_POP2(stack, right, utstack_handle); + result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_BOOL; + result->argc = 1; + + if (left->type[0] == VM_DATA_IPV4 && right->type[0] == VM_DATA_STR) { + ipv4_t ipv4; + if (parse_ipv4(right->value[0].str_val, &ipv4) == NGX_HTTP_WAF_SUCCESS) { + if (ipv4_netcmp(left->value[0].inx_addr_val.ipv4.s_addr, &ipv4) == NGX_HTTP_WAF_MATCHED) { + result->value[0].bool_val = 1; + } else { + result->value[0].bool_val = 0; + } + } else { + result->value[0].bool_val = 0; + } + + } else if (left->type[0] == VM_DATA_IPV6 && right->type[0] == VM_DATA_STR) { + ipv6_t ipv6; + if (parse_ipv6(right->value[0].str_val, &ipv6) == NGX_HTTP_WAF_SUCCESS) { + if (ipv6_netcmp(left->value[0].inx_addr_val.ipv6.s6_addr, &ipv6) == NGX_HTTP_WAF_MATCHED) { + result->value[0].bool_val = 1; + } else { + result->value[0].bool_val = 0; + } + } else { + result->value[0].bool_val = 0; + } + } else { + result->value[0].bool_val = 0; + } + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_OP_EQUALS: + { + vm_stack_item_t *left = NULL, *right = NULL, *result = NULL; + STACK_POP2(stack, left, utstack_handle); + STACK_POP2(stack, right, utstack_handle); + result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_BOOL; + result->argc = 1; + if (left->type[0] == VM_DATA_STR && right->type[0] == VM_DATA_STR) { + if (ngx_strcmp(left->value[0].str_val.data, right->value[0].str_val.data) == 0) { + result->value[0].bool_val = 1; + } else { + result->value[0].bool_val = 0; + } + } else if (left->type[0] == VM_DATA_IPV4 && right->type[0] == VM_DATA_STR) { + struct in_addr addr4; + if (inet_pton(AF_INET, (char*)right->value[0].str_val.data, &addr4) != 1) { + result->value[0].bool_val = 0; + } else { + result->value[0].bool_val = ngx_memcmp(&(left->value[0].inx_addr_val.ipv4), + &addr4, + sizeof(struct in_addr)); + } + + } else if (left->type[0] == VM_DATA_IPV6 && right->type[0] == VM_DATA_STR) { + struct in6_addr addr6; + if (inet_pton(AF_INET6, (char*)right->value[0].str_val.data, &addr6) != 1) { + result->value[0].bool_val = 0; + } else { + result->value[0].bool_val = ngx_memcmp(&(left->value[0].inx_addr_val.ipv6), + &addr6, + sizeof(struct in6_addr)); + } + } else { + result->value[0].bool_val = 0; + } + + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_OP_CONTAINS: + { + vm_stack_item_t *left = NULL, *right = NULL, *result = NULL; + STACK_POP2(stack, left, utstack_handle); + STACK_POP2(stack, right, utstack_handle); + result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_BOOL; + result->argc = 1; + if (ngx_strstr(left->value[0].str_val.data, right->value[0].str_val.data) != NULL) { + result->value[0].bool_val = 1; + } else { + result->value[0].bool_val = 0; + } + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_OP_MATCHES: + { + vm_stack_item_t *left = NULL, *right = NULL, *result = NULL; + STACK_POP2(stack, left, utstack_handle); + STACK_POP2(stack, right, utstack_handle); + + result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_BOOL; + result->argc = 1; + + ngx_regex_compile_t regex_compile; + u_char errstr[NGX_MAX_CONF_ERRSTR]; + ngx_regex_elt_t* ngx_regex_elt = ngx_pcalloc(r->pool, sizeof(ngx_regex_elt_t)); + ngx_memzero(®ex_compile, sizeof(ngx_regex_compile_t)); + ngx_memcpy(&(regex_compile.pattern), &(right->value[0].str_val), sizeof(ngx_str_t)); + regex_compile.pool = r->pool; + regex_compile.err.len = NGX_MAX_CONF_ERRSTR; + regex_compile.err.data = errstr; + + if (ngx_regex_compile(®ex_compile) != NGX_OK) { + result->value[0].bool_val = 0; + } else { + ngx_regex_elt->regex = regex_compile.regex; + ngx_int_t rc = ngx_regex_exec(ngx_regex_elt->regex, &(left->value[0].str_val), NULL, 0); + if (rc >= 0) { + result->value[0].bool_val = 1; + } else { + result->value[0].bool_val = 0; + } + } + + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_OP_SQLI_DETN: + { + vm_stack_item_t *operand = NULL, *result = NULL; + STACK_POP2(stack, operand, utstack_handle); + result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_BOOL; + result->argc = 1; + result->value[0].bool_val = 0; + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_LIB_INJECTION_SQLI) == NGX_HTTP_WAF_TRUE) { + sfilter sf; + libinjection_sqli_init(&sf, + (char*)(operand->value[0].str_val.data), + operand->value[0].str_val.len, + FLAG_NONE | + FLAG_QUOTE_NONE | + FLAG_QUOTE_SINGLE | + FLAG_QUOTE_DOUBLE | + FLAG_SQL_ANSI | + FLAG_SQL_MYSQL); + if (libinjection_is_sqli(&sf) == 1) { + result->value[0].bool_val = 1; + } + } + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_OP_XSS_DETN: + { + vm_stack_item_t *operand = NULL, *result = NULL; + STACK_POP2(stack, operand, utstack_handle); + result = ngx_pcalloc(r->pool, sizeof(vm_stack_item_t)); + result->type[0] = VM_DATA_BOOL; + result->argc = 1; + result->value[0].bool_val = 0; + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_LIB_INJECTION_XSS) == NGX_HTTP_WAF_TRUE) { + if (libinjection_xss((char*)(operand->value[0].str_val.data), operand->value[0].str_val.len) == 1) { + result->value[0].bool_val = 1; + } + } + STACK_PUSH2(stack, result, utstack_handle); + break; + } + + case VM_CODE_ACT_RETURN: + { + vm_stack_item_t *bool_val = NULL, *id = NULL; + STACK_POP2(stack, bool_val, utstack_handle); + STACK_POP2(stack, id, utstack_handle); + if (bool_val->value->bool_val) { + ctx->blocked = NGX_HTTP_WAF_TRUE; + ctx->checked = NGX_HTTP_WAF_TRUE; + *out_http_status = argv->value[0].int_val; + ngx_strcpy(ctx->rule_type, "ADVANCED"); + ngx_strcpy(ctx->rule_deatils, id->value[0].str_val.data); + ret = NGX_HTTP_WAF_MATCHED; + goto RELEASE; + } + break; + } + + case VM_CODE_ACT_ALLOW: + { + vm_stack_item_t *bool_val = NULL, *id = NULL; + STACK_POP2(stack, bool_val, utstack_handle); + STACK_POP2(stack, id, utstack_handle); + if (bool_val->value->bool_val) { + ctx->blocked = NGX_HTTP_WAF_FALSE; + ctx->checked = NGX_HTTP_WAF_TRUE; + *out_http_status = NGX_DECLINED; + ngx_strcpy(ctx->rule_type, "ADVANCED"); + ngx_strcpy(ctx->rule_deatils, id->value[0].str_val.data); + ret = NGX_HTTP_WAF_MATCHED; + goto RELEASE; + } + break; + } + + default: + break; + } + } + + RELEASE: + while (!STACK_EMPTY(stack)) { + vm_stack_item_t* temp = NULL; + STACK_POP2(stack, temp, utstack_handle); + } + + key_value_t *temp0 = NULL, *temp1 = NULL; + HASH_ITER(hh, query_string, temp0, temp1) { + free(temp0->key.data); + free(temp0->value.data); + free(temp0); + HASH_DEL(query_string, temp0); + } + + temp0 = NULL; + temp1 = NULL; + HASH_ITER(hh, header_in, temp0, temp1) { + free(temp0->key.data); + free(temp0->value.data); + free(temp0); + HASH_DEL(header_in, temp0); + } + + return ret; +} + + +static void print_code(UT_array* array) { + vm_code_t* p = NULL; + while (p = (vm_code_t*)utarray_next(array, p), p != NULL) { + vm_code_t* q = p; + switch (q->type) { + case VM_CODE_PUSH_INT: + printf("PUSH_INT %d\n", q->argv.value[0].int_val); + break; + case VM_CODE_PUSH_IP: + if (q->argv.type[0] == VM_DATA_IPV4) { + printf("PUSH_IPV4\n"); + } else if (q->argv.type[0] == VM_DATA_IPV6) { + printf("PUSH_IPV6\n"); + } + break; + case VM_CODE_PUSH_STR: + printf("PUSH_STR %s\n", q->argv.value[0].str_val.data); + break; + case VM_CODE_PUSH_REGEXP: + printf("PUSH_REGEXP %s\n", (char*)(q->argv.value[0].regexp_val.name)); + break; + case VM_CODE_PUSH_CLIENT_IP: + printf("PUSH_CLIENT_IP\n"); + break; + case VM_CODE_PUSH_URL: + printf("PUSH_URL\n"); + break; + case VM_CODE_PUSH_USER_AGENT: + printf("PUSH_USER_AGENT\n"); + break; + case VM_CODE_PUSH_QUERY_STRING: + printf("PUSH_QUERY_STRING %s\n", (char*)(q->argv.value[0].str_val.data)); + break; + case VM_CODE_PUSH_REFERER: + printf("PUSH_REFERER %s\n", (char*)(q->argv.value[0].str_val.data)); + break; + case VM_CODE_PUSH_HEADER_IN: + printf("PUSH_HEADER_IN %s\n", (char*)(q->argv.value[0].str_val.data)); + break; + case VM_CODE_OP_NOT: + printf("OP_NOT\n"); + break; + case VM_CODE_OP_AND: + printf("OP_AND\n"); + break; + case VM_CODE_OP_OR: + printf("OP_OR\n"); + break; + case VM_CODE_OP_CONTAINS: + printf("OP_CONTAINS\n"); + break; + case VM_CODE_OP_MATCHES: + printf("OP_MATCHES\n"); + break; + case VM_CODE_OP_EQUALS: + printf("OP_EQUALS\n"); + break; + case VM_CODE_OP_BELONG_TO: + printf("OP_BELONG_TO\n"); + break; + case VM_CODE_ACT_RETURN: + printf("ACT_RET %d\n", q->argv.value[0].int_val); + break; + case VM_CODE_ACT_ALLOW: + printf("ACT_ALLOW\n"); + break; + case VM_CODE_OP_SQLI_DETN: + printf("OP_SQLI_DETN\n"); + break; + case VM_CODE_OP_XSS_DETN: + printf("OP_XSS_DETN\n"); + break; + case VM_CODE_POP: + printf("POP\n"); + break; + case VM_CODE_TOP: + printf("TOP\n"); + break; + case VM_CODE_NOP: + printf("NOP\n"); + break; + default: + break; + } + } +} + + +#endif \ No newline at end of file diff --git a/src/ngx_http_waf_module_core.c b/src/ngx_http_waf_module_core.c index 2a532046..d41f176e 100644 --- a/src/ngx_http_waf_module_core.c +++ b/src/ngx_http_waf_module_core.c @@ -118,7 +118,7 @@ static ngx_int_t ngx_http_waf_init_process(ngx_cycle_t *cycle) { static ngx_int_t ngx_http_waf_handler_server_rewrite_phase(ngx_http_request_t* r) { ngx_http_waf_srv_conf_t* srv_conf = ngx_http_get_module_srv_conf(r, ngx_http_waf_module); - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_COMPAT) == NGX_HTTP_WAF_TRUE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_COMPAT) == NGX_HTTP_WAF_TRUE) { ngx_http_waf_trigger_mem_collation_event(r); return check_all(r, NGX_HTTP_WAF_TRUE); } @@ -128,11 +128,11 @@ static ngx_int_t ngx_http_waf_handler_server_rewrite_phase(ngx_http_request_t* r static ngx_int_t ngx_http_waf_handler_access_phase(ngx_http_request_t* r) { ngx_http_waf_srv_conf_t* srv_conf = ngx_http_get_module_srv_conf(r, ngx_http_waf_module); - if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_COMPAT) == NGX_HTTP_WAF_FALSE) { + if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_COMPAT) == NGX_HTTP_WAF_FALSE) { ngx_http_waf_trigger_mem_collation_event(r); return check_all(r, NGX_HTTP_WAF_TRUE); } - else if (NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_COMPAT | NGX_HTTP_WAF_MODE_EXTRA_STRICT) == NGX_HTTP_WAF_TRUE) { + else if (ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_EXTRA_COMPAT | NGX_HTTP_WAF_MODE_EXTRA_STRICT) == NGX_HTTP_WAF_TRUE) { return check_all(r, NGX_HTTP_WAF_FALSE); } return NGX_DECLINED; @@ -397,7 +397,7 @@ static ngx_int_t check_all(ngx_http_request_t* r, ngx_int_t is_check_cc) { if ((r->method & NGX_HTTP_POST) != 0 && ctx->read_body_done == NGX_HTTP_WAF_FALSE && is_matched != NGX_HTTP_WAF_MATCHED - && NGX_HTTP_WAF_CHECK_FLAG(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_RB) == NGX_HTTP_WAF_TRUE) { + && ngx_http_waf_check_flag(srv_conf->waf_mode, NGX_HTTP_WAF_MODE_INSPECT_RB) == NGX_HTTP_WAF_TRUE) { r->request_body_in_persistent_file = 0; r->request_body_in_clean_file = 0; ctx->spend = ((double)clock() / CLOCKS_PER_SEC * 1000) - ctx->spend;