Nginx 隐藏敏感响应头信息

很多时候,安全问题不是因为我们的漏洞多或者低级,纯粹是因为我们暴露了太多信息给攻击者,攻击者得到这些信息后就会进行针对性的攻击。

如果我们隐藏掉这些信息,虽然攻击者通过暴力穷举的方式也能获取他想要的信息,但是相对应的,他的试错时间成本也会相应提高。就像老话说的:“苍蝇不叮无缝蛋”,如果苍蝇可以通过不停地叮同一个地方,迟早能叮破鸡蛋,但是实际情况是,在苍蝇叮破鸡蛋之前,苍蝇自己就要饿死了。这里体现的就是一个时间成本问题。

1. 问题起因

起因是客户的一次漏扫,扫出了"x-powered-by"这个响应头信息。这个响应头一般用来表示提供 web 服务的程序,比如 php / express 之类的,平时在开发环境中,我们不关心这个,但是在对外提供服务的情况下,这个信息就十分敏感,就像前面提到的,攻击者会利用这个信息展开有针对性的攻击。

针对这次事件,我们重新审查了所有 api 的响应头,发现除了 x-powered-by 之外,还出现了 server 这个暴露了 nginx 版本的头信息。

2. 默认情况

默认情况下,我们可以使用 curl 工具来获取某个 api 返回的响应头信息:

$ curl -I -k https://10.33.108.9/api/f-user/v1/users/current
HTTP/1.1 200 OK
Server: nginx/1.16.1
Date: Wed, 04 Jan 2023 05:37:45 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 352
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
ETag: W/"160-hoGVrHn8wBoEyijJ/nSYPd0SeY8"
Vary: Accept-Encoding
Set-Cookie: connect.sid=s%3AxfA0UMpQaRhLfOC5Usfx6LZie16eKUtD.GBOEX5eVNR3GRHj%2FVWprtyBrmSSGGITBLLeBX1yPijA; Path=/; Expires=Wed, 04 Jan 2023 05:40:45 GMT; HttpOnly
Content-Security-Policy: worker-src 'self' data: blob:; object-src 'self'; media-src 'self' data: blob:; font-src 'self' data:; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff

从返回响应头中,我们可以轻松的知道提供 Web 服务的是 Openresty,提供 api 服务的是 Express。

3. 隐藏 Nginx 信息

最简单的做法,根据官方文档的描述:

Syntax: server_tokens on | off | build | string;
Default: server_tokens on;
Context: http, server, location

Enables or disables emitting nginx version on error pages and in the “Server” response header field.

The build parameter (1.11.10) enables emitting a build name along with nginx version.

Additionally, as part of our commercial subscription, starting from version 1.9.13 the signature on error pages and the “Server” response header field value can be set explicitly using the string with variables. An empty string disables the emission of the “Server” field.

通过添加 server_tokens 指令,可以将 Nginx 信息进行隐藏。同时官方也提供了几种值可供选择。

  • off: 关闭版本号输出,但仍会返回 Server: nginx 信息。
  • build: 使用编译时提供的版本信息,需要在编译时提供对应的变量。
  • string: 使用字符串替换默认值,如果字符串为空,则关闭 Server 字段输出,此特性只适用商业版。

原以为只需要设置空字符串就能隐藏 Nginx 信息,结果这个特性只能在商业版上使用,没办法,只能通过设置 off 关闭版本号了。

$ curl -I -k https://10.33.108.9/api/f-user/v1/users/current
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 04 Jan 2023 05:37:45 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 352
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
ETag: W/"160-hoGVrHn8wBoEyijJ/nSYPd0SeY8"
Vary: Accept-Encoding
Set-Cookie: connect.sid=s%3AxfA0UMpQaRhLfOC5Usfx6LZie16eKUtD.GBOEX5eVNR3GRHj%2FVWprtyBrmSSGGITBLLeBX1yPijA; Path=/; Expires=Wed, 04 Jan 2023 05:40:45 GMT; HttpOnly
Content-Security-Policy: worker-src 'self' data: blob:; object-src 'self'; media-src 'self' data: blob:; font-src 'self' data:; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff

可以看到,只是隐藏版本信息,我们仍然可以知道是 Nginx 提供的服务。

4. 完全隐藏 Nginx 信息

官方文档上除了字符串,还有一个 build 选项,说明社区版如果想完全隐藏信息,只能通过自编译来实现了:

4.1 修改 src/core/nginx.h

文件内可以看到下面几个宏定义,对 NGINX_VERSION 和 NGINX_VER 进行任意的修改,比如空字符串或者 Google 等混淆字符串。

#define nginx_version      1016001
#define NGINX_VERSION      "1.16.1"
#define NGINX_VER          "nginx/" NGINX_VERSION

4.2 修改 src/http/ngx_http_header_filter_module.c

文件可以看到下面这一行,其中的 “Server: nginx” 就是我们通过 server_tokens 指令无法去除的信息,你可以将其改为空字符串、混淆字符串,或者直接删除。

static u_char ngx_http_server_string[] = "Server: nginx" CRLF;

4.3 修改 src/http/ngx_http_special_response.c

这个文件里面可以修改很多东西,先改最明显的,将 ngx_http_error_tail 中的 nginx 信息删掉。

static u_char ngx_http_error_tail[] = "<hr><center>nginx</center>" CRLF "</body>" CRLF "</html>" CRLF ;

同理还可以将其他错误码信息进行修改,比如把 404 Not Found 改为 404。

static char ngx_http_error_404_page[] = "<html>" CRLF "<head><title>404 Not Found</title></head>" CRLF "<body>" CRLF "<center><h1>404 Not Found</h1></center>" CRLF ;

这样修改完,就基本隐藏的差不多了,重新进行编译和更新,再次通过 curl 进行验证,将会看到你自定义的内容了。

$ curl -I -k https://10.33.108.9/api/f-user/v1/users/current
HTTP/1.1 200 OK
Server: google
Date: Wed, 04 Jan 2023 05:37:45 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 352
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
ETag: W/"160-hoGVrHn8wBoEyijJ/nSYPd0SeY8"
Vary: Accept-Encoding
Set-Cookie: connect.sid=s%3AxfA0UMpQaRhLfOC5Usfx6LZie16eKUtD.GBOEX5eVNR3GRHj%2FVWprtyBrmSSGGITBLLeBX1yPijA; Path=/; Expires=Wed, 04 Jan 2023 05:40:45 GMT; HttpOnly
Content-Security-Policy: worker-src 'self' data: blob:; object-src 'self'; media-src 'self' data: blob:; font-src 'self' data:; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff

可以看到 Server 字段的值已经变成了你自定义的字符串了,可以说达到隐藏/混淆的目的了。

5. 过滤应用特征响应头

上面的操作虽然隐藏了 Nginx 本身的信息,但是我们可以注意到,仍然有一个“X-Powered-By”字段暴露了真正提供服务的程序信息,这该怎么隐藏呢?

一般来说这个字段都是应用程序自己添加的,应用程序自身也提供了设置来关闭这个字段输出,但是我们有很多、不同语言的应用,每个都去改一遍代价太大,有没有更加简单的方式?

这就涉及到了 Nginx 中一个指令:proxy_hide_header。

Syntax:proxy_hide_header field;
Default:—Context:http, server, location

By default, nginx does not pass the header fields “Date”, “Server”, “X-Pad”, and “X-Accel-...” from the response of a proxied server to a client. The proxy_hide_header directive sets additional fields that will not be passed. If, on the contrary, the passing of fields needs to be permitted, the proxy_pass_header directive can be used.

通过这个指令,我们就可以隐藏应用程序返回的一些包含敏感信息的响应头了。