A Mechanism to Help Write Web Application Firewalls for Nginx
Developing a Web Application Firewall module for Nginx is not an easy task. The lack of input body filters makes it harder. Nginx is an outstanding web server, but it is not perfect. Actually, nothing is perfect.
So we added the input body filter mechanism to our own Nginx distribution, which is named Tengine. By taking advantage of this mechanism, processing the request body is not that complicated anymore (In standard Nginx, request body may be buffered to disk file and you have to deal with up to two buffers)
Here I have an example to demonstrate how to write an input body filter. It is a simple module to fight hash collision DoS attacks.
/* * Copyright (C) Joshua Zhu, http://www.zhuzhaoyuan.com */ #include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> typedef struct { ngx_flag_t enable; ngx_uint_t max_post_params; } ngx_http_anti_hashdos_loc_conf_t; typedef struct { ngx_uint_t post_params; } ngx_http_anti_hashdos_ctx_t; static ngx_int_t ngx_http_anti_hashdos_input_body_filter(ngx_http_request_t *r, ngx_buf_t *buf); static void *ngx_http_anti_hashdos_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_anti_hashdos_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); static ngx_int_t ngx_http_anti_hashdos_init(ngx_conf_t *cf); static ngx_command_t ngx_http_anti_hashdos_filter_commands[] = { { ngx_string("anti_hashdos"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_anti_hashdos_loc_conf_t, enable), NULL }, { ngx_string("anti_hashdos_max_post_params"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_conf_set_num_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_anti_hashdos_loc_conf_t, max_post_params), NULL }, ngx_null_command }; static ngx_http_module_t ngx_http_anti_hashdos_filter_module_ctx = { NULL, /* preconfiguration */ ngx_http_anti_hashdos_init, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_anti_hashdos_create_loc_conf,/* create location configuration */ ngx_http_anti_hashdos_merge_loc_conf /* merge location configuration */ }; ngx_module_t ngx_http_anti_hashdos_filter_module = { NGX_MODULE_V1, &ngx_http_anti_hashdos_filter_module_ctx, /* module context */ ngx_http_anti_hashdos_filter_commands,/* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; static ngx_http_input_body_filter_pt ngx_http_next_input_body_filter; static ngx_int_t ngx_http_anti_hashdos_input_body_filter(ngx_http_request_t *r, ngx_buf_t *buf) { u_char *p; ngx_http_anti_hashdos_ctx_t *ctx; ngx_http_anti_hashdos_loc_conf_t *ahlf; ahlf = ngx_http_get_module_loc_conf(r, ngx_http_anti_hashdos_filter_module); if (!ahlf->enable) { return ngx_http_next_input_body_filter(r, buf); } ctx = ngx_http_get_module_ctx(r, ngx_http_anti_hashdos_filter_module); if (ctx == NULL) { ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_anti_hashdos_ctx_t)); if (ctx == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } ctx->post_params = 1; } for (p = buf->pos; p < buf->last; p++) { if (*p == '&') { ctx->post_params++; } } if (ctx->post_params > ahlf->max_post_params) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "anti hashdos: \"%V\" blocked, too many post params: %d", &r->connection->addr_text, ctx->post_params); return NGX_HTTP_BAD_REQUEST; } return ngx_http_next_input_body_filter(r, buf); } static void * ngx_http_anti_hashdos_create_loc_conf(ngx_conf_t *cf) { ngx_http_anti_hashdos_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_anti_hashdos_loc_conf_t)); if (conf == NULL) { return NULL; } conf->enable = NGX_CONF_UNSET; conf->max_post_params = NGX_CONF_UNSET_UINT; return conf; } static char * ngx_http_anti_hashdos_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_anti_hashdos_loc_conf_t *prev = parent; ngx_http_anti_hashdos_loc_conf_t *conf = child; ngx_conf_merge_value(conf->enable, prev->enable, 0); ngx_conf_merge_uint_value(conf->max_post_params, prev->max_post_params, 120); return NGX_CONF_OK; } static ngx_int_t ngx_http_anti_hashdos_init(ngx_conf_t *cf) { ngx_http_next_input_body_filter = ngx_http_top_input_body_filter; ngx_http_top_input_body_filter = ngx_http_anti_hashdos_input_body_filter; return NGX_OK; }
The code looks quite straight forward, right? And it is similar to an output body filter, just a few steps:
1) Implement your own input body filter function. e.g.
static ngx_int_t ngx_http_anti_hashdos_input_body_filter(ngx_http_request_t *r, ngx_buf_t *buf) { /* Do the input body filtering here */ }
2) In your input body filter function, return an HTTP error code if something is wrong. Otherwise, call ngx_http_next_input_body_filter(r, buf) directly to pass the buf to the next input body filters.
3) Install your input body filter in the post_configuration hook function. Push your input body filter to the head of the input body filter chain. e.g.
static ngx_int_t ngx_http_anti_hashdos_init(ngx_conf_t *cf) { ngx_http_next_input_body_filter = ngx_http_top_input_body_filter; ngx_http_top_input_body_filter = ngx_http_anti_hashdos_input_body_filter; return NGX_OK; }
NOTE: This is just a demonstration to show how to write input body filters. If you want to fight hash collision DoS attacks completely, you have to write more code and various POST content types should be processed.
Download the code here:
http://www.zhuzhaoyuan.com/download/tengine/anti_hashdos.tar.gz