Skip to content

Fix shared-state/thread-safety bug in Payment.capture() (mutable default argument)#331

Open
vinothksekar wants to merge 1 commit into
razorpay:masterfrom
vinothksekar:fix/capture-mutable-default-thread-safety
Open

Fix shared-state/thread-safety bug in Payment.capture() (mutable default argument)#331
vinothksekar wants to merge 1 commit into
razorpay:masterfrom
vinothksekar:fix/capture-mutable-default-thread-safety

Conversation

@vinothksekar

Copy link
Copy Markdown

Summary

Payment.capture() uses a mutable default argument (data={}) and mutates it (data['amount'] = amount). Because the default {} is created once and shared across every call, every Client instance, and every thread, captured state leaks between calls and — on a Client shared across threads (the documented usage) — concurrent captures race on the shared dict.

This was previously flagged in #134 and silenced with a # nosemgrep suppression rather than fixed.

Impact

  • State leak: after client.payment.capture(id, 4242) with no data, the function's default is permanently {'amount': 4242}.
  • Thread-safety / correctness: with a shared client, two threads calling capture() concurrently can post one payment's amount for a different payment. In a minimal 2-thread reproduction (25 captures each, amounts 100 vs 999), 47 of 50 calls posted the wrong amount. After the fix: 0 of 50.

Fix

Use data=None and build a fresh dict per call (this also avoids mutating a caller-supplied dict). The now-unnecessary # nosemgrep suppression is removed.

Tests

Added two regression tests to tests/test_client_payment.py:

  • test_payment_capture_does_not_leak_state_into_default
  • test_payment_capture_does_not_mutate_caller_dict

Full suite: 159 passed, 1 skipped.

Refs #134.

capture() used a mutable default argument (data={}) and mutated it
(data['amount'] = amount). The default dict is created once and shared
across every call, every Client instance and every thread, so:

* state leaks between calls: after capture(id, 4242) with no data, the
  function's default is permanently {'amount': 4242}; and
* on a Client shared across threads (the documented usage) concurrent
  capture() calls race on the shared dict and can post one payment's
  amount for another payment.

Fixed by using data=None and building a fresh dict per call (also avoids
mutating a caller-supplied dict). Removes the # nosemgrep suppression
that was hiding the mutable-default warning.

Refs razorpay#134.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant