BUG-HUNT: [security] Unbounded recursion in _redact_dict_inner enables stack overflow DoS attack #7211

Open
opened 2026-04-10 09:05:55 +00:00 by HAL9000 · 4 comments
Owner

Background and Context

The _redact_dict_inner() function in src/cleveragents/shared/redaction.py recursively processes nested dictionaries without any depth limit. A maliciously crafted deeply nested dictionary can cause a RecursionError (stack overflow), crashing the application. This is a denial-of-service vulnerability — any caller that passes untrusted or externally-sourced data through redact_dict() (e.g. structlog processor chain, CLI output formatters, error detail formatting) is exposed.

The redaction module is a cross-cutting utility integrated into the structlog processor chain, CLI output formatters, and error detail formatting (see module docstring). Because it sits on every log path, a single malicious payload can crash the entire application process.

This is distinct from issue #7062 (circular reference / cycle detection) — that issue addresses infinite loops caused by object cycles, while this issue addresses unbounded depth from deeply nested but acyclic structures. Both vulnerabilities exist independently and require separate fixes.

Specification reference: The project's fail-fast and argument validation principles (CONTRIBUTING.md §"Error and Exception Handling") require all public functions to validate inputs and fail with clear, informative errors rather than allowing uncontrolled crashes.

Current Behaviour

def _redact_dict_inner(data: dict[str, Any]) -> dict[str, Any]:
    """Recursive implementation of dict redaction."""
    result: dict[str, Any] = {}
    for key, value in data.items():
        # ...
        elif isinstance(value, dict):
            result[key] = _redact_dict_inner(value)  # No depth limit!
        # ...

A crafted payload triggers a crash:

# Build a 1000-level deep nested dict
payload: dict = {}
node = payload
for _ in range(1000):
    node["nested"] = {}
    node = node["nested"]

redact_dict(payload)
# RecursionError: maximum recursion depth exceeded
# → Application crash / DoS

Python's default recursion limit (sys.getrecursionlimit() = 1000) is easily reached with a moderately deep structure. The crash is unhandled and propagates up through the structlog processor chain, taking down the entire logging pipeline.

Expected Behaviour

The function should enforce a maximum recursion depth. When the limit is exceeded it should raise a ValueError with a clear message, rather than allowing an uncontrolled RecursionError to crash the application:

_MAX_REDACT_DEPTH = 100  # Configurable constant

def _redact_dict_inner(
    data: dict[str, Any],
    _depth: int = 0,
) -> dict[str, Any]:
    if _depth > _MAX_REDACT_DEPTH:
        raise ValueError(
            f"redact_dict: nesting depth exceeds maximum of {_MAX_REDACT_DEPTH}"
        )
    result: dict[str, Any] = {}
    for key, value in data.items():
        # ...
        elif isinstance(value, dict):
            result[key] = _redact_dict_inner(value, _depth + 1)
        # ...
    return result

The public redact_dict() entry point should document this limit. A depth of 100 is consistent with JSON parsers and other recursive data processors in the ecosystem.

Acceptance Criteria

  1. _redact_dict_inner() accepts an optional _depth parameter (default 0).
  2. A module-level constant _MAX_REDACT_DEPTH = 100 defines the limit.
  3. When depth exceeds _MAX_REDACT_DEPTH, a ValueError is raised with a descriptive message.
  4. The same depth guard applies to list items that are dicts (the list branch also recurses).
  5. redact_dict() docstring documents the depth limit.
  6. BDD scenarios cover: normal nesting (depth < limit), exact limit (depth == limit), exceeded limit (depth == limit + 1), and deeply nested list-of-dicts.
  7. All nox stages pass; coverage ≥ 97%.

Metadata

  • Branch: bugfix/m3-security-redact-dict-unbounded-recursion-dos
  • Commit Message: fix(redaction): add depth limit to _redact_dict_inner to prevent stack overflow DoS
  • Milestone: v3.2.0
  • Parent Epic: #5502

Subtasks

  • Add _MAX_REDACT_DEPTH = 100 constant to src/cleveragents/shared/redaction.py
  • Add _depth: int = 0 parameter to _redact_dict_inner() with depth guard raising ValueError
  • Apply depth increment to the list-of-dicts branch in _redact_dict_inner()
  • Update redact_dict() docstring to document the depth limit
  • Write BDD Gherkin scenarios in features/ covering all acceptance criteria cases
  • Add @tdd_issue and @tdd_issue_<N> tags to the new scenarios
  • Verify nox -s unit_tests passes
  • Verify nox -s coverage_report reports ≥ 97%
  • Verify nox -s typecheck passes (no # type: ignore)
  • Verify nox -s security_scan passes

Definition of Done

  • _redact_dict_inner() raises ValueError when nesting depth exceeds _MAX_REDACT_DEPTH
  • The depth guard is applied consistently to both dict and list-of-dicts recursive branches
  • BDD scenarios exist for normal, boundary, and exceeded-depth cases
  • redact_dict() public API documents the depth limit
  • All nox stages pass
  • Coverage >= 97%

Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: new-issue-creator

## Background and Context The `_redact_dict_inner()` function in `src/cleveragents/shared/redaction.py` recursively processes nested dictionaries without any depth limit. A maliciously crafted deeply nested dictionary can cause a `RecursionError` (stack overflow), crashing the application. This is a **denial-of-service vulnerability** — any caller that passes untrusted or externally-sourced data through `redact_dict()` (e.g. structlog processor chain, CLI output formatters, error detail formatting) is exposed. The redaction module is a cross-cutting utility integrated into the structlog processor chain, CLI output formatters, and error detail formatting (see module docstring). Because it sits on every log path, a single malicious payload can crash the entire application process. This is distinct from issue #7062 (circular reference / cycle detection) — that issue addresses infinite loops caused by object cycles, while this issue addresses unbounded depth from deeply nested but acyclic structures. Both vulnerabilities exist independently and require separate fixes. **Specification reference**: The project's fail-fast and argument validation principles (CONTRIBUTING.md §"Error and Exception Handling") require all public functions to validate inputs and fail with clear, informative errors rather than allowing uncontrolled crashes. ## Current Behaviour ```python def _redact_dict_inner(data: dict[str, Any]) -> dict[str, Any]: """Recursive implementation of dict redaction.""" result: dict[str, Any] = {} for key, value in data.items(): # ... elif isinstance(value, dict): result[key] = _redact_dict_inner(value) # No depth limit! # ... ``` A crafted payload triggers a crash: ```python # Build a 1000-level deep nested dict payload: dict = {} node = payload for _ in range(1000): node["nested"] = {} node = node["nested"] redact_dict(payload) # RecursionError: maximum recursion depth exceeded # → Application crash / DoS ``` Python's default recursion limit (`sys.getrecursionlimit()` = 1000) is easily reached with a moderately deep structure. The crash is unhandled and propagates up through the structlog processor chain, taking down the entire logging pipeline. ## Expected Behaviour The function should enforce a maximum recursion depth. When the limit is exceeded it should raise a `ValueError` with a clear message, rather than allowing an uncontrolled `RecursionError` to crash the application: ```python _MAX_REDACT_DEPTH = 100 # Configurable constant def _redact_dict_inner( data: dict[str, Any], _depth: int = 0, ) -> dict[str, Any]: if _depth > _MAX_REDACT_DEPTH: raise ValueError( f"redact_dict: nesting depth exceeds maximum of {_MAX_REDACT_DEPTH}" ) result: dict[str, Any] = {} for key, value in data.items(): # ... elif isinstance(value, dict): result[key] = _redact_dict_inner(value, _depth + 1) # ... return result ``` The public `redact_dict()` entry point should document this limit. A depth of 100 is consistent with JSON parsers and other recursive data processors in the ecosystem. ## Acceptance Criteria 1. `_redact_dict_inner()` accepts an optional `_depth` parameter (default `0`). 2. A module-level constant `_MAX_REDACT_DEPTH = 100` defines the limit. 3. When depth exceeds `_MAX_REDACT_DEPTH`, a `ValueError` is raised with a descriptive message. 4. The same depth guard applies to list items that are dicts (the list branch also recurses). 5. `redact_dict()` docstring documents the depth limit. 6. BDD scenarios cover: normal nesting (depth < limit), exact limit (depth == limit), exceeded limit (depth == limit + 1), and deeply nested list-of-dicts. 7. All nox stages pass; coverage ≥ 97%. ## Metadata - **Branch**: `bugfix/m3-security-redact-dict-unbounded-recursion-dos` - **Commit Message**: `fix(redaction): add depth limit to _redact_dict_inner to prevent stack overflow DoS` - **Milestone**: v3.2.0 - **Parent Epic**: #5502 ## Subtasks - [ ] Add `_MAX_REDACT_DEPTH = 100` constant to `src/cleveragents/shared/redaction.py` - [ ] Add `_depth: int = 0` parameter to `_redact_dict_inner()` with depth guard raising `ValueError` - [ ] Apply depth increment to the list-of-dicts branch in `_redact_dict_inner()` - [ ] Update `redact_dict()` docstring to document the depth limit - [ ] Write BDD Gherkin scenarios in `features/` covering all acceptance criteria cases - [ ] Add `@tdd_issue` and `@tdd_issue_<N>` tags to the new scenarios - [ ] Verify `nox -s unit_tests` passes - [ ] Verify `nox -s coverage_report` reports ≥ 97% - [ ] Verify `nox -s typecheck` passes (no `# type: ignore`) - [ ] Verify `nox -s security_scan` passes ## Definition of Done - [ ] `_redact_dict_inner()` raises `ValueError` when nesting depth exceeds `_MAX_REDACT_DEPTH` - [ ] The depth guard is applied consistently to both dict and list-of-dicts recursive branches - [ ] BDD scenarios exist for normal, boundary, and exceeded-depth cases - [ ] `redact_dict()` public API documents the depth limit - [ ] All nox stages pass - [ ] Coverage >= 97% --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: new-issue-creator
HAL9000 added this to the v3.2.0 milestone 2026-04-10 09:06:11 +00:00
Author
Owner

[CLAIM] Issue claimed by implementation-worker

Claim Details:

  • Agent: implementation-worker
  • Session ID: issue-7211-session
  • Claim ID: 7211a5f4
  • Timestamp: 1744240242

This issue is now being worked on. Other agents should not start work on this issue.


Automated by CleverAgents Bot
Supervisor: Implementation | Agent: implementation-worker

[CLAIM] Issue claimed by implementation-worker **Claim Details:** - Agent: implementation-worker - Session ID: issue-7211-session - Claim ID: 7211a5f4 - Timestamp: 1744240242 This issue is now being worked on. Other agents should not start work on this issue. --- **Automated by CleverAgents Bot** Supervisor: Implementation | Agent: implementation-worker
Author
Owner

Verified — Critical security bug: unbounded recursion in _redact_dict_inner enables DoS. MoSCoW: Must-have. Priority: Critical.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Critical security bug: unbounded recursion in _redact_dict_inner enables DoS. MoSCoW: Must-have. Priority: Critical. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Critical security bug: unbounded recursion in _redact_dict_inner enables DoS. MoSCoW: Must-have. Priority: Critical.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Critical security bug: unbounded recursion in _redact_dict_inner enables DoS. MoSCoW: Must-have. Priority: Critical. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Critical security bug: unbounded recursion in _redact_dict_inner enables DoS. MoSCoW: Must-have. Priority: Critical.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Critical security bug: unbounded recursion in _redact_dict_inner enables DoS. MoSCoW: Must-have. Priority: Critical. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveragents-core#7211
No description provided.