├── .dockerignore ├── .travis.yml ├── Dockerfile.test ├── LICENSE ├── Makefile ├── README.md ├── config ├── ngx_http_length_hiding_filter_module.c └── t ├── 01-on-off.t ├── 02-response-status.t └── 03-content-type.t /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile* 2 | Makefile -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM perl:latest 2 | 3 | ENV NGINX_VERSION 1.14.0 4 | 5 | RUN cpanm Test::Harness 6 | RUN cpanm Test::Nginx 7 | 8 | COPY config /root/nginx-length-hiding-filter-module/ 9 | COPY ngx_http_length_hiding_filter_module.c /root/nginx-length-hiding-filter-module/ 10 | 11 | RUN curl -fSL http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz -o nginx.tar.gz \ 12 | && tar zxfv nginx.tar.gz \ 13 | && cd nginx-${NGINX_VERSION} \ 14 | && ./configure --add-module=/root/nginx-length-hiding-filter-module \ 15 | && make \ 16 | && make install \ 17 | && ln -sf /dev/stdout /usr/local/nginx/logs/access.log \ 18 | && ln -sf /dev/stderr /usr/local/nginx/logs/error.log 19 | 20 | COPY t/*.t /root/nginx-length-hiding-filter-module/t/ 21 | 22 | WORKDIR /root/nginx-length-hiding-filter-module 23 | 24 | ENV TEST_NGINX_BINARY /usr/local/nginx/sbin/nginx 25 | CMD prove -v --timer t/*.t -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Nulab Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Nginx Length Hiding Filter Module 3 | 4 | [![Build Status](https://travis-ci.org/nulab/nginx-length-hiding-filter-module.svg?branch=master)](https://travis-ci.org/nulab/nginx-length-hiding-filter-module) 5 | 6 | ## Introduction 7 | 8 | In [BREACH site](http://breachattack.com/), the mitigations against BREACH attack are given as follows: 9 | 10 | 1. Disabling HTTP compression 11 | 2. Separating secrets from user input 12 | 3. Randomizing secrets per request 13 | 4. Masking secrets (effectively randomizing by XORing with a random secret per request) 14 | 5. Protecting vulnerable pages with CSRF 15 | 6. Length hiding (by adding random number of bytes to the responses) 16 | 7. Rate-limiting the requests 17 | 18 | BREACH relies on HTTP compression and it's reasonable to disable it to secure your website. However without compresseion, some websites may meet severe performance degression or the cost may increase if you're charged based on the volume of traffic like AWS. In such case it may be difficult to turn off HTML compression for whole responses from your website and need to adopt other proper ways. 19 | 20 | Other mitigations listed from the 2nd to 5th above are basically applicable to your application but the 6th one, Length hiding, can be done on nginx. This filter module provides functionality to append randomly generated HTML comment to the end of response body to hide correct response length and make it difficult for attackers to guess secure token. 21 | 22 | The sample of randomly appended HTML comment is here. 23 | ``` 24 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /t/01-on-off.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::Nginx::Socket; 5 | 6 | # repeat_each(2); 7 | plan tests => repeat_each() * 3 * blocks(); 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: on 14 | --- config 15 | location /on { 16 | default_type text/html; 17 | return 200 'hello'; 18 | length_hiding on; 19 | } 20 | --- request 21 | GET /on 22 | --- response_headers 23 | Content-Type: text/html 24 | --- response_body_like: ^hello