Skip to content

Commit 760c5bc

Browse files
committed
Add "unset" option for X-XSS-Protection header (now default)
The X-XSS-Protection header is deprecated and introduces XSS vulnerabilities in browsers that support it. The new "unset" option actively removes this header from responses, including any set by upstream servers. Changes: - Add "unset" value for security_headers_xss directive - Change default from "off" (sends "0") to "unset" (removes header) - "unset" bypasses content-type filtering since removal applies to all - "omit" still allows upstream headers through unchanged This is a breaking change for users who relied on the default "0" value being sent. Use explicit "security_headers_xss off;" to restore the previous behavior.
1 parent 62d2685 commit 760c5bc

3 files changed

Lines changed: 102 additions & 37 deletions

File tree

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ Accept-Ranges: bytes
2525
Connection: keep-alive
2626
<b>X-Frame-Options: SAMEORIGIN
2727
X-Content-Type-Options: nosniff
28-
X-XSS-Protection: 0
2928
Referrer-Policy: strict-origin-when-cross-origin
3029
Cross-Origin-Resource-Policy: same-site
3130
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload</b>
@@ -71,11 +70,12 @@ start NGINX with the module to avoid having your domain preloaded by Chrome.
7170
Enables or disables applying security headers. The default set includes:
7271

7372
* `X-Frame-Options: SAMEORIGIN`
74-
* `X-XSS-Protection: 0`
7573
* `Referrer-Policy: strict-origin-when-cross-origin`
7674
* `X-Content-Type-Options: nosniff`
7775
* `Cross-Origin-Resource-Policy: same-site`
7876

77+
The deprecated `X-XSS-Protection` header is actively removed by default.
78+
7979
The values of these headers (or their inclusion) can be controlled with other `security_headers_*` directives below.
8080

8181
### `hide_server_tokens`
@@ -106,16 +106,17 @@ A special value `omit` disables sending a particular header by the module (usefu
106106

107107
### `security_headers_xss`
108108

109-
- **syntax**: `security_headers_xss off | on | block | omit`
110-
- **default**: `off`
109+
- **syntax**: `security_headers_xss off | on | block | omit | unset`
110+
- **default**: `unset`
111111
- **context**: `http`, `server`, `location`
112112

113-
Controls `X-XSS-Protection` header.
114-
Special `omit` value will disable sending the header by the module.
115-
The `off` value is for disabling XSS protection: `X-XSS-Protection: 0`.
116-
This is the default because
117-
[modern browsers do not support it](https://github.com/GetPageSpeed/ngx_security_headers/issues/19) and where it is
118-
supported, it introduces vulnerabilities.
113+
Controls `X-XSS-Protection` header.
114+
115+
* `unset` (default): Actively removes the header from responses, including any set by upstream servers. This is the recommended setting because the header is deprecated and [introduces XSS vulnerabilities](https://github.com/nicosalm/security-lab-xss-filter) in browsers that support it.
116+
* `omit`: Does not add or remove the header; allows upstream headers through unchanged.
117+
* `off`: Sends `X-XSS-Protection: 0` to explicitly disable browser XSS filtering.
118+
* `on`: Sends `X-XSS-Protection: 1`.
119+
* `block`: Sends `X-XSS-Protection: 1; mode=block`.
119120

120121
### `security_headers_frame`
121122

src/ngx_http_security_headers_module.c

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#define NGX_HTTP_XSS_HEADER_OFF 1
1414
#define NGX_HTTP_XSS_HEADER_ON 2
1515
#define NGX_HTTP_XSS_HEADER_BLOCK 3
16+
#define NGX_HTTP_XSS_HEADER_UNSET 4 /* actively remove header */
1617

1718
#define NGX_HTTP_FO_HEADER_SAME 1
1819
#define NGX_HTTP_FO_HEADER_DENY 2
@@ -93,6 +94,7 @@ static ngx_conf_enum_t ngx_http_xss_protection[] = {
9394
{ ngx_string("on"), NGX_HTTP_XSS_HEADER_ON },
9495
{ ngx_string("block"), NGX_HTTP_XSS_HEADER_BLOCK },
9596
{ ngx_string("omit"), NGX_HTTP_SECURITY_HEADER_OMIT },
97+
{ ngx_string("unset"), NGX_HTTP_XSS_HEADER_UNSET },
9698
{ ngx_null_string, 0 }
9799
};
98100

@@ -333,30 +335,34 @@ ngx_http_security_headers_filter(ngx_http_request_t *r)
333335
ngx_set_headers_out_by_search(r, &key, &val);
334336
}
335337

336-
/* Add X-XSS-Protection */
338+
/* Handle X-XSS-Protection (deprecated header) */
337339
if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED
338-
&& NGX_HTTP_SECURITY_HEADER_OMIT != slcf->xss
339-
&& ngx_http_test_content_type(r, &slcf->text_types) != NULL)
340+
&& NGX_HTTP_SECURITY_HEADER_OMIT != slcf->xss)
340341
{
342+
ngx_str_set(&key, "X-XSS-Protection");
343+
344+
if (slcf->xss == NGX_HTTP_XSS_HEADER_UNSET) {
345+
/* Actively remove the deprecated X-XSS-Protection header */
346+
ngx_set_headers_out_by_search(r, &key, &empty_val);
347+
} else if (ngx_http_test_content_type(r, &slcf->text_types) != NULL) {
348+
switch (slcf->xss) {
349+
case NGX_HTTP_XSS_HEADER_ON:
350+
ngx_str_set(&val, "1");
351+
break;
352+
case NGX_HTTP_XSS_HEADER_BLOCK:
353+
ngx_str_set(&val, "1; mode=block");
354+
break;
355+
case NGX_HTTP_XSS_HEADER_OFF:
356+
ngx_str_set(&val, "0");
357+
break;
358+
default:
359+
val.len = 0;
360+
val.data = NULL;
361+
}
341362

342-
switch (slcf->xss) {
343-
case NGX_HTTP_XSS_HEADER_ON:
344-
ngx_str_set(&val, "1");
345-
break;
346-
case NGX_HTTP_XSS_HEADER_BLOCK:
347-
ngx_str_set(&val, "1; mode=block");
348-
break;
349-
case NGX_HTTP_XSS_HEADER_OFF:
350-
ngx_str_set(&val, "0");
351-
break;
352-
default:
353-
val.len = 0;
354-
val.data = NULL;
355-
}
356-
357-
if (val.data) {
358-
ngx_str_set(&key, "X-XSS-Protection");
359-
ngx_set_headers_out_by_search(r, &key, &val);
363+
if (val.data) {
364+
ngx_set_headers_out_by_search(r, &key, &val);
365+
}
360366
}
361367
}
362368

@@ -556,7 +562,7 @@ ngx_http_security_headers_merge_loc_conf(ngx_conf_t *cf, void *parent,
556562
}
557563

558564
ngx_conf_merge_uint_value(conf->xss, prev->xss,
559-
NGX_HTTP_XSS_HEADER_OFF);
565+
NGX_HTTP_XSS_HEADER_UNSET);
560566
ngx_conf_merge_uint_value(conf->fo, prev->fo,
561567
NGX_HTTP_FO_HEADER_SAME);
562568
ngx_conf_merge_uint_value(conf->rp, prev->rp,

t/headers.t

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ hello world
2020
2121
2222
23-
=== TEST 2: no nosniff for html
23+
=== TEST 2: basic security headers (default xss is unset)
2424
--- config
2525
security_headers on;
2626
charset utf-8;
@@ -35,7 +35,7 @@ hello world
3535
content-type: text/plain; charset=utf-8
3636
x-content-type-options: nosniff
3737
x-frame-options: SAMEORIGIN
38-
x-xss-protection: 0
38+
!x-xss-protection
3939
4040
4141
@@ -118,7 +118,7 @@ hello world
118118
--- response_headers
119119
x-content-type-options: nosniff
120120
x-frame-options: SAMEORIGIN
121-
x-xss-protection: 0
121+
!x-xss-protection
122122
referrer-policy: unsafe-url
123123
124124
@@ -143,7 +143,7 @@ hello world
143143
--- response_headers
144144
x-content-type-options: nosniff
145145
x-frame-options: SAMEORIGIN
146-
x-xss-protection: 0
146+
!x-xss-protection
147147
referrer-policy: origin
148148
149149
@@ -433,4 +433,62 @@ hello world
433433
--- response_headers
434434
Cross-Origin-Resource-Policy: same-origin
435435
Cross-Origin-Opener-Policy: same-origin
436-
Cross-Origin-Embedder-Policy: require-corp
436+
Cross-Origin-Embedder-Policy: require-corp
437+
438+
439+
440+
=== TEST 25: XSS unset removes header from upstream
441+
--- config
442+
location = /hello {
443+
add_header X-XSS-Protection "1; mode=block";
444+
return 200 "hello world\n";
445+
}
446+
location = /hello-proxied {
447+
security_headers on;
448+
security_headers_xss unset;
449+
proxy_buffering off;
450+
proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello;
451+
}
452+
--- request
453+
GET /hello-proxied
454+
--- response_body
455+
hello world
456+
--- response_headers
457+
!x-xss-protection
458+
459+
460+
461+
=== TEST 26: XSS omit allows upstream header through
462+
--- config
463+
location = /hello {
464+
add_header X-XSS-Protection "1; mode=block";
465+
return 200 "hello world\n";
466+
}
467+
location = /hello-proxied {
468+
security_headers on;
469+
security_headers_xss omit;
470+
proxy_buffering off;
471+
proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello;
472+
}
473+
--- request
474+
GET /hello-proxied
475+
--- response_body
476+
hello world
477+
--- response_headers
478+
x-xss-protection: 1; mode=block
479+
480+
481+
482+
=== TEST 27: XSS off still sends header value 0
483+
--- config
484+
security_headers on;
485+
security_headers_xss off;
486+
location = /hello {
487+
return 200 "hello world\n";
488+
}
489+
--- request
490+
GET /hello
491+
--- response_body
492+
hello world
493+
--- response_headers
494+
x-xss-protection: 0

0 commit comments

Comments
 (0)