Skip to content

bug: Mutable default arguments in Config methods may cause cross-session state leakage #130

@AaryanCode69

Description

@AaryanCode69

Labels

bug, python-antipattern, config, rate-limiting


Summary

Two methods in the Config class use mutable default arguments (dict and list), which are evaluated only once at function definition time in Python.

If these objects are ever mutated in-place, the mutation persists across subsequent calls that rely on the default value. This can unintentionally leak state between independent sessions.

Python documentation:
https://docs.python.org/3/faq/programming.html#why-are-default-values-shared-between-objects


Affected Locations

File:

src/util/config_yml/__init__.py

1️⃣ get_messages() (line ~40)

def get_messages(
    self,
    user_id: str | None = None,
    event: TriggerEvent | None = None,
    after_messages: int | None = None,
    last_messages: dict[str, str] = {},   # mutable default
) -> dict[str, str]:

2️⃣ get_message_rate_usage_limited() (line ~57)

def get_message_rate_usage_limited(
    self,
    user_id: str | None = None,
    message_times_queue: list[str] = [],   # mutable default
) -> MessageRate | None:

Current Behavior

Python creates the default {} and [] once at function definition time.

If the function mutates these objects, the mutation persists across future calls.

Example:

def example(data=[]):
    data.append("x")
    return data

example()  # ['x']
example()  # ['x', 'x']
example()  # ['x', 'x', 'x']

Why This Matters

1️⃣ Static messaging (get_messages)

The last_messages dictionary is used to filter messages per user.
A shared default dictionary could cause one user’s message history to affect another user’s filtering logic.

2️⃣ Rate limiting (get_message_rate_usage_limited)

message_times_queue tracks timestamps used for rate limiting.
A shared list could accumulate timestamps across users, potentially causing:

  • premature throttling
  • inaccurate rate-limit enforcement

Current Impact

Based on current usage:

src/util/chainlit_helpers.py

(lines ~98 and ~137)

Both callers currently pass explicit arguments, meaning the issue is latent and not triggered today.

However, any future call relying on the default value could introduce subtle cross-session state leakage.


Static Analysis / Linter Warnings

This pattern is flagged by several Python linters:

  • pylintW0102: dangerous-default-value
  • flake8-bugbearB006
  • ruffB006

Proposed Fix

Use None as the default and instantiate a fresh object inside the function.

Fix 1 — get_messages

def get_messages(
    self,
    user_id: str | None = None,
    event: TriggerEvent | None = None,
    after_messages: int | None = None,
-   last_messages: dict[str, str] = {},
+   last_messages: dict[str, str] | None = None,
) -> dict[str, str]:

+   if last_messages is None:
+       last_messages = {}

Fix 2 — get_message_rate_usage_limited

def get_message_rate_usage_limited(
    self,
    user_id: str | None = None,
-   message_times_queue: list[str] = [],
+   message_times_queue: list[str] | None = None,
) -> MessageRate | None:

+   if message_times_queue is None:
+       message_times_queue = []

Compatibility

This change is fully backward compatible, since:

  • callers can still pass explicit dict / list
  • only the internal default initialization changes

Files to Update

src/util/config_yml/__init__.py

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions