Skip to content

Commit 821964e

Browse files
authored
Merge pull request #30 from MegaManSec/recheck
regex_redos.py: Check whether location-block contains ReDoS regexp.
2 parents 726b74c + a1289d4 commit 821964e

File tree

4 files changed

+123
-1
lines changed

4 files changed

+123
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ GIXY
99
[![GitHub pull requests](https://img.shields.io/github/issues-pr/dvershinin/gixy.svg?style=flat-square)](https://github.com/dvershinin/gixy/pulls)
1010

1111
> [!TIP]
12-
> This is an **actively maintained fork** of the original [Gixy](https://github.com/yandex/gixy) project by **Yandex LLC**.
12+
> This is an **actively maintained fork** of the original [Gixy](https://github.com/yandex/gixy) project by **Yandex LLC**.
1313
1414
# Overview
1515
<img align="right" width="192" height="192" src="docs/gixy.png">
@@ -43,6 +43,7 @@ Right now Gixy can find:
4343
* [[worker_rlimit_nofile_vs_connections] `worker_rlimit_nofile` must be at least twice `worker_connections`](https://gixy.getpagespeed.com/en/plugins/worker_rlimit_nofile_vs_connections/)
4444
* [[error_log_off] `error_log` set to `off`](https://gixy.getpagespeed.com/en/plugins/error_log_off/)
4545
* [[unanchored_regex] Regular expression without anchors](https://gixy.getpagespeed.com/en/plugins/unanchored_regex/)
46+
* [[regex_redos] Regular expressions may result in easy denial-of-service (ReDoS) attacks](https://joshua.hu/regex-redos-recheck-nginx-gixy)
4647

4748
You can find things that Gixy is learning to detect at [Issues labeled with "new plugin"](https://github.com/dvershinin/gixy/issues?q=is%3Aissue+is%3Aopen+label%3A%22new+plugin%22)
4849

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Right now Gixy can find:
3333
* [[resolver_external] Using external DNS nameservers](https://blog.zorinaq.com/nginx-resolver-vulns/)
3434
* [[version_disclosure] Using insecure values for server_tokens](en/plugins/version_disclosure.md)
3535
* [[proxy_pass_normalized] Using proxy_pass with a pathname will normalize and decode the requested path when proxying](https://joshua.hu/proxy-pass-nginx-decoding-normalizing-url-path-dangerous#nginx-proxy_pass)
36+
* [[regex_redos] Regular expressions may result in easy denial-of-service (ReDoS) attacks](https://joshua.hu/regex-redos-recheck-nginx-gixy)
3637

3738
You can find things that Gixy is learning to detect at [Issues labeled with "new plugin"](https://github.com/dvershinin/gixy/issues?q=is%3Aissue+is%3Aopen+label%3A%22new+plugin%22)
3839

gixy/plugins/regex_redos.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import requests
2+
import gixy
3+
from gixy.plugins.plugin import Plugin
4+
5+
6+
class regex_redos(Plugin):
7+
r"""
8+
This plugin checks all directives for regular expressions that may be
9+
vulnerable to ReDoS (Regular Expression Denial of Service). ReDoS
10+
vulnerabilities may be used to overwhelm nginx servers with minimal
11+
resources from an attacker.
12+
13+
Example of a vulnerable directive:
14+
location ~ ^/(a|aa|aaa|aaaa)+$
15+
16+
Accessing the above location with a path such as
17+
/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
18+
can result in catastrophic backtracking.
19+
20+
This plugin relies on an external, public API to determine vulnerability.
21+
Because of this network-dependence, and the fact that potentially private
22+
expressions are sent over the network, usage of this plugin requires
23+
the --regex-redos-url flag. This flag must specify the full URL to a
24+
service which can be queried with expressions, responding with a report
25+
matching the https://github.com/makenowjust-labs/recheck format.
26+
27+
An implementation of a compatible server:
28+
https://github.com/MegaManSec/recheck-http-api
29+
"""
30+
31+
summary = (
32+
'Detect directives with regexes that are vulnerable to '
33+
'Regular Expression Denial of Service (ReDoS).'
34+
)
35+
severity = gixy.severity.HIGH
36+
unknown_severity = gixy.severity.UNSPECIFIED
37+
description = (
38+
'Regular expressions with the potential for catastrophic backtracking '
39+
'allow an nginx server to be denial-of-service attacked with very low '
40+
'resources (also known as ReDoS).'
41+
)
42+
help_url = 'https://joshua.hu/regex-redos-recheck-nginx-gixy'
43+
directives = ['location'] # XXX: Missing server_name, rewrite, if, map, proxy_redirect
44+
options = {
45+
'url': ""
46+
}
47+
skip_test = True
48+
49+
def __init__(self, config):
50+
super(regex_redos, self).__init__(config)
51+
self.redos_server = self.config.get('url')
52+
53+
def audit(self, directive):
54+
# If we have no ReDoS check URL, skip.
55+
if not self.redos_server:
56+
return
57+
58+
# Only process directives that have regex modifiers.
59+
if directive.modifier not in ('~', '~*'):
60+
return
61+
62+
regex_pattern = directive.path
63+
fail_reason = f'Could not check regex {regex_pattern} for ReDoS.'
64+
65+
modifier = "" if directive.modifier == "~" else "i"
66+
json_data = {"1": {"pattern": regex_pattern, "modifier": modifier}}
67+
68+
# Attempt to contact the ReDoS check server.
69+
try:
70+
response = requests.post(
71+
self.redos_server,
72+
json=json_data,
73+
headers={"Content-Type": "application/json"},
74+
timeout=60
75+
)
76+
except requests.RequestException:
77+
self.add_issue(directive=directive, reason=fail_reason, severity=self.unknown_severity)
78+
return
79+
80+
# If we get a non-200 response, skip.
81+
if response.status_code != 200:
82+
self.add_issue(directive=directive, reason=fail_reason, severity=self.unknown_severity)
83+
return
84+
85+
# Attempt to parse the JSON response.
86+
try:
87+
response_json = response.json()
88+
except ValueError:
89+
self.add_issue(directive=directive, reason=fail_reason, severity=self.unknown_severity)
90+
return
91+
92+
# Ensure the expected data structure is present and matches the pattern.
93+
if (
94+
"1" not in response_json or
95+
response_json["1"] is None or
96+
"source" not in response_json["1"] or
97+
response_json["1"]["source"] != regex_pattern
98+
):
99+
self.add_issue(directive=directive, reason=fail_reason, severity=self.unknown_severity)
100+
return
101+
102+
recheck = response_json["1"]
103+
status = recheck.get("status")
104+
105+
# If status is neither 'vulnerable' nor 'unknown', the expression is safe.
106+
if status not in ("vulnerable", "unknown"):
107+
return
108+
109+
# If the status is unknown, add a low-severity issue (likely the server timed out)
110+
if status == "unknown":
111+
reason = f'Could not check complexity of regex {regex_pattern}.'
112+
self.add_issue(directive=directive, reason=reason, severity=self.unknown_severity)
113+
return
114+
115+
# Status is 'vulnerable' here. Report as a high-severity issue.
116+
complexity_summary = recheck.get("complexity", {}).get("summary", "unknown")
117+
reason = f'Regex is vulnerable to {complexity_summary} ReDoS: {regex_pattern}.'
118+
self.add_issue(directive=directive, reason=reason, severity=self.severity)

tests/plugins/test_simply.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def generate_config_test_cases():
4646

4747
manager = PluginsManager()
4848
for plugin in manager.plugins:
49+
if getattr(plugin, 'skip_test', False):
50+
continue
4951
plugin = plugin.name
5052
assert plugin in tested_plugins, 'Plugin {name!r} should have at least one simple test config'.format(name=plugin)
5153
assert plugin in tested_fp_plugins, 'Plugin {name!r} should have at least one simple test config with false positive'.format(name=plugin)

0 commit comments

Comments
 (0)