nginx篇09-location中的if指令是魔鬼吧

本文最后更新于:May 27, 2021 pm

本文主要是对nginx官方的文章If is Evil... when used in location context的翻译和理解。

1、简介

location块中的if指令的有点问题,在某些情况下,if指令并不会按照我们预期的那般工作,而是有可能往完全不同的方向发展甚至可能会引起错误。因此最好的方法就是尽可能的不要使用if指令。

location块的if指令内100%安全的指令有:

除了上面的两个指令之外的任何操作都有可能导致不可预测的效果甚至可能是SIGSEGV错误(可能出现内存错误导致程序异常终止)。

需要注意的是:if指令的执行结果是一致的,也就是说相同的两个请求操作,不可能会出现一个成功但是另一个失败的情况。这就意味着只要经过足够合适的测试以及我们对相应操作的足够了解,if指令是“可以”被使用的。这个建议对于其他的指令也是同样适用的。

很多情况下我们是没办法避免使用if指令的,例如当我们需要判断某个变量是否等于某个值或者包含某个字段的时候,就会需要用到if指令:

1
2
3
4
5
6
if ($request_method = POST ) {
return 405;
}
if ($args ~ post=140){
rewrite ^ http://example.com/ permanent;
}

2、替代方案

我们可以使用 try_files 指令、“return …”指令或者“rewrite … last”指令用来替代if指令。在某些条件允许的情况下,也可以把if指令移动到server块的层级,此时的if指令是可以安全使用的,因为这里只有其他的rewrite模块指令能被使用(原因下文会解释)。

当然对于一些需要使用if指令来判断返回4xx5xx之类的异常代码响应页面或者是操作也可以尝试使用return指令搭配error_page指令来保证安全,例如下面的这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
location / {
error_page 418 = @other;
recursive_error_pages on;

if ($something) {
return 418;
}

# some configuration
...
}

location @other {
# some other configuration
...
}

此外,在某些情况下使用一些嵌入的脚本模块(如lua,embedded perl, 或者是其他的ngx第三方模块)来完成一些较负责的逻辑操作和判断也不失为一个好主意。

3、已知错误

下面列举了一些if指令不按照预期工作的的情况,请注意不要在生产环境上配置这些指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# Here is collection of unexpectedly buggy configurations to show that
# if inside location is evil.

# only second header will be present in response
# not really bug, just how it works

location /only-one-if {
set $true 1;

if ($true) {
add_header X-First 1;
}

if ($true) {
add_header X-Second 2;
}

return 204;
}

# request will be sent to backend without uri changed
# to '/' due to if

location /proxy-pass-uri {
proxy_pass http://127.0.0.1:8080/;

set $true 1;

if ($true) {
# nothing
}
}

# try_files wont work due to if

location /if-try-files {
try_files /file @fallback;

set $true 1;

if ($true) {
# nothing
}
}

# nginx will SIGSEGV

location /crash {

set $true 1;

if ($true) {
# fastcgi_pass here
fastcgi_pass 127.0.0.1:9000;
}

if ($true) {
# no handler here
}
}

# alias with captures isn't correcly inherited into implicit nested
# location created by if

location ~* ^/if-and-alias/(?<file>.*) {
alias /tmp/$file;

set $true 1;

if ($true) {
# nothing
}
}

如果发现了上面没有列出出来的情况,可以邮件到NGINX development mailing list.

4、原因分析

if指令本质上其实是rewrite模块的一部分,rewrite模块本身设计就是用来“命令式”地执行操作(这就是前面说的为什么if里面搭配return和rewrite指令会100%安全的原因)。另一方面。nginx的配置通常来说是声明式的,但是出于某些原因,用户需要在if中添加某些非重写(non-rewrite)的指令(例如使用if判断http_origin之类的参数然后添加header),就有可能出现我们前面说的不正常工作的情况,尽管大部分情况下是能够正常工作的……

目前看来唯一的解决方案就是完全禁用掉if中的所有非重写指令(non-rewrite directives),但是这会导致很多已有的配置无法正常工作(会极大地影响前向兼容性),因此一直没有处理这个问题。

5、如仍使用

如果还是需要在location中使用if指令,请务必注意:

  • 明确if工作原理和流程
  • 事先进行足够的测试以确保能够正常工作

You were warned.