Skip to content

Commit a05de4a

Browse files
committed
Overwrite existing headers
1 parent d275bc2 commit a05de4a

3 files changed

Lines changed: 123 additions & 43 deletions

File tree

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Running `curl -IL http://example.com/` will yield additional headers:
1515

1616
```
1717
HTTP/1.1 200 OK
18-
<Server: header removed>
18+
Server: nginx
1919
Date: Tue, 21 May 2019 16:15:46 GMT
2020
Content-Type: text/html; charset=UTF-8
2121
Vary: Accept-Encoding
@@ -29,7 +29,7 @@ Running `curl -IL http://example.com/some.css` (or `some.js`) will yield additio
2929

3030
```
3131
HTTP/1.1 200 OK
32-
<Server: header removed>
32+
Server: nginx
3333
Date: Tue, 21 May 2019 16:15:46 GMT
3434
Content-Type: text/css; charset=UTF-8
3535
Vary: Accept-Encoding
@@ -46,8 +46,8 @@ X-Content-Type-Options: nosniff <-----------
4646
* Plug-n-Play: the default set of security headers can be enabled with `security_headers on;` in your NGINX configuration
4747
* Sends `X-Content-Type-Options` only for appropriate MIME types, preserving unnecessary bits from being transferred for non-JS and non-CSS resources
4848
* Plays well with conditional `GET` requests: the security headers are not included there unnecessarily
49-
* Hides `X-Powered-By`, which often leaks PHP version information
5049
* Does not suffer the `add_header` directive's pitfalls
50+
* Hides `X-Powered-By`, which often leaks PHP version information
5151
* Hides `Server` header altogether, not just the version information
5252

5353
## Configuration directives
@@ -64,10 +64,18 @@ Enables or disables applying security headers. The default set includes:
6464
* `X-XSS-Protection: 1; mode=block`
6565
* `X-Content-Type-Options: nosniff` (for CSS and Javascript)
6666

67-
Headers which are hidden:
67+
The values of these headers (or their inclusion) can be controlled with other `security_headers_*` directives below.
68+
69+
### `hide_server_tokens`
70+
71+
- **syntax**: `hide_server_tokens on | off`
72+
- **default**: `off`
73+
- **context**: `http`, `server`, `location`
74+
75+
Enables hiding headers which leak software information:
6876

69-
* `X-Powered-By`
7077
* `Server`
78+
* `X-Powered-By`
7179

7280
### `security_headers_xss`
7381

src/ngx_http_security_headers_module.c

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
typedef struct {
2121
ngx_flag_t enable;
22+
ngx_flag_t hide_server_tokens;
2223

2324
ngx_uint_t xss;
2425
ngx_uint_t fo;
@@ -67,6 +68,13 @@ static ngx_command_t ngx_http_security_headers_commands[] = {
6768
offsetof( ngx_http_security_headers_loc_conf_t, enable ),
6869
NULL },
6970

71+
{ ngx_string( "hide_server_tokens" ),
72+
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
73+
ngx_conf_set_flag_slot,
74+
NGX_HTTP_LOC_CONF_OFFSET,
75+
offsetof(ngx_http_security_headers_loc_conf_t, hide_server_tokens ),
76+
NULL },
77+
7078
{ ngx_string("security_headers_nosniff_types"),
7179
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
7280
ngx_http_types_slot,
@@ -134,12 +142,31 @@ ngx_http_security_headers_filter(ngx_http_request_t *r)
134142
{
135143
ngx_http_security_headers_loc_conf_t *slcf;
136144

145+
ngx_table_elt_t *h_server;
146+
137147
ngx_str_t key;
138148
ngx_str_t val;
139149

140-
141150
slcf = ngx_http_get_module_loc_conf(r, ngx_http_security_headers_module);
142151

152+
if (1 == slcf->hide_server_tokens) {
153+
/* Hide the Server header */
154+
h_server = r->headers_out.server;
155+
if (h_server == NULL) {
156+
h_server = ngx_list_push(&r->headers_out.headers);
157+
if (h_server == NULL) {
158+
return NGX_ERROR;
159+
}
160+
r->headers_out.server = h_server;
161+
}
162+
h_server->hash = 0;
163+
164+
/* Hide X-Powered-By header */
165+
ngx_str_set(&key, "x-powered-by");
166+
ngx_str_set(&val, "");
167+
ngx_set_headers_out_by_search(r, &key, &val);
168+
}
169+
143170
if (1 != slcf->enable) {
144171
return ngx_http_next_header_filter(r);
145172
}
@@ -169,6 +196,14 @@ ngx_http_security_headers_filter(ngx_http_request_t *r)
169196
ngx_set_headers_out_by_search(r, &key, &val);
170197
}
171198

199+
#if (NGX_HTTP_SSL)
200+
if (r->connection->ssl) {
201+
ngx_str_set(&key, "Strict-Transport-Security");
202+
ngx_str_set(&val, "max-age=63072000; includeSubDomains; preload");
203+
ngx_set_headers_out_by_search(r, &key, &val);
204+
}
205+
#endif
206+
172207
/* Add X-Frame-Options */
173208
if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED
174209
&& NGX_HTTP_SECURITY_HEADER_OMIT != slcf->fo)
@@ -182,19 +217,16 @@ ngx_http_security_headers_filter(ngx_http_request_t *r)
182217
ngx_set_headers_out_by_search(r, &key, &val);
183218
}
184219

185-
/* Find X-Powered-By header */
186-
ngx_str_set(&key, "x-powered-by");
187-
ngx_str_set(&val, "");
188-
ngx_set_headers_out_by_search(r, &key, &val);
220+
/* Referrer-Policy: no-referrer-when-downgrade */
221+
if (r->headers_out.status != NGX_HTTP_NOT_MODIFIED) {
222+
ngx_str_set(&key, "Referrer-Policy");
223+
ngx_str_set(&val, "no-referrer-when-downgrade");
224+
ngx_set_headers_out_by_search(r, &key, &val);
225+
}
189226

190227

191-
/* Deal with Server header */
192-
ngx_str_set(&key, "server");
193-
ngx_str_set(&val, "");
194-
ngx_set_headers_out_by_search(r, &key, &val);
195228

196229
/* proceed to the next handler in chain */
197-
198230
return ngx_http_next_header_filter(r);
199231
}
200232

@@ -212,6 +244,7 @@ ngx_http_security_headers_create_loc_conf(ngx_conf_t *cf)
212244
conf->xss = NGX_CONF_UNSET_UINT;
213245
conf->fo = NGX_CONF_UNSET_UINT;
214246
conf->enable = NGX_CONF_UNSET;
247+
conf->hide_server_tokens = NGX_CONF_UNSET_UINT;
215248

216249
return conf;
217250
}
@@ -225,6 +258,8 @@ ngx_http_security_headers_merge_loc_conf(ngx_conf_t *cf, void *parent,
225258
ngx_http_security_headers_loc_conf_t *conf = child;
226259

227260
ngx_conf_merge_value( conf->enable, prev->enable, 0 );
261+
ngx_conf_merge_value(conf->hide_server_tokens,
262+
prev->hide_server_tokens, 0 );
228263

229264
if (ngx_http_merge_types(cf, &conf->types_keys, &conf->nosniff_types,
230265
&prev->types_keys, &prev->nosniff_types,
@@ -259,63 +294,74 @@ ngx_set_headers_out_by_search(ngx_http_request_t *r,
259294
ngx_str_t *key, ngx_str_t *value)
260295
{
261296
ngx_list_part_t *part;
262-
ngx_table_elt_t *hi;
263297
ngx_uint_t i;
264-
ngx_table_elt_t *h = NULL;
298+
ngx_table_elt_t *h;
299+
ngx_flag_t matched = 0;
265300

266-
/*
267-
Get the first part of the list. There is usual only one part.
268-
*/
269301
part = &r->headers_out.headers.part;
270-
hi = part->elts;
302+
h = part->elts;
303+
304+
for (i = 0; /* void */; i++) {
271305

272-
/*
273-
Headers list array may consist of more than one part,
274-
so loop through all of it
275-
*/
276-
for (i = 0; /* void */ ; i++) {
277306
if (i >= part->nelts) {
278307
if (part->next == NULL) {
279-
/* The last part, search is done. */
280308
break;
281309
}
282310

283311
part = part->next;
284-
hi = part->elts;
312+
h = part->elts;
285313
i = 0;
286314
}
287315

288-
if (hi[i].hash == 0) {
316+
if (h[i].hash == 0) {
289317
continue;
290318
}
291319

292-
/*
293-
Just compare the lengths and then the names case insensitively.
294-
*/
295-
if (key->len != hi[i].key.len
296-
|| ngx_strcasecmp(key->data, hi[i].key.data) != 0)
320+
if (h[i].key.len == key->len
321+
&& ngx_strncasecmp(h[i].key.data, key->data,
322+
h[i].key.len) == 0)
297323
{
298-
/* This header doesn't match. */
299-
continue;
324+
goto matched;
300325
}
301326

302-
h = hi;
303-
break;
327+
/* not matched */
328+
continue;
329+
matched:
330+
331+
if (value->len == 0 || matched) {
332+
h[i].value.len = 0;
333+
h[i].hash = 0;
334+
} else {
335+
h[i].value = *value;
336+
h[i].hash = 1;
337+
}
338+
339+
matched = 1;
304340
}
305-
if (h == NULL) {
306-
h = ngx_list_push(&r->headers_out.headers);
341+
342+
if (matched){
343+
return NGX_OK;
307344
}
345+
346+
/* XXX we still need to create header slot even if the value
347+
* is empty because some builtin headers like Last-Modified
348+
* relies on this to get cleared */
349+
350+
h = ngx_list_push(&r->headers_out.headers);
308351
if (h == NULL) {
309352
return NGX_ERROR;
310353
}
311-
h->key = *key;
312-
h->value = *value;
313-
if (value->len == 0) {
354+
355+
if (value->len == 0) {
314356
h->hash = 0;
357+
315358
} else {
316359
h->hash = 1;
317360
}
318361

362+
h->key = *key;
363+
h->value = *value;
364+
319365
h->lowcase_key = ngx_pnalloc(r->pool, h->key.len);
320366
if (h->lowcase_key == NULL) {
321367
return NGX_ERROR;
@@ -325,3 +371,4 @@ ngx_set_headers_out_by_search(ngx_http_request_t *r,
325371

326372
return NGX_OK;
327373
}
374+

t/headers.t

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ __DATA__
77
=== TEST 1: server is hidden
88
--- config
99
security_headers on;
10+
hide_server_tokens on;
1011
location = /hello {
1112
return 200 "hello world\n";
1213
}
@@ -72,3 +73,27 @@ hello world
7273
!x-content-type-options
7374
x-frame-options: SAMEORIGIN
7475
!server
76+
77+
78+
79+
=== TEST 5: simple failure
80+
--- config
81+
hide_server_tokens on;
82+
location = /hello {
83+
security_headers on;
84+
85+
return 200 "hello world\n";
86+
}
87+
location = /hello-proxied {
88+
proxy_buffering off;
89+
proxy_pass http://127.0.0.1:$TEST_NGINX_SERVER_PORT/hello;
90+
}
91+
--- request
92+
GET /hello-proxied
93+
--- response_body
94+
hello world
95+
--- response_headers
96+
!x-content-type-options
97+
x-frame-options: SAMEORIGIN
98+
!Server
99+
Referrer-Policy: no-referrer-when-downgrade

0 commit comments

Comments
 (0)