Any Nautobot user with rename access on a single object type (e.g.
dcim.change_interface) can submit a(a+)+$-style "evil regex" to the bulk-rename endpoint and pin a Nautobot worker thread onre.sub()until the connection is killed — application-wide DoS gated only by low-privilege object-change permission.
| CVE | CVE-2026-44796 (NVD) |
| GHSA | GHSA-qrpw-gjvh-x5gm |
| Severity | Moderate — CVSS 6.5 |
| Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-1333: Inefficient Regular Expression Complexity (also CWE-400: Uncontrolled Resource Consumption) |
| Affected | nautobot < 2.4.33, < 3.1.2 |
| Fixed | 2.4.33, 3.1.2 |
| Authentication | Authenticated user with change_<model> permission for any model that exposes the bulk-rename action (Interface, Device, Cable, …) |
| Reporter | @whatisproblem |
| Fix commits | c2b7669 (2.4.33), 5a30d09 (3.1.2) |
Nautobot's UI ships a "bulk rename" action on most object list views — /dcim/interfaces/rename/, /dcim/devices/rename/, /dcim/cables/rename/, etc. The form lets the operator type a find pattern, a replace string, and tick a use_regex checkbox; if use_regex is on, Nautobot compiles the find value with Python's re module and runs re.sub(find, replace, name) against each selected object's name to produce the renamed value.
Pre-2.4.33 / 3.1.2, that re.sub() had no timeout, no length cap, and no complexity bound. Python's re engine is a backtracking NFA — there is a well-known family of ReDoS patterns whose evaluation time is exponential in the input length:
(a+)+$
(a|a)*b
^(([a-z])+.)+[A-Z]([a-z])+$
Submitting one of those with use_regex=True, plus an input string that nearly matches but eventually fails (a long aaaaaaaa...x), pins the worker thread on the re.sub() call. With Nautobot's default Gunicorn worker count, one such request per worker is enough to exhaust the entire pool. New requests queue, hit worker timeout, and the application is unavailable until the workers are restarted.
The minimum permission required is the change_<model> permission for any one of the models that has a bulk-rename action — a permission routinely granted to network engineers, not just admins. A user with dcim.change_interface (a baseline-operator permission) is enough.
Pre-fix nautobot/core/views/generic.py — BulkRenameView.post() (paraphrased to the security-relevant lines):
import re
class BulkRenameView(...):
def post(self, request, *args, **kwargs):
...
find = form.cleaned_data["find"]
replace = form.cleaned_data["replace"]
use_regex = form.cleaned_data["use_regex"]
for obj in selected_objects:
if use_regex:
obj.new_name = re.sub(find, replace, obj.name) # ⚠ no timeout, no bound
else:
obj.new_name = obj.name.replace(find, replace)
...Three properties combine to make this exploitable:
- User-controlled regex pattern.
findis whatever the user typed. There is no static analysis of the pattern's complexity, no allow-list of "safe regex constructs", no length cap. - Server-side execution with no timeout. Python's
remodule does not support a timeout (the standard library'sre.compile/re.subcannot be cancelled). There.subcall runs to completion or until the worker is killed. - Loop multiplier.
re.subruns once per selected object. The attacker can select 100 objects in the bulk-rename form to multiply wall-clock time by 100×, but in practice a single ReDoS pattern + a single ~30-character input string is already enough to exceed Gunicorn's--timeout.
The exploit input is typed straight into the bulk-rename form — it doesn't even need API access. The find field is (a+)+$, the replace field is anything, use_regex is checked, and any one selected object whose name happens to be an aaaaaaaaaaaaa...! shape (or where the attacker has rename-permission to first set a name to such a shape) will trigger the catastrophic backtracking on submit.
git clone https://github.com/nautobot/nautobot
cd nautobot && git checkout v2.4.32
docker compose -f development/docker-compose.yml up -dProvision a user with dcim.change_interface (or any change_<model> permission for a model that has a bulk-rename action — Device, Cable, Rack, etc.).
Create one Interface named aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! (any string of ~30 as followed by a non-a character).
Browse to /dcim/interfaces/, tick the checkbox next to the aaaa... interface, click Rename Selected.
In the rename form:
| Field | Value |
|---|---|
| Find | (a+)+$ |
| Replace | x |
| Use regex | ✅ |
Click Apply.
The HTTP request hangs. Tail the worker process (docker compose logs nautobot -f) and observe one Gunicorn worker pinned at 100% CPU. After Gunicorn's worker timeout (default 30s) elapses:
[CRITICAL] WORKER TIMEOUT (pid:42)
Gunicorn kills and respawns the worker. Sending the same request to N workers simultaneously (where N = Gunicorn worker count) starves the entire pool — all subsequent requests, including ones from other users, queue indefinitely until the workers cycle.
Demonstrative scope only. The PoC uses a single ~30-character interface name and the textbook
(a+)+$pattern. A real attacker would (a) tune the pattern + input length to exceed worker timeout reliably while staying under request body limits, (b) send the request to N workers in parallel from a single shell loop, and (c) re-send on a tight loop to keep the worker pool starved as workers respawn. None of those amplifications are reproduced here.
Patched in v2.4.33 (commit c2b7669) and v3.1.2 (commit 5a30d09) by switching from the standard-library re module to the third-party regex module — which supports a timeout= parameter on every match call — and bounding the bulk-rename regex evaluation to a hard wall-clock budget.
- import re
+ import regex
class BulkRenameView(...):
+ bulk_rename_regex_timeout = 1.0 # seconds
def post(self, request, *args, **kwargs):
...
if use_regex:
- try:
- pattern = re.compile(find)
- except re.error as e:
- form.add_error("find", f"Invalid regex: {e}")
- else:
- for obj in selected_objects:
- obj.new_name = pattern.sub(replace, obj.name)
+ try:
+ pattern = regex.compile(find)
+ except regex.error as e:
+ form.add_error("find", f"Invalid regex: {e}")
+ else:
+ try:
+ for obj in selected_objects:
+ obj.new_name = pattern.sub(
+ replace, obj.name,
+ timeout=self.bulk_rename_regex_timeout,
+ )
+ except TimeoutError:
+ form.add_error(
+ "find",
+ "Regex matching exceeded the configured timeout and was aborted; "
+ "the pattern may have catastrophic backtracking."
+ )Three things this gets right:
- Hard wall-clock cap.
regexraisesTimeoutErrorafterbulk_rename_regex_timeoutseconds per call, regardless of pattern complexity. There is no way to construct a pattern that bypasses the timer. - Two-phase form processing. The fix splits validation from application: phase 1 runs the regex with the timeout to compute
new_namefor every selected object; phase 2 only persists if phase 1 had no errors. A timeout in phase 1 surfaces as a form error, not a half-applied rename. - Configurable budget.
bulk_rename_regex_timeout = 1.0is a class attribute, so per-view subclasses can adjust it (in either direction) without forking the whole bulk-rename flow.
The regex library is added as a runtime dependency (pyproject.toml + poetry.lock updates in the same commit). It's a long-lived, well-maintained drop-in: same API surface as re, plus the timeout= kwarg that the standard library has resisted adding for over a decade.
- Apply the same
regex.compile(...).sub(..., timeout=...)treatment to every place Nautobot evaluates a user-supplied regex (filter forms with regex operators, custom-field validation patterns, etc.). The fix here is scoped to the bulk-rename surface; a second pass through the codebase forre.compile/re.sub/re.search/re.matchwith user-controlled patterns would catch the rest. - Consider rate-limiting the bulk-rename endpoint per user — a 1-second timeout still lets an attacker burn 60 worker-seconds per minute per worker if they fire requests in a tight loop. A short
1 req/10srate limit on rename actions would keep that bounded.
- (internal research, prior to disclosure) — Discovered.
- (prior to 2026-05-08) — Reported privately via GHSA Draft to the Nautobot maintainers.
- 2026-05-08 — Patches released in
v2.4.33andv3.1.2; GHSA-qrpw-gjvh-x5gm published; CVE-2026-44796 assigned. - 2026-05-10 — Public write-up.
- Python's
reis a ReDoS hazard wherever the pattern is user-controlled. The standard library has no timeout, no static-complexity check, and no way to cancel an in-progress match. Any user-controlledfindfield that ends up inre.sub/re.match/re.searchis a one-shot DoS unless wrapped. Theregexlibrary'stimeout=kwarg is the cheapest known fix; alternatives are RE2-based engines (google-re2,pyre2) that reject backtracking-prone patterns at compile time, or running the regex inside a subprocess with a wall-clock kill. - "Authenticated" is not a meaningful gate when the permission is broadly delegated.
dcim.change_interfaceis a baseline operator permission. Any DoS gated by "needs to be logged in as a network engineer" is functionally a low-skill attack against operations teams. The fix correctly treats this as a real DoS path, not a "trusted user" issue. - Synchronous-request CPU work needs a wall-clock budget. Anything that runs inside a
request → responsecycle and can take user-controlled time (regex, JSONPath, XPath, GraphQL query depth, template rendering) needs an explicit timeout. The Gunicorn worker timeout is a failsafe, not a control: by the time it fires, the worker is already killed and respawned and the user has visibly seen a 504. - Two-phase form handling makes safe-cancellation possible. The pre-fix code interleaved "compute the new name" and "persist the new name". The fix separates the compute pass (which can fail-stop on timeout) from the persist pass (which only runs if compute succeeded for every selected object). Forms that mutate state in a loop should always validate-everything-then-apply-everything, not validate-and-apply per item.
- Variant analysis: grep for the shape, not the symptom. The same
re.compile(<user-input>)pattern likely exists elsewhere in the codebase. After landing the bulk-rename fix, the natural follow-up isgrep -r 'compile\(.*find\)' nautobot/andgrep -r 're\.\(sub\|match\|search\)' nautobot/to enumerate every other surface that takes a user pattern.
- CVE Record: https://www.cve.org/CVERecord?id=CVE-2026-44796
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-44796
- GHSA: https://github.com/nautobot/nautobot/security/advisories/GHSA-qrpw-gjvh-x5gm
- CWE-1333: https://cwe.mitre.org/data/definitions/1333.html
- CWE-400: https://cwe.mitre.org/data/definitions/400.html
- Fix commits:
c2b7669(2.4.33) /5a30d09(3.1.2) regexlibrary (drop-inrereplacement withtimeout=): https://pypi.org/project/regex/- OWASP ReDoS reference: https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- Upstream project: https://github.com/nautobot/nautobot