Skip to content

[Security] Regular Expression Denial of Service (ReDoS) in cleanco.basename() #102

Description

@gnsehfvlr

Hello,

I believe I have identified a security vulnerability and would like to report it through responsible disclosure. If the issue is confirmed, I would appreciate it if a CVE identifier could be requested and assigned after the vulnerability has been patched.

Please find the details of the vulnerability below.

Best regards,


CVE Vulnerability Report: Regular Expression Denial of Service (ReDoS) in cleanco.basename()

Summary

Field Value
Package cleanco
Version 2.3
Vulnerability ReDoS via tail_removal_rexp catastrophic backtracking in basename()
CWE CWE-1333 (Inefficient Regular Expression Complexity)
CVSS 3.1 7.5 (High)
CVSS Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Monthly Downloads ~150,000
Repository https://github.com/psolin/cleanco
Affected Function cleanco.basename() in cleanco/clean.py

GitHub Security Advisory Form

Field Value
Ecosystem pip
Package name cleanco
Affected versions <= 2.3
Patched versions No patch available
Severity High
CVSS Score 7.5
Vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-1333

Description

The cleanco company name normalization library (~150,000 monthly downloads) contains a Regular Expression Denial of Service (ReDoS) vulnerability in its basename() function. The tail_removal_rexp regular expression used to strip legal entity suffixes (e.g., "Ltd", "GmbH") exhibits catastrophic backtracking when the input ends with a long sequence of non-word characters, causing processing to take hundreds of milliseconds per call.

Since cleanco.basename() is designed to process external company name data (from APIs, CRMs, KYC providers, web scraping), this vulnerability is directly exploitable in production systems. An attacker who can supply company name strings to an application using this library can trigger severe per-request latency, exhausting worker threads and causing a denial of service.

Root Cause

In cleanco/clean.py line 22:

tail_removal_rexp = re.compile(r"[^\.\.\w]+$")

This regex uses a character class with a + quantifier anchored to $. On CPython's re module, inputs containing many non-word characters that ultimately fail to match (because no word character terminates the string in the expected position) cause O(N²) backtracking. The engine attempts every possible split of the non-word character sequence against the + quantifier before concluding no match is possible, resulting in quadratic time complexity relative to the number of non-word characters.

Verified timing:

  • Normal input "Acme Ltd": ~0.001ms
  • Adversarial input "Acme " + "&" * 2000 + " Ltd": 157ms (measured, reproducible)

Proof of Concept

import cleanco, time

# Normal input — fast
t0 = time.perf_counter()
cleanco.basename("Acme Corporation Ltd.")
print(f"Normal: {(time.perf_counter()-t0)*1000:.2f}ms")  # ~0.001ms

# Adversarial input — catastrophic backtracking
adversarial = "Acme " + "&" * 2000 + " Ltd"
t0 = time.perf_counter()
cleanco.basename(adversarial)
print(f"Adversarial: {(time.perf_counter()-t0)*1000:.1f}ms")  # ~157ms

# Simpler trigger: string ending in many non-word characters
adversarial2 = " " * 3000 + "X"
t0 = time.perf_counter()
cleanco.basename(adversarial2)
print(f"Adversarial2: {(time.perf_counter()-t0)*1000:.1f}ms")  # ~150ms+

Impact

  • Denial of Service: Adversarial company name strings cause ~150ms+ processing delay per call, compared to sub-millisecond for normal inputs.
  • Service Disruption: B2B APIs, KYC pipelines, CRM enrichment services, and any application that processes user-submitted or third-party company names using cleanco.basename() are vulnerable.
  • No Authentication Required: The vulnerability is exploitable by any party that can influence the company name strings processed by the application (unauthenticated attackers via public APIs, form submissions, or injected third-party data).

Attack Scenario

  1. A B2B SaaS application accepts company names from users or third-party APIs and normalizes them using cleanco.basename().
  2. An attacker submits a company name containing a long sequence of non-word characters: "Fake Corp &&&&&&&&&&&&&&&&&&&&&&&&&&&&& Ltd".
  3. Each call to basename() with the adversarial input blocks the Python process for ~157ms.
  4. Concurrent requests with such inputs exhaust the server's worker threads, causing a denial of service affecting all users of the application.

Remediation

Replace the vulnerable regex with one that avoids catastrophic backtracking, or add an input length guard:

# BEFORE (vulnerable)
tail_removal_rexp = re.compile(r"[^\.\.\w]+$")

# AFTER (safe) — use atomic grouping via the `regex` module
import regex
tail_removal_rexp = regex.compile(r"[^\w.]+$")  # atomic by default in `regex`

# Alternatively, simplest fix: add input length limit
def basename(name, terms=None):
    if len(name) > 500:  # company names are never this long
        raise ValueError(f"Input too long: {len(name)} chars")
    # ... existing logic

The core issue is that the character class [^\.\.\w] (equivalent to [^\w.]) combined with + and $ creates an ambiguous quantifier over the non-word characters. Using the third-party regex module (which supports possessive quantifiers and atomic groups) or imposing a maximum input length prevents the catastrophic backtracking.

Timeline

Date Event
2026-06-30 Vulnerability discovered
2026-06-30 Report drafted
TBD Vendor notification
TBD CVE ID assigned
TBD Patch released

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions