一、前言

近期公司要求对门户网站域名URL信息进行安全整改限制,避免一些敏感信息泄露通过门户网站整体暴露出去,造成用户信息泄露。如某些域名后端的URL路由禁止外网访问,仅允许公司内网用户访问;此时,熟悉Nginx 用户的小伙伴首先第一时间就会想到通过deny\allow进行限制;那么大家有没有想过一个问题,该规则真的适用吗?接下来带大家验证逐一分析演示

二、问题需求

2.1、实际配置

下述是一个很简单的Nginx location规则,当用户访问路由匹配到/visit.html时,默认拒绝所有,仅允许公司内网访问;

location  /visit.html {
            add_header X-Frame-Options SAMEORIGIN;
            alias /var/www/html/ywq/am/index;
            index visit.html;
            allow x.x.x.x; #公司内网出口IP
            deny all;	#拒绝所有
    }

2.2、分析思路流程

用户访问反馈404页面,那么问题来了,为什么做了deny all\allow 策略只有直接返回404呢?正常情况如果被拒难道不是会返回403状态码吗?

记一篇企业级Nginx Location 安全策略限制问题研究分析_Nginx 安全策略

此时不要慌,先确认下匹配路由之后是否真实生效,于是乎打开浏览器控制台近一步检索问题。可以发现

我们模拟用户的请求是返回403的,说明我们的deny限制是真实有效的,只是浏览器页面没有给我们直接返回403拒绝页面而已,那么为什么浏览器没有给返回403呢?接下来我们继续放下看。控制台可以清晰的看到403下面还有其它GET请求信息, 毫无疑问,它就是造成我们用户看到页面404的罪魁祸首。那么为什么我们明明会这样呢?此时我们就需要到Nginx.conf配置文件里面去看了

GET https://www.daixiaorui.com/Public/images/head404.png net::ERR_CERT_DATE_INVALID
GET https://www.daixiaorui.com/Public/images/txtbg404.png net::ERR_CERT_DATE_INVALID

记一篇企业级Nginx Location 安全策略限制问题研究分析_Nginx 安全策略_02

来到Nginx配置这块,发现如下配置,这就能解释为什么我们访问url 403被拒绝了却跳转到其它不知名的域名404页面,下面我们对这些参数分析一波,下述配置在Nginx web服务器上设置错误页面处理和代理拦截;为了便于理解,我直接指定注释

proxy_intercept_errors on;  #Nginx在代理过程中拦截错误,从而能够显示自定义的错误页面
    error_page  404 403  /404.html;	#当发生404或者403错误时,都会统一展示/下的404.html页面
    error_page 404 /404.html;	# 与上行解析相同,实际上定义了重定向,会将404错误页面指向了/404.html
        location = /40x.html {	#这里定义了404页面实际的路径
    }

    error_page 500 502 503 504 /50x.html;	#同时这里含义与上述一致
        location = /50x.html {
    }
    location = /404.html {	#定义了404错误页面位置,并在返回的时候添加了X-Frame-Options头部信息以及指定页面的根目录位置/var/www/html/;
            add_header X-Frame-Options SAMEORIGIN always;
            root   /var/www/html;
    }

记一篇企业级Nginx Location 安全策略限制问题研究分析_Nginx 安全策略_03

经过上述一系列分析,我们知道配置上deny策略之后返回了404页面原因;实际上我们配置的限制策略是正确的,对于URL来说,确实是403拒绝了,无非是将403重定向404页面而已。接下来我们验证一下内网发起访问请求,并追踪具体日志;

结果令人感到意外,在内网同样无法正常访问,仍然是拒绝URL请求。下述是捕获的三段不同的日志信息;

内网访问日志输出
[11/Apr/2024:18:07:12 +0800] "-" 100.122.16.18 "219.142.137.159, 221.195.20.60" 403 1524 "-" "GET /visit.html HTTP/1.1" 80 "PostmanRuntime/7.37.3" 0.000 - 726 "-"  "-" "-" "-" "-" "-" "-"  "-" "-"access_log/var/log/nginx/accelogdebugss.logdebug
外网访问日志输出
[11/Apr/2024:18:08:42 +0800] "-" 100.122.16.47 "2409:8a00:54ce:ff20:1457:d753:b7ea:16d9, 221.195.20.60" 403 1524 "-" "GET /visit.html HTTP/1.1" 80 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" 0.000 - 1331 "-"  "-" "-" "-" "-" "-" "-"  "-" "-"access_log/var/log/nginx/access.
外网iphone访问
[11/Apr/2024:19:06:36 +0800] "-" 100.122.19.48 "2409:8900:5420:14b4:98e7:2a8d:9a01:325f, 221.195.20.60" 403 1524 "-" "GET /visit.html HTTP/1.1" 80 "Mozilla/5.0 (iPhone; CPU iPhone OS 15_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)  Mobile/15E148 wxwork/4.1.20 MicroMessenger/7.0.1 Language/zh ColorScheme/Dark" 0.000 - 946 "-"  "-" "-" "-" "-" "-" "-"  "-" "-"access_log/var/log/nginx/access.logdebug

为什么允许公司内网访问IP同样做403处理呢?难道是因为我限制的IP地址不对还是因为我限制方式有问题呢?

于是乎模拟在不同网络环境下访问,并对其输出的日志信息进行分析

[11/Apr/2024:18:07:12 +0800]:这是记录的时间戳,表明记录发生的时间为2024年4月11日18点07分12秒,时区为+0800。

"-":这个字段通常包含了用户身份验证信息,因为是"-",说明这里没有提供身份验证信息。

100.122.16.18:这是客户端的IP地址,表示发起请求的客户端IP地址。

"219.142.137.159, 221.195.20.60":这是表示请求经过的代理服务器IP地址,第一个IP表示最近一级代理,第二个IP表示更早之前的代理。这表明请求通过了两个代理服务器。

403:这是HTTP状态码,表示服务器拒绝了请求。

1524:这是响应的大小,以字节为单位。

"-" "GET /visit.html HTTP/1.1":这是发起的请求行,GET请求访问了/visit.html页面,使用的是HTTP/1.1协议。

80:这是指定的端口号。

"PostmanRuntime/7.37.3":这是用户代理(User-Agent)字符串,指明了发起请求的客户端应用程序。

0.000:这是最后一个参数,它表示处理请求所花费的时间,以秒为单位。

记一篇企业级Nginx Location 安全策略限制问题研究分析_Nginx 安全策略_04

通过上述日志分析可以看到,我们能允许的只有两组IP地址,如下

100.122.16.18
219.142.137.159 221.195.20.60

要想确定我们要允许什么字段的IP才能真正实现我们内网访问,前提就是我们要弄明白日志中携带的两组IP地址是什么意思

Ps: 上述我们截图我们是为了便于理解,通过阿里云日志系统中做了过Nginx日志配置,通过log_format做了提取;这样可以在平台上更直观的看到日志输出的情况,如下所示

log_format  debug '[$time_local] "$upstream_addr" $remote_addr "$http_x_forwarded_for" '
                      '$status $body_bytes_sent "$http_referer" "$request" $server_port '
                      '"$http_user_agent" $request_time $upstream_response_time $request_length "$request_body"  "$http_version" "$http_deviceid" "$http_clientId" "$http_userId" "$http_phoneBrand" "$http_phoneVersion"  "$http_phoneModel" "$http_deviceType"'
  • http_x_forwarded_for: 219.142.137.159,221.195.20.60

该字段是用来记录经过代理服务器的IP地址,由于http请求可能会经过多个代理服务器,这个字段可能会包含多个IP,通过逗号分隔,在一些设置合理的代理服务器中,代理服务器可以将请求的原始IP地址添加到请求头的“x_forwarded_for”字段中,以便服务器能够获取到最初发起的请求客户端的真实IP地址

  • remote_addr: 100.122.18.76

该字段是用来记录发起请求的客户端的IP地址,主要记录实际发起请求的客户端IP地址,该地址在Tcp层传递过来的客户端真实地址,相对可靠。

当你允许对应的IP地址访问时,如果你希望允许最初发起请求的客户端的真实IP地址,那么应该允许“remote_addr”对应的IP,如果你希望允许代理层传递过来的IP地址,那么你就允许"http_x_forwarded_for"对应的IP地址,在实际应用中,为了确保安全性,通常会结合使用这两个字段来获取客户端的真实IP地址,在验证"http_x_forwareded_for"中的IP地址是否在你信任的代理服务器IP列表中,并确保"remote_addr"中的IP地址也符合个人预期。

三、解决方案

我们说要允许在某一局域网的客户端的IP,我们一般怎么限制呢?我们不可能把某一网络环境下所有的客户端IP地址都一一对应写到白名单列表吧?这不现实,如果对局域网限制一般会限制该网络的出口IP,那么出口IP怎么查询呢,很简单,直接百度搜IP即可查询到当前所在网络的IP地址,如下图所示:

记一篇企业级Nginx Location 安全策略限制问题研究分析_Nginx 安全策略_05

 location  /visit.html {	#指定了对访问/visit.html页面的处理规则。
        if ( $http_x_forwarded_for !~ "(x.x.x.x|x.x.x.x)" ) 使用条件判断,如果"http_x_forwarded_for"中的IP地址不匹配给定的正则表达式就会执行接下来的操作
          {
       return 403;	#如果条件判断成立,即请求的"$http_x_forwarded_for"中的IP不在指定的IP列表中,那么返回状态码403,拒绝访问
        }
         add_header X-Frame-Options SAMEORIGIN;	#响应头中增加"X-Frame-Options"字段,设置为"SAMEORIGIN",这是为了防止页面钱遇到其它网站中
         alias /var/www/html/ywq/am/index;	#指定了请求文件被映射到具体的路径
         index visit.html;	#指定了默认索引文件
        }

四、总结

在面对上述需求,我们只需限制局域网的出口IP即可,而该局域网出口IP一般都是固定的,在生产代理配置中该IP会经过代理层,既然如此,http_x_forwarded_for字段必然会将出口IP进行绑定,因此我们只需要对其http_x_forwarded_for对应的IP做放心策略即可,其它默认拒绝,相对一开始提到的allow x.x.x.x/dent allow这样的策略更具灵活性。