From 99b270d4b85f0b38a287eaaccc5a565ccca21ce1 Mon Sep 17 00:00:00 2001 From: Danila Vershinin <250071+dvershinin@users.noreply.github.com> Date: Sat, 29 Feb 2020 03:17:45 +0300 Subject: [PATCH] Optimization: e.g. stop sending X-Frame-Options for CSS --- README.md | 22 ++++++++++---- src/ngx_http_security_headers_module.c | 40 ++++++++++++++++++++++---- t/headers.t | 21 +++++++++++++- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1f5b030..9859892 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ X-Content-Type-Options: nosniff <----------- ## Key Features * Plug-n-Play: the default set of security headers can be enabled with `security_headers on;` in your NGINX configuration -* Sends `X-Content-Type-Options` only for appropriate MIME types, preserving unnecessary bits from being transferred for non-JS and non-CSS resources +* Sends `X-Content-Type-Options` only for relevant MIME types (CSS/JS), preserving unnecessary headers from being sent for HTML documents +* Similiarly, sends HTML-only relevant headers for relevant types and skips sending for others e.g. `X-Frame-Options` is useless for CSS * Plays well with conditional `GET` requests: the security headers are not included there unnecessarily * Does not suffer the `add_header` directive's pitfalls * Hides `X-Powered-By`, which often leaks PHP version information @@ -74,8 +75,18 @@ Enables hiding headers which leak software information: * `X-Page-Speed` * `X-Varnish` -Next are the common security headers being set. It's worth noting that special value of `omit` for directives below -will disable sending a particular header by the module (useful if you want to let your backend app to send it). +It's worth noting that some of those headers bear functional use, e.g. [`X-Page-Speed` docs](https://www.modpagespeed.com/doc/configuration#XHeaderValue) mention: + +> ... it is used to prevent infinite loops and unnecessary rewrites when PageSpeed +> fetches resources from an origin that also uses PageSpeed + +So it's best to specify `hide_server_tokens on;` in a front-facing NGINX insances, e.g. +the one being accessed by actual browsers, and not the ones consumed by Varnish or other software. + +In most cases you will be just fine with `security_headers on;` and `hide_server_tokens on;`, without any adjustments. + +For fine-tuning, use the header-specific directives below. +A special value `omit` disables sending a particular header by the module (useful if you want to let your backend app to send it). ### `security_headers_xss` @@ -141,6 +152,7 @@ To compile the module into NGINX, run: make make install -Or you can compile it as dynamic module. In that case, use `--add-dynamic-module` instead, and load the module after compilation via: +Or you can compile it as dynamic module. In that case, use `--add-dynamic-module` instead, and load the module after +compilation by adding to `nginx.conf`: - load_module modules/ngx_http_security_headers_module.so; + load_module /path/to/ngx_http_security_headers_module.so; diff --git a/src/ngx_http_security_headers_module.c b/src/ngx_http_security_headers_module.c index 2f8ed88..2e8bdc0 100644 --- a/src/ngx_http_security_headers_module.c +++ b/src/ngx_http_security_headers_module.c @@ -37,7 +37,10 @@ typedef struct { ngx_uint_t rp; ngx_hash_t nosniff_types; - ngx_array_t *types_keys; + ngx_array_t *nosniff_types_keys; + + ngx_hash_t text_types; + ngx_array_t *text_types_keys; } ngx_http_security_headers_loc_conf_t; @@ -99,6 +102,14 @@ ngx_str_t ngx_http_security_headers_default_nosniff_types[] = { ngx_null_string }; +ngx_str_t ngx_http_security_headers_default_text_types[] = { + ngx_string("text/html"), + ngx_string("application/xhtml+xml"), + ngx_string("text/xml"), + ngx_string("text/plain"), + ngx_null_string +}; + static ngx_command_t ngx_http_security_headers_commands[] = { { ngx_string( "security_headers" ), @@ -119,7 +130,7 @@ static ngx_command_t ngx_http_security_headers_commands[] = { NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, ngx_http_types_slot, NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_http_security_headers_loc_conf_t, types_keys), + offsetof(ngx_http_security_headers_loc_conf_t, nosniff_types_keys), &ngx_http_security_headers_default_nosniff_types[0] }, { ngx_string("security_headers_xss"), @@ -143,6 +154,13 @@ static ngx_command_t ngx_http_security_headers_commands[] = { offsetof(ngx_http_security_headers_loc_conf_t, rp), ngx_http_referrer_policy }, + { ngx_string("security_headers_text_types"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, + ngx_http_types_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_security_headers_loc_conf_t, text_types_keys), + &ngx_http_security_headers_default_text_types[0] }, + ngx_null_command }; @@ -242,7 +260,8 @@ ngx_http_security_headers_filter(ngx_http_request_t *r) /* Add X-XSS-Protection */ if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED - && NGX_HTTP_SECURITY_HEADER_OMIT != slcf->xss) + && NGX_HTTP_SECURITY_HEADER_OMIT != slcf->xss + && ngx_http_test_content_type(r, &slcf->text_types) != NULL) { ngx_str_set(&key, "X-XSS-Protection"); if (NGX_HTTP_XSS_HEADER_ON == slcf->xss) { @@ -265,7 +284,8 @@ ngx_http_security_headers_filter(ngx_http_request_t *r) /* Add X-Frame-Options */ if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED - && NGX_HTTP_SECURITY_HEADER_OMIT != slcf->fo) + && NGX_HTTP_SECURITY_HEADER_OMIT != slcf->fo + && ngx_http_test_content_type(r, &slcf->text_types) != NULL) { ngx_str_set(&key, "X-Frame-Options"); if (NGX_HTTP_FO_HEADER_SAME == slcf->fo) { @@ -339,14 +359,22 @@ ngx_http_security_headers_merge_loc_conf(ngx_conf_t *cf, void *parent, ngx_conf_merge_value(conf->hide_server_tokens, prev->hide_server_tokens, 0 ); - if (ngx_http_merge_types(cf, &conf->types_keys, &conf->nosniff_types, - &prev->types_keys, &prev->nosniff_types, + if (ngx_http_merge_types(cf, &conf->nosniff_types_keys, &conf->nosniff_types, + &prev->nosniff_types_keys, &prev->nosniff_types, ngx_http_security_headers_default_nosniff_types) != NGX_OK) { return NGX_CONF_ERROR; } + if (ngx_http_merge_types(cf, &conf->text_types_keys, &conf->text_types, + &prev->text_types_keys, &prev->text_types, + ngx_http_security_headers_default_text_types) + != NGX_OK) + { + return NGX_CONF_ERROR; + } + ngx_conf_merge_uint_value(conf->xss, prev->xss, NGX_HTTP_XSS_HEADER_BLOCK); ngx_conf_merge_uint_value(conf->fo, prev->fo, diff --git a/t/headers.t b/t/headers.t index e3048a0..a225d2b 100644 --- a/t/headers.t +++ b/t/headers.t @@ -23,6 +23,7 @@ hello world === TEST 2: no nosniff for html --- config security_headers on; + charset utf-8; location = /hello { return 200 "hello world\n"; } @@ -31,6 +32,7 @@ hello world --- response_body hello world --- response_headers +content-type: text/plain; charset=utf-8 !x-content-type-options x-frame-options: SAMEORIGIN x-xss-protection: 1; mode=block @@ -140,4 +142,21 @@ hello world !x-content-type-options x-frame-options: SAMEORIGIN x-xss-protection: 1; mode=block -referrer-policy: origin \ No newline at end of file +referrer-policy: origin + +=== TEST 8: X-Frame-Options should not be sent for CSS (even when encoding specified) +--- config + security_headers on; + charset utf-8; + charset_types text/css; + location = /hello.css { + default_type "text/css"; + return 200 "hello world\n"; + } +--- request + GET /hello.css +--- response_body +hello world +--- response_headers +content-type: text/css; charset=utf-8 +!x-frame-options \ No newline at end of file