Weblate · Report #4022462
Uncaught RecursionError crash in translation-finder (deep directory tree DoS)
How it was found
Type: Denial of Service (Availability) Severity: Low-Medium (CVSS 3.7-5.5 depending on access) Affected: WeblateOrg/translation-finder v3.4.1, commit f5f8bb5 (current main)
TL;DR
translation-finder scans repository content using recursion without any depth limit. A repository with ~1000 nested folders causes an uncaught RecursionError that crashes the entire discovery call. The error is not handled anywhere, so Weblate's import/update/hook processing fails repeatedly.
Tested environment
- Python 3.13.9
- charset-normalizer 3.5.1
- ruamel.yaml 0.19.1
- Linux x86_64
Where the bug is
translation_finder/finder.py, lines 157–177:
def list_files(self, root: PurePath, files: PathListType, dirs: PathListType) -> None:
with scandir(root) as matches:
for match in matches:
if match.is_symlink():
continue
is_dir = match.is_dir()
path = Path(match.path)
if any(path.match(exclude) for exclude in EXCLUDES):
continue
if is_dir:
dirs.append(self.process_path(path))
try:
self.list_files(path, files, dirs) # <-- line 176: unbounded recursion
except OSError:
continue
else:
files.append(self.process_path(path))
The function calls itself recursively for every subdirectory. Only OSError is caught — RecursionError escapes and crashes discover() / Finder().
Why it matters
Git fully supports trees that are 1000+ levels deep. Any contributor can push such a tree to a repository. When Weblate scans that repo (import, update, webhook, PR), the discovery task crashes. Every retry also crashes, so the component's translation activity stops.
Steps to reproduce
- Run the PoC script:
python3 poc_deepnest.py 1000 - The script creates a tree with 1000 nested directories plus two tiny translation files.
- It calls
discover()on the tree.
Result (measured, repeatable)
| Depth | Result |
|---|---|
| 960 | pass — no crash |
| 985 | pass — no crash |
| 990 | CRASH — RecursionError |
| 1000, 1050, 1100 | CRASH |
The exact boundary is ~960–1000 depending on Python version and stack. Below it works, above it always crashes.
Proof
Attachment: bug1_traceback.txt — actual crash traceback from a real run.
Key frames:
File "translation_finder/api.py", line 54, in discover
finder = Finder(root, mock=mock)
File "translation_finder/finder.py", line 73, in __init__
self.list_files(root, files, dirs)
File "translation_finder/finder.py", line 176, in list_files
self.list_files(path, files, dirs)
[Previous line repeated 984 more times]
File "translation_finder/finder.py", line 154, in process_path
relative = path.relative_to(self.root)
RecursionError: maximum recursion depth exceeded
Attachment: bug1_finder_frames.txt — same crash with library frames extracted for readability.
CVSS 3.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H= 3.7 (Low) — admin action required for importPR:Nin webhook / PR-driven setups = 5.5 (Medium)
Weakness
- CWE-674: Uncontrolled Recursion
- CWE-400: Uncontrolled Resource Consumption (secondary)
Suggested fix
Best: Replace recursion with an iterative walk (os.walk()), or add a depth cap (~256).
Minimum: Catch RecursionError and skip that subtree:
try:
self.list_files(path, files, dirs)
except (OSError, RecursionError): # add RecursionError here
continue
This is a one-line fix that turns the crash into a skip.
Attachments
| File | What it is |
|---|---|
poc_deepnest.py | PoC script — creates deep tree and crashes discover() |
bug1_traceback.txt | Real crash traceback (proof) |
bug1_finder_frames.txt | Same crash, library frames extracted (readable summary) |
Impact
- Availability: uncaught exception kills Weblate's discovery task
- Repeated failures: every retry crashes again
- Component stuck: import/update/hook activity stops for affected component
- Aggregate CPU DoS: possible with many nested repos