Skip to content

Commit 3d4b64d

Browse files
authored
Merge pull request #23 from MegaManSec/master
Ensure that a variable is used in the path when using request_uri, and do not warn on proxy_pass_normalized if request_uri is used
2 parents 621d8ad + fbf1f8c commit 3d4b64d

File tree

11 files changed

+92
-15
lines changed

11 files changed

+92
-15
lines changed

gixy/directives/directive.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ def parents(self):
4646
def variables(self):
4747
raise NotImplementedError()
4848

49-
def find_directive_in_scope(self, name):
50-
"""Find directive in the current scope"""
49+
def find_directives_in_scope(self, name):
50+
"""Find directives in the current scope"""
5151
for parent in self.parents:
5252
directive = parent.some(name, flat=False)
5353
if directive:
54-
return directive
54+
yield directive
5555
return None
5656

5757
def __str__(self):
Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import gixy
3+
import sys
34
from gixy.plugins.plugin import Plugin
45

56
class proxy_pass_normalized(Plugin):
@@ -12,8 +13,8 @@ class proxy_pass_normalized(Plugin):
1213
"""
1314

1415
summary = 'Detect path after host in proxy_pass (potential URL decoding issue)'
15-
severity = gixy.severity.LOW
16-
description = ("A slash immediately after the host in proxy_pass leads to the path being decoded and normalized before proxying downstream, leading to unexpected behavior related to encoded slashes.")
16+
severity = gixy.severity.MEDIUM
17+
description = ("A path (beginning with a slash) after the host in proxy_pass leads to the path being decoded and normalized before proxying downstream, leading to unexpected behavior related to encoded slashes like %2F..%2F. Likewise, the usage of 'rewrite ^ $request_uri;' without using '$1' or '$uri' (or another captured group) in the path of proxy_pass leads to double-encoding of paths.")
1718
help_url = 'https://joshua.hu/proxy-pass-nginx-decoding-normalizing-url-path-dangerous#nginx-proxy_pass'
1819
directives = ['proxy_pass']
1920

@@ -22,23 +23,46 @@ def __init__(self, config):
2223
self.parse_uri_re = re.compile(r'(?P<scheme>[^?#/)]+://)?(?P<host>[^?#/)]+)(?P<path>/.*)?')
2324

2425
def audit(self, directive):
25-
proxy_pass_arg = directive.args[0]
26-
if not proxy_pass_arg:
26+
proxy_pass_args = directive.args
27+
rewrite_fail = False
28+
num_pattern = r'\$\d+'
29+
30+
if not proxy_pass_args:
2731
return
2832

29-
parsed = self.parse_uri_re.match(proxy_pass_arg)
33+
parsed = self.parse_uri_re.match(proxy_pass_args[0])
3034

3135
if not parsed:
3236
return
3337

34-
if not parsed.group('path'):
38+
for rewrite in directive.find_directives_in_scope("rewrite"):
39+
if hasattr(rewrite, 'pattern') and hasattr(rewrite, 'replace'):
40+
if rewrite.pattern == '^' and rewrite.replace == '$request_uri':
41+
if parsed.group('path'):
42+
match = re.search(num_pattern, parsed.group('path'))
43+
if match or '$uri' in parsed.group('path'):
44+
return
45+
else:
46+
rewrite_fail = True
47+
break
48+
else:
49+
if not parsed.group('host'):
50+
return # ?!
51+
match = re.search(num_pattern, parsed.group('host'))
52+
if match or '$uri' in parsed.group('host'):
53+
return
54+
else:
55+
rewrite_fail = True
56+
break
57+
58+
if not parsed.group('path') and not rewrite_fail:
3559
return
3660

3761
self.add_issue(
3862
severity=self.severity,
3963
directive=[directive, directive.parent],
4064
reason=(
41-
"Found a slash (and possibly more) after the hostname in proxy_pass. "
42-
"This can lead to path decoding issues."
65+
"Found a path after the host in proxy_pass, without using $request_uri and a variable (such as $1 or $uri). "
66+
"This can lead to path decoding issues or double-encoding issues."
4367
)
4468
)

gixy/plugins/try_files_is_evil_too.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class try_files_is_evil_too(Plugin):
1818

1919
def audit(self, directive):
2020
# search for open_file_cache ...; on the same or higher level
21-
open_file_cache = directive.find_directive_in_scope("open_file_cache")
22-
if not open_file_cache or open_file_cache.args[0] == "off":
21+
open_file_cache = list(directive.find_directives_in_scope("open_file_cache"))
22+
if not open_file_cache or open_file_cache[0].args[0] == "off":
2323
self.add_issue(
2424
severity=gixy.severity.MEDIUM,
2525
directive=[directive],
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
location /b {
2+
rewrite ^ $request_uri; # Sets the $1/$uri variable to the raw path
3+
proxy_pass http://127.0.0.1:8000/; # No $1 or $uri or other variable, resulting in path being double-encoded
4+
}
5+
6+
# Request received by nginx: /%2F
7+
# Request received by backend: /%252F
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
location /b {
2+
rewrite ^ $request_uri; # Sets the $1/$uri variable to the raw path
3+
proxy_pass http://127.0.0.1:8000/$1; # $1 used, resulting in path being passed as the raw path from the original request.
4+
}
5+
6+
# Request received by nginx: /%2F
7+
# Request received by backend: /%2F # Possibly also receives //%2F)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
location /b {
2+
rewrite ^ $request_uri; # Sets the $1/$uri variable to the raw path
3+
proxy_pass http://127.0.0.1:8000; # No $1 or $uri or other variable in either host or path, resulting in path being double-encoded
4+
}
5+
6+
# Request received by nginx: /%2F
7+
# Request received by backend: /%252F
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
location /b {
2+
rewrite ^ $request_uri; # Sets the $1/$uri variable to the raw path
3+
proxy_pass http://127.0.0.1:8000$1; # $1 used in host, resulting in path being passed as the raw path from the original request.
4+
}
5+
6+
# Request received by nginx: /%2F
7+
# Request received by backend: /%2F # Possibly also receives //%2F)
8+
# This is actually no different than proxy_pass_path_fp.conf since $1 is not a path.
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
location / {
2-
proxy_pass http://server/;
2+
proxy_pass http://server/; # Path "/" used, resulting in proxy_pass urldecoding the path.
33
}
4+
5+
# Request received by nginx: /%2F
6+
# Request received by backend: //
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
location / {
2-
proxy_pass http://downstream;
2+
proxy_pass http://downstream; # No rewrite rules, and no path used: no extra/missing urldecoding/urlencoding occurs.
33
}
4+
5+
# Request received by nginx: /%2F
6+
# Request received by backend: /%2F
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
location /1/ {
2+
rewrite ^ $request_uri; # Sets $1/$uri to raw path.
3+
rewrite ^/1(/.*) $1 break; # Extracts everything after /1 and places it into $1/$uri.
4+
return 400; # extremely important! # If rewrite rule does not break (e.g. //1/), return 400, otherwise the location-block will fall-through and proxy_pass will not actually happen at all.
5+
proxy_pass http://127.0.0.1:8080/$1; # $1 used, resulting in path being passed as the raw path from the original request.
6+
}
7+
8+
# Request received by nginx: /1/%2F
9+
# Request received by backend: /%2F (or possibly //%2F)

0 commit comments

Comments
 (0)