├── .dockerignore ├── Makefile ├── .travis.yml ├── config ├── t ├── 03-content-type.t ├── 02-response-status.t └── 01-on-off.t ├── Dockerfile.test ├── LICENSE ├── README.md └── ngx_http_length_hiding_filter_module.c /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile* 2 | Makefile -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build-test-image test 2 | 3 | build-test-image: 4 | docker build --rm -t nginx-length-hiding-filter-module-test -f Dockerfile.test . 5 | 6 | test: 7 | docker run --rm nginx-length-hiding-filter-module-test 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | before_install: 7 | - docker build --rm -t nginx-length-hiding-filter-module-test -f Dockerfile.test . 8 | 9 | script: 10 | - docker run --rm nginx-length-hiding-filter-module-test -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_length_hiding_filter_module 2 | 3 | if test -n "$ngx_module_link"; then 4 | ngx_module_type=HTTP_FILTER 5 | ngx_module_name=ngx_http_length_hiding_filter_module 6 | ngx_module_srcs="$ngx_addon_dir/ngx_http_length_hiding_filter_module.c" 7 | . auto/module 8 | else 9 | HTTP_FILTER_MODULES="$HTTP_FILTER_MODULES ngx_http_length_hiding_filter_module" 10 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_length_hiding_filter_module.c" 11 | fi 12 | -------------------------------------------------------------------------------- /t/03-content-type.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::Nginx::Socket; 5 | 6 | # repeat_each(2); 7 | plan tests => repeat_each() * 2 * blocks(); 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: application/json 14 | --- config 15 | location /json { 16 | length_hiding on; 17 | default_type application/json; 18 | return 200 '{"test":"ok"}'; 19 | } 20 | --- request 21 | GET /json 22 | --- error_code: 200 23 | --- response_body: {"test":"ok"} 24 | 25 | === TEST 2: text/plan 26 | --- config 27 | location /json { 28 | length_hiding on; 29 | default_type text/plan; 30 | return 200 'Hello World'; 31 | } 32 | --- request 33 | GET /json 34 | --- error_code: 200 35 | --- response_body: Hello World 36 | -------------------------------------------------------------------------------- /t/02-response-status.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::Nginx::Socket; 5 | 6 | # repeat_each(2); 7 | plan tests => repeat_each() * 2 * blocks(); 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: 204 14 | --- config 15 | location /204 { 16 | default_type text/html; 17 | length_hiding on; 18 | return 204; 19 | } 20 | --- request 21 | GET /204 22 | --- error_code: 204 23 | --- response_body_unlike: 25 | ``` 26 | For every response, length of the random strings will vary within a given range. 27 | 28 | This idea originally came from [breach-mitigation-rails](https://github.com/meldium/breach-mitigation-rails/). Thanks team! 29 | 30 | ## Warning 31 | 32 | As said in breach-migration-rails, BREACH is complicated and wide-ranging attack and this module provides only PARTIAL protection. To secure your website or service wholly, you need to review BREACH paper and find proper way according to your own website or service. 33 | 34 | ## Installation 35 | 36 | Module version | Nginx version 37 | --- | --- 38 | 1.1.x | 1.10.1 or higher 39 | 1.0.0 | 1.10.0 or earlier 40 | 41 | Download nginx sources from [http://nginx.org](http://nginx.org) and unpack it. 42 | 43 | Run configure script with adding --add-module option with the directory where this module is extracted like this: 44 | ``` 45 | ./configure --add-module=/path/to/nginx-length-hiding-filter-module 46 | ``` 47 | To compile this module as dynamic module available in 1.9.11 or later, use `--add-dynamic-module` instead 48 | ``` 49 | ./configure --add-dynamic-module=/path/to/nginx-length-hiding-filter-module 50 | ``` 51 | You can add other options along with it. Then build and install. 52 | ``` 53 | make 54 | sudo make install 55 | ``` 56 | 57 | ## Configuration Directives 58 | 59 | 60 | ### length_hiding 61 | 62 | * syntax: length_hiding on | off 63 | * default: off 64 | * context: http, server, location, if in location 65 | 66 | Enables or disables adding random generated HTML comment. 67 | 68 | ### length_hiding_max 69 | 70 | * syntax: length_hiding_max size 71 | * default: 2048 72 | * context: http, server, location 73 | 74 | Sets maximum length of random generated string used in HTML comment. The size should be within a range from 256 and 2048. 75 | 76 | ### length_hiding_types 77 | 78 | * syntax: length_hiding_types [..] 79 | * default: text/html 80 | * context: http, server, location, if in location 81 | 82 | Enables adding random generated HTML comment to responses of the specified MIME types in addition to text/html. The special value * matches any MIME type. 83 | 84 | ## Example Configuration 85 | 86 | Enable this module for specific location ('/hiding'). In this example, the length of random strings will be less than 1024. 87 | ``` 88 | server { 89 | listen 443 default_server deferred ssl http2; 90 | server_name example.com; 91 | length_hiding_max 1024; 92 | 93 | location /hiding { 94 | length_hiding on; 95 | } 96 | } 97 | ``` 98 | 99 | If this module is built as dynamic module, do NOT forget including `load_module` line in nginx configuration. 100 | ``` 101 | load_module modules/ngx_http_length_hiding_filter_module.so; 102 | ``` 103 | 104 | ## Services using this module 105 | 106 | * [Cacoo](https://cacoo.com/) 107 | * [Backlog](https://backlog.com/) 108 | * [Typetalk](https://typetalk.com/) 109 | * [Nulab Account](https://apps.nulab.com/) 110 | -------------------------------------------------------------------------------- /ngx_http_length_hiding_filter_module.c: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (C) 2013 Nulab, Inc. 4 | */ 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | typedef struct { 11 | ngx_flag_t enable; 12 | ngx_int_t max; 13 | ngx_hash_t types; 14 | ngx_array_t *types_keys; 15 | } ngx_http_length_hiding_conf_t; 16 | 17 | typedef struct { 18 | ngx_str_t comment; 19 | } ngx_http_length_hiding_ctx_t; 20 | 21 | 22 | static void* ngx_http_length_hiding_create_conf(ngx_conf_t *cf); 23 | static char* ngx_http_length_hiding_merge_conf(ngx_conf_t *cf, void *parent, 24 | void *child); 25 | static ngx_int_t ngx_http_length_hiding_filter_init(ngx_conf_t *cf); 26 | 27 | static ngx_int_t ngx_http_length_hiding_generate_random(ngx_http_request_t *r, 28 | ngx_http_length_hiding_ctx_t *ctx, ngx_http_length_hiding_conf_t *conf); 29 | 30 | static ngx_conf_num_bounds_t ngx_http_length_hiding_max_bounds = { 31 | ngx_conf_check_num_bounds, 256, 2048 32 | }; 33 | 34 | 35 | static ngx_command_t ngx_http_length_hiding_filter_commands[] = { 36 | { ngx_string("length_hiding"), 37 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_FLAG, 38 | ngx_conf_set_flag_slot, 39 | NGX_HTTP_LOC_CONF_OFFSET, 40 | offsetof(ngx_http_length_hiding_conf_t, enable), 41 | NULL }, 42 | 43 | { ngx_string("length_hiding_max"), 44 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 45 | ngx_conf_set_num_slot, 46 | NGX_HTTP_LOC_CONF_OFFSET, 47 | offsetof(ngx_http_length_hiding_conf_t, max), 48 | &ngx_http_length_hiding_max_bounds }, 49 | 50 | { ngx_string("length_hiding_types"), 51 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, 52 | ngx_http_types_slot, 53 | NGX_HTTP_LOC_CONF_OFFSET, 54 | offsetof(ngx_http_length_hiding_conf_t, types_keys), 55 | &ngx_http_html_default_types[0] }, 56 | 57 | ngx_null_command 58 | }; 59 | 60 | 61 | static ngx_http_module_t ngx_http_length_hiding_filter_module_ctx = { 62 | NULL, /* preconfiguration */ 63 | ngx_http_length_hiding_filter_init, /* postconfiguration */ 64 | 65 | NULL, /* create main configuration */ 66 | NULL, /* init main configuration */ 67 | 68 | NULL, /* create server configuration */ 69 | NULL, /* merge server configuration */ 70 | 71 | ngx_http_length_hiding_create_conf, /* create location configuration */ 72 | ngx_http_length_hiding_merge_conf /* merge location configuration */ 73 | }; 74 | 75 | 76 | ngx_module_t ngx_http_length_hiding_filter_module = { 77 | NGX_MODULE_V1, 78 | &ngx_http_length_hiding_filter_module_ctx, /* module context */ 79 | ngx_http_length_hiding_filter_commands, /* module directives */ 80 | NGX_HTTP_MODULE, /* module type */ 81 | NULL, /* init master */ 82 | NULL, /* init module */ 83 | NULL, /* init process */ 84 | NULL, /* init thread */ 85 | NULL, /* exit thread */ 86 | NULL, /* exit process */ 87 | NULL, /* exit master */ 88 | NGX_MODULE_V1_PADDING 89 | }; 90 | 91 | 92 | static ngx_http_output_header_filter_pt ngx_http_next_header_filter; 93 | static ngx_http_output_body_filter_pt ngx_http_next_body_filter; 94 | 95 | 96 | static ngx_int_t 97 | ngx_http_length_hiding_header_filter(ngx_http_request_t *r) 98 | { 99 | ngx_http_length_hiding_ctx_t *ctx; 100 | ngx_http_length_hiding_conf_t *cf; 101 | 102 | cf = ngx_http_get_module_loc_conf(r, ngx_http_length_hiding_filter_module); 103 | 104 | if(!cf->enable 105 | || r->headers_out.status == NGX_HTTP_NO_CONTENT 106 | || r->header_only 107 | || (r->method & NGX_HTTP_HEAD) 108 | || r != r->main 109 | || ngx_http_test_content_type(r, &cf->types) == NULL ) 110 | { 111 | return ngx_http_next_header_filter(r); 112 | } 113 | 114 | ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_length_hiding_ctx_t)); 115 | if(ctx == NULL){ 116 | return NGX_ERROR; 117 | } 118 | 119 | /* 120 | * generate random string comment to make it difficult for attackers to 121 | * detect size change during BREACH attach 122 | * */ 123 | if( ngx_http_length_hiding_generate_random(r, ctx, cf) != NGX_OK ){ 124 | return NGX_ERROR; 125 | } 126 | 127 | ngx_http_set_ctx(r, ctx, ngx_http_length_hiding_filter_module); 128 | 129 | if (r->headers_out.content_length_n != -1) { 130 | r->headers_out.content_length_n += ctx->comment.len; 131 | } 132 | 133 | if (r->headers_out.content_length) { 134 | r->headers_out.content_length->hash = 0; 135 | r->headers_out.content_length = NULL; 136 | } 137 | 138 | ngx_http_clear_accept_ranges(r); 139 | 140 | return ngx_http_next_header_filter(r); 141 | } 142 | 143 | 144 | static ngx_int_t 145 | ngx_http_length_hiding_body_filter(ngx_http_request_t *r, ngx_chain_t *in) 146 | { 147 | ngx_buf_t *buf; 148 | ngx_uint_t last; 149 | ngx_chain_t *cl, *nl; 150 | ngx_http_length_hiding_ctx_t *ctx; 151 | 152 | ctx = ngx_http_get_module_ctx(r, ngx_http_length_hiding_filter_module); 153 | 154 | if (ctx == NULL) { 155 | return ngx_http_next_body_filter(r, in); 156 | } 157 | 158 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 159 | "http length hiding filter : random length %d", 160 | ctx->comment.len); 161 | 162 | last = 0; 163 | for (cl = in; cl; cl = cl->next) { 164 | if (cl->buf->last_buf) { 165 | last = 1; 166 | break; 167 | } 168 | } 169 | 170 | if(!last){ 171 | return ngx_http_next_body_filter(r, in); 172 | } 173 | 174 | buf = ngx_calloc_buf(r->pool); 175 | if (buf == NULL) { 176 | return NGX_ERROR; 177 | } 178 | 179 | buf->pos = ctx->comment.data; 180 | buf->last = buf->pos + ctx->comment.len; 181 | buf->start = buf->pos; 182 | buf->end = buf->last; 183 | buf->last_buf = 1; 184 | buf->memory = 1; 185 | 186 | if (ngx_buf_size(cl->buf) == 0) { 187 | cl->buf = buf; 188 | } else { 189 | nl = ngx_alloc_chain_link(r->pool); 190 | if (nl == NULL) { 191 | return NGX_ERROR; 192 | } 193 | nl->buf = buf; 194 | nl->next = NULL; 195 | cl->next = nl; 196 | cl->buf->last_buf = 0; 197 | } 198 | 199 | return ngx_http_next_body_filter(r, in); 200 | } 201 | 202 | 203 | static void * 204 | ngx_http_length_hiding_create_conf(ngx_conf_t *cf) 205 | { 206 | ngx_http_length_hiding_conf_t *conf; 207 | 208 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_length_hiding_conf_t)); 209 | if (conf == NULL) { 210 | return NGX_CONF_ERROR; 211 | } 212 | 213 | conf->enable = NGX_CONF_UNSET; 214 | conf->max = NGX_CONF_UNSET; 215 | 216 | return conf; 217 | } 218 | 219 | 220 | static char * 221 | ngx_http_length_hiding_merge_conf(ngx_conf_t *cf, void *parent, void *child){ 222 | ngx_http_length_hiding_conf_t *prev = parent; 223 | ngx_http_length_hiding_conf_t *conf = child; 224 | 225 | if(ngx_http_merge_types(cf, &conf->types_keys, &conf->types, 226 | &prev->types_keys,&prev->types, 227 | ngx_http_html_default_types) != NGX_OK){ 228 | return NGX_CONF_ERROR; 229 | } 230 | 231 | ngx_conf_merge_value(conf->enable, prev->enable, 0); 232 | ngx_conf_merge_value(conf->max, prev->max, 2048); 233 | 234 | return NGX_CONF_OK; 235 | } 236 | 237 | 238 | static ngx_int_t 239 | ngx_http_length_hiding_filter_init(ngx_conf_t *cf) 240 | { 241 | ngx_http_next_header_filter = ngx_http_top_header_filter; 242 | ngx_http_top_header_filter = ngx_http_length_hiding_header_filter; 243 | 244 | ngx_http_next_body_filter = ngx_http_top_body_filter; 245 | ngx_http_top_body_filter = ngx_http_length_hiding_body_filter; 246 | 247 | return NGX_OK; 248 | } 249 | 250 | 251 | static ngx_int_t 252 | ngx_http_length_hiding_generate_random(ngx_http_request_t *r, 253 | ngx_http_length_hiding_ctx_t *ctx, ngx_http_length_hiding_conf_t *cf) 254 | { 255 | 256 | u_int len; 257 | u_char *s, *d; 258 | 259 | static u_char base[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 260 | static u_int base_len = sizeof(base) - 1; 261 | 262 | len = ngx_random() % cf->max + 1; 263 | 264 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 265 | "http length hiding filter : length %d", len); 266 | 267 | s = d = ngx_palloc(r->pool, len + 37); 268 | if( s == NULL ){ 269 | return NGX_ERROR; 270 | } 271 | 272 | s = ngx_copy(s,"", 4); 278 | 279 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 280 | "http length hiding filter : str %s", d); 281 | ctx->comment.data = d; 282 | ctx->comment.len = s - d; 283 | 284 | return NGX_OK; 285 | } 286 | --------------------------------------------------------------------------------