引言
在 NGINX 中常用一种 “比较变量” 的手法,在编程语言中称为 “多路分支” (Case statement),也就是 nginx map,需要注意的一点是,太低版本 NGINX MAP 中只能使用单变量
Before version 0.9.0 only a single variable could be specified in the first parameter. [1]
下面将了解下 nginx map 的具体使用方式
nginx map使用
Nginx 配置主要是声明性的,这同样应用于 MAP 指令,NGINX MAP 是定义在 http{}
级别,最大的特点是仅在引用时进行处理, 如果请求未触及使用 NGINX MAP 变量的配置部分,则不会执行该 map 变量查找。换句话来理解,当在上下文 server, Location, if 等中使用结果变量时(指定的不是计算结果,而是在需要时计算该结果的公式),才会被使用,在 NGINX 需要使用该变量之前,NGINX MAP 不会给请求增加任何开销。
NGINX MAP 用于根据另一个变量的值创建一个变量,如下所示:
map $variable_to_check $variable_to_set {
"check_if_variable_matches_me" "variable_matches_checked_value";
default "no_match";
}
在上面的例子中, 变量 $variable_to_set 的被设置的结果为:如果 $variable_to_check 值为 “check_if_variable_matches_me”, 那么 $variable_to_set 将被设置为值 “variable_matches_checked_value” , 否则将设置为 “no_match”。
上面的就是一个编程语言的分支语句,例如将上面语句转换为 bash shell,那么意思为
|
|
当然作为分支语句,是支持多路分支的,他的写法如下:
map $thing $useful_variable {
"thing_matches_me" "thing_matched_1";
"nope_thing_matches_me" "thing_matched_2";
default "no_match";
}
正则表达式在map中特性
正则表达式 (regular expression) 是一种用于匹配源变量中复杂字符串模式的有用方法,但它会增加解析表达式的开销。默认情况下,NGINX MAP 指令在每个请求处理过程中只执行一次查找,即正则表达式的开销被限制为一次查找。但启用 “volatile” 参数会关闭变量缓存,这意味着每次使用 NGINX MAP 时都需要执行一次完整查找,从而增加了请求的额外开销,尤其是在高负载情况下,特别是当使用正则表达式时,可能会导致性能下降。
另外 “volatile” 参数的副作用是关闭了依赖于 “volatile” MAP 变量的缓存。如果需要复杂的正则表达式,那么在 MAP 中不要使用 “volatile” 参数,如果关联的 MAP 变量或任何依赖于该 MAP 的变量在多个地方被引用。这种情况在流量增加并导致查找操作增多时才会变得明显,此时CPU利用率会升高;最重要的是要注意,CPU利用率上升的原因可能不明显,因此需要谨慎使用 “volatile” 参数。
volatile 在计算机中的术语是 “易失性”,例如IP 地址暂时保存在Web 服务器的 易失性 存储器中;随后将被立即删除
使用正则表达式检查漏洞接口
这里提出一个关于正则表达式的示例 “用于验证某些输入数据” 将其代理到一个有漏洞的后端服务器的 URI 中包含 “//” 字符,从而绕过了某些安全保护措施,观察到的模式符合以下格式,"//api/product" 或 “/api//product”。此外,在某些时间点会出现周期性的请求峰值,URI 中包含 “%2f” 或 “%2F”,类似这样:"/api%2Fproduct" 或 “%2fapi/product”。这些模式可以使用带有正则表达式的 MAP 来匹配,并可以在安全规则中使用 MAP 变量。
使用到的匹配模式包含内置变量 $uri (不包含请求参数),这个参数可以用于上面提到的案例来做风险过滤,因为使用 $uri
来构建匹配规则看起来很适合。然而,这里存在一个问题,即 NGINX 可能会在请求处理的执行阶段修改或规范化 $uri
的值,这可能导致匹配规则不会匹配实际发送的 URI。
规范化编码:NGINX 在规范化
$uri
时可能会解码其中的特殊字符。例如,%2f
可能被解码为/
,这意味着匹配规则很将不会匹配实际的编码。
这里建议使用 NGINX 变量 $request_uri
来构建匹配器,而不是 $uri
,以确保准确匹配请求的 URI,同时保留查询字符串。$request_uri
是一个 NGINX 内置变量,包含了请求的完整 URI,包括查询字符串。与之前提到的 $uri
不同,$request_uri
不会在请求处理的执行阶段修改或规范化,因此匹配内容与原请求保持原始不变。
另外,为了简化处理,可以创建两个不同的 MAP,一个用于匹配实际内容,另一个用于 shun 规则。这种分离方式将允许在其他 MAP 中重复使用包含原始不变 URI 的 $uri_only
变量,如下所示:
map $request_uri $uri_only {
"~^(?<u>[^\?]+)\?(?:.*)?" $u;
default $request_uri;
}
map $uri_only $shun_if_client_is_a_baddy {
"~\/\/" 1;
"~*%2f" 1;
default 0;
}
在上面示例中,建立了 “分离” 方式的规则,下面是针对这组 map 含义解释:
map \$request_uri \$uri_only 创建了一个 map 将 $request_uri
的值用于匹配和分析
~
:告诉解析器后面的字符串应被解释为正则表达式。^
:表示正则表达式将从$request_uri
的字符串值的开头进行匹配。^(?<u>...)
:这里创建了一个 “命名捕获组”,名为u
,它会匹配括号中的表达式,并将匹配的部分分配给$u
变量。这个捕获组只在映射内部有效,不能在其他地方使用。^(?<u>[^\?]+)
:这部分正则表达式使用方括号来定义捕获的字符,匹配的内容是从开头(^
)到第一个问号?
之前的所有字符。这样,它捕获了$request_uri
的未修改部分,即不包括查询字符串的部分。\?(?:.*)?
:这一部分匹配一个问号?
,后面跟着一个可选的未捕获组,包含任意数量的任何字符直到字符串的末尾。尽管这部分正则表达式不是必要的,因为前面已经捕获了整个 URI 的未修改部分,但它被添加为完整性和可能的其他情况。
map \$uri_only \$shun_if_client_is_a_baddy :这个 map,用于将 $uri_only
的值用于匹配和决定是否将客户端标记为不良客户端。以下是有关这行代码的解释:
~
:告诉解析器后面的字符串应被解释为正则表达式,用于匹配$uri_only
。\/\/
:这部分正则表达式匹配$uri_only
中是否包含两个连续的正斜杠。如果匹配成功,将为$shun_if_client_is_a_baddy
赋值 “1”,否则为 “0”。~*
:这部分告诉解析器正则表达式应该以不区分大小写的方式进行匹配。%2f
:这部分正则表达式匹配$uri_only
中是否包含字符串 “%2f”。如果匹配成功,将为$shun_if_client_is_a_baddy
赋值 “1”,否则为 “0”。
规则应用
要使用上面 map 生效,可以将其放置在 server{}
上(同级),用于执行上述两个 map 并在请求处理阶段的早期触发响应:
if ($shun_if_client_is_a_baddy = 1) {
return 403 'You shall not pass!!!';
}
比较变量
在 NGINX MAP 应用中经常遇到的示例是使用纯 NGINX 指令来比较两个变量是否相等。而在 nginx location 上下文中,并不推荐使用 if [2],更多情况下执行变量比较通常推荐借助脚本语言来处理,例如下面一个 lua 示例
location /compare {
access_by_lua_block {
if ngx.var.variable1 == ngx.var.variable2 then
ngx.say("Variables are equal")
else
ngx.say("Variables are not equal")
end
}
}
但使用脚本语言增加了 NGINX 配置的复杂性以及需要内嵌或在引用单独文件中管理的另一段代码,在这种情况下 map 操作比较两个变量的场景是非常有用的。
# if delimeter between two variables is ':'
map $thing1:$thing2 $do_things_match {
"~^([^:]+):\1$" 1;
default 0;
}
上面的规则解析如下:
~
:字符告诉解析器后面的字符串应被解释为正则表达式。^
:这个符号表示正则表达式将从字符串值的开头进行匹配。^([^:]+)
:这部分正则表达式创建了一个无名捕获组,用于捕获从字符串开头(^
)到冒号:
之前的所有字符(不包含冒号)。这个捕获的内容将被分配给一个名为 “\1” 的后向引用变量。:\1$
:这一部分用于将捕获的内容与$thing2
变量的值进行比较。
如果 $thing1
和 $thing2
匹配,则这个表达式将被视为匹配(或为真),并将设置一个新的变量 $do_things_match
的值为 “1”。如果 $thing1
不匹配 $thing2
,则表达式不匹配,并将设置 $do_things_match
的值为 “0”。
下面是这个示例的一个应用,请求应该具有匹配的查询字符串 $arg_foo 和 X-BAR header
map $arg_FOO:$http_x_bar $shun_mismatched_payload {
"~^([^:]+):\1$" 1;
default 0;
}
那么可以在 server{} 段中增加判断
if ($shun_mismatched_payload = 1) {
return 403 'You shall not pass!!!';
}
真实请求IP的获取
例如通常我们需要在日志中打印用户的真实IP,而这个IP隐藏的很深,通常引用了多个字段,例如
- Remote Address 是nginx与客户端进行TCP连接过程中,获得的客户端真实地址。Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。 一般情况下,在Envoy作为最外层代理时,此IP为真实的IP客户端IP
- X-Real-IP 是一个自定义头。X-Real-Ip 通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端。X-Real-Ip 目前并不属于任何标准,代理和 Web 应用之间可以约定用任何自定义头来传递这个信息。
- X-Forwarded-For X-Forwarded-For 是一个扩展头。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP,现在已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。通常,X-Forwarded-For可被伪造,并且使用CDN会被重写
例如,下面从 CDN 获取真实 IP 的示例
map $http_x_connecting_ip $client_vsip {
"" $http_x_real_ip;
~^(?P<firstAddr>[0-9\.]+),?.*$ $fristAddr;
}
map $client_vsip $client_ydip {
"" $http_Incap_client_IP;
~^(?P<firstAddr>[0-9\.]+),?.*$ $fristAddr;
}
map $client_ydip $client_ip {
"" $http_x_forwarded_for;
~^(?P<firstAddr>[0-9\.]+),?.*$ $fristAddr;
}
map $client_ip $clientRealIP {
"" $remote_addr;
~^(?P<firstAddr>[0-9\.]+),?.*$ $fristAddr;
}
这个 NGINX 配置示例中包含了一系列的 map
指令,用于创建变量映射,以根据不同的请求头信息来设置一系列变量的值。这些变量之间形成了一种链式映射,最终将请求的真实 IP 地址存储在 $clientRealIP
变量中。让我解释这些映射的作用:
- 第一个 MAP 指令:
$http_x_connecting_ip
是输入变量,根据请求中的X-Connecting-IP
头部的值。$client_vsip
是输出变量,它根据$http_x_real_ip
或请求头部中的X-Real-IP
值进行映射。- 这个映射检查
$http_x_connecting_ip
的值,如果为空(""),则将$http_x_real_ip
的值赋给$client_vsip
,否则根据正则表达式提取$http_x_connecting_ip
中的 IP 地址,并赋给$client_vsip
。
- 第二个 MAP 指令:
$client_vsip
是输入变量,它是前一个映射的输出。$client_ydip
是输出变量,它根据$http_Incap_client_IP
或$client_vsip
进行映射。- 这个映射检查
$client_vsip
的值,如果为空(""),则将$http_Incap_client_IP
的值赋给$client_ydip
,否则根据正则表达式提取$client_vsip
中的 IP 地址,并赋给$client_ydip
。
- 第三个 MAP 指令:
$client_ydip
是输入变量,它是前一个映射的输出。$client_ip
是输出变量,它根据$http_x_forwarded_for
或$client_ydip
进行映射。- 这个映射检查
$client_ydip
的值,如果为空(""),则将$http_x_forwarded_for
的值赋给$client_ip
,否则根据正则表达式提取$client_ydip
中的 IP 地址,并赋给$client_ip
。
- 第四个 MAP 指令:
$client_ip
是输入变量,它是前一个映射的输出。$clientRealIP
是输出变量,它根据$remote_addr
或$client_ip
进行映射。- 这个映射检查
$client_ip
的值,如果为空(""),则将$remote_addr
的值赋给$clientRealIP
,否则根据正则表达式提取$client_ip
中的 IP 地址,并赋给$clientRealIP
。
最后在 location 段中将 $clientRealIP
向后传递
proxy_set_header X-Forwarded-For $clientRealIP
Reference
[1] Module ngx_http_map_module