Nginx有一个配置项是merge_slashes,官方的说明如下:

Syntax: merge_slashes on  off;

Default: merge_slashes on;

Context: http, server

Enables or disables compression of two or more adjacent slashes in a URI into a single slash.

顾名思义,就是会在处理URI时将相邻的单斜杠合并。这个配置项在Nginx中对应的源码是在https://github.com/nginx/nginx/blob/master/src/http/ngx_http_parse.c#L1248,该函数的声明是:

ngx_int_t ngx_http_parse_complex_uri(ngx_http_request_t *r, ngx_uint_t merge_slashes)

第一个参数是URI的请求结构,第二个int类型参数merge_slashes就是配置项的开(on)或关(off)。

函数中,首先有一个枚举类型的变量state用于函数执行中标识每个字符的状态,或者是字符的分类,初始情况下是sw_usual正常状态。

1
2
3
4
5
6
7
8
enum {
sw_usual = 0,
sw_slash,
sw_dot,
sw_dot_dot,
sw_quoted,
sw_quoted_second
} state, quoted_state;

字符型指针*u在函数执行过程中用于记录处理的中间URI字符串,字符型指针*p是URI的字符串,字符ch用于记录当前在处理中的URI的字符。

接着是一个大的while循环,用于逐个处理URI中的字符:

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
ch = *p++;
while (p <= r->uri_end) {
switch (state) {
case sw_usual:
...
switch (ch) {
...
case '/':
...
state = sw_slash;
*u++ = ch;
break;
...
}
ch = *p++;
break;

case sw_slash:
...
switch (ch) {
case '/':
if (!merge_slashes) {
*u++ = ch;
}
break;
...
}
ch = *p++;
break;
...
case sw_dot_dot:
...
switch (ch) {
case '/':
u -= 4;
for ( ;; ) {
if (u < r->uri.data) {
return NGX_HTTP_PARSE_INVALID_REQUEST;
}
if (*u == '/') {
u++;
break;
}
u--;
}
state = sw_slash;
break;
}
}
}

函数根据每个处理的字符类型标识当前字符的状态或类型,该状态决定在之后的条件判断语句中该如何处理后续字符。通过以上函数的逻辑可知,如果merge_slashes为1,相邻出现的多个单斜杠会在sw_slash状态的条件处理中被忽略,*u不记录单斜杠,如果为0,则会记录单斜杠字符。

因此,多个斜杠相邻的URI会被处理为单个斜杠,如果需要在URI中传输BASE64字符,则需要关闭merge_slashes,避免其中的斜杠被处理。

另外,如果URI中存在路径穿越的利用,比如“/../../../”,会条件处理到sw_dot_dot中,并返回NGX_HTTP_PARSE_INVALID_REQUEST,即无效的请求。所以Nginx默认情况下可以防范路径穿越攻击,除非将merge_slashes参数设置为关闭。