BUG-HUNT: correctness - redact_dict returns shallow copy when show_secrets=True allowing aliased mutation #7769

Open
opened 2026-04-12 03:30:21 +00:00 by HAL9000 · 3 comments
Owner

Bug Report: Correctness — redact_dict Returns Shallow Copy When show_secrets=True

Severity Assessment

  • Impact: When show_secrets=True, redact_dict returns dict(data) — a shallow copy. Nested dicts and lists are shared references between the returned copy and the original. A caller mutating the returned dict can inadvertently mutate the original data structure, causing silent data corruption or unexpected state changes.
  • Likelihood: Low-Medium — only triggered when show_secrets=True (debug/development mode), but the bug is silent and hard to detect.
  • Priority: Medium

Location

  • File: src/cleveragents/shared/redaction.py
  • Function/Class: redact_dict
  • Lines: 163–167

Description

The show_secrets=True branch returns dict(data) which performs only a shallow copy of the top-level dict. Nested structures (dicts, lists) within data are the same objects in both the original and the returned copy. This violates the expectation that redact_dict returns an independent copy of the data.

Evidence

# redaction.py lines 163–167
def redact_dict(data, *, show_secrets=None):
    if show_secrets is None:
        show_secrets = get_show_secrets()
    if show_secrets:
        return dict(data)   # BUG: shallow copy — nested objects are aliased!
    return _redact_dict_inner(data)

Demonstration:

original = {"config": {"nested": "value"}, "items": [1, 2, 3]}
result = redact_dict(original, show_secrets=True)

result["config"]["nested"] = "MUTATED"
print(original["config"]["nested"])  # "MUTATED" — original is corrupted!

Note: _redact_dict_inner (used when show_secrets=False) builds a new dict recursively, so nested objects are not shared. The two code paths have inconsistent copy semantics.

Expected Behavior

redact_dict should return an independent copy in both branches (with and without secrets). Callers should be able to mutate the returned dict without affecting the original.

Actual Behavior

When show_secrets=True, the returned dict shares nested mutable structures with the original input dict. Mutations to nested objects in the result propagate to the original.

Suggested Fix

Use copy.deepcopy for the show_secrets=True branch:

import copy

def redact_dict(data, *, show_secrets=None):
    if show_secrets is None:
        show_secrets = get_show_secrets()
    if show_secrets:
        return copy.deepcopy(data)   # deep copy to avoid aliasing
    return _redact_dict_inner(data)

Category

correctness

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Correctness — `redact_dict` Returns Shallow Copy When `show_secrets=True` ### Severity Assessment - **Impact**: When `show_secrets=True`, `redact_dict` returns `dict(data)` — a shallow copy. Nested dicts and lists are shared references between the returned copy and the original. A caller mutating the returned dict can inadvertently mutate the original data structure, causing silent data corruption or unexpected state changes. - **Likelihood**: Low-Medium — only triggered when `show_secrets=True` (debug/development mode), but the bug is silent and hard to detect. - **Priority**: Medium ### Location - **File**: `src/cleveragents/shared/redaction.py` - **Function/Class**: `redact_dict` - **Lines**: 163–167 ### Description The `show_secrets=True` branch returns `dict(data)` which performs only a **shallow copy** of the top-level dict. Nested structures (dicts, lists) within `data` are the same objects in both the original and the returned copy. This violates the expectation that `redact_dict` returns an independent copy of the data. ### Evidence ```python # redaction.py lines 163–167 def redact_dict(data, *, show_secrets=None): if show_secrets is None: show_secrets = get_show_secrets() if show_secrets: return dict(data) # BUG: shallow copy — nested objects are aliased! return _redact_dict_inner(data) ``` Demonstration: ```python original = {"config": {"nested": "value"}, "items": [1, 2, 3]} result = redact_dict(original, show_secrets=True) result["config"]["nested"] = "MUTATED" print(original["config"]["nested"]) # "MUTATED" — original is corrupted! ``` Note: `_redact_dict_inner` (used when `show_secrets=False`) builds a new dict recursively, so nested objects are **not** shared. The two code paths have inconsistent copy semantics. ### Expected Behavior `redact_dict` should return an independent copy in both branches (with and without secrets). Callers should be able to mutate the returned dict without affecting the original. ### Actual Behavior When `show_secrets=True`, the returned dict shares nested mutable structures with the original input dict. Mutations to nested objects in the result propagate to the original. ### Suggested Fix Use `copy.deepcopy` for the `show_secrets=True` branch: ```python import copy def redact_dict(data, *, show_secrets=None): if show_secrets is None: show_secrets = get_show_secrets() if show_secrets: return copy.deepcopy(data) # deep copy to avoid aliasing return _redact_dict_inner(data) ``` ### Category correctness ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-12 03:44:48 +00:00
Author
Owner

Verified — Bug: redact_dict returns shallow copy allowing aliased mutation. MoSCoW: Should-have. Priority: Medium.


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

✅ **Verified** — Bug: redact_dict returns shallow copy allowing aliased mutation. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: redact_dict returns shallow copy allowing aliased mutation. MoSCoW: Should-have. Priority: Medium.


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

✅ **Verified** — Bug: redact_dict returns shallow copy allowing aliased mutation. MoSCoW: Should-have. Priority: Medium. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Bug: redact_dict returns shallow copy allowing aliased mutation. MoSCoW: Should-have. Priority: Medium.


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

✅ **Verified** — Bug: redact_dict returns shallow copy allowing aliased mutation. MoSCoW: Should-have. Priority: Medium. --- **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.

Dependencies

No dependencies set.

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