Skip to content

fix(account): stop sharing one additional_data dict across token calls - #39816

Open
ErenAta16 wants to merge 2 commits into
langgenius:mainfrom
ErenAta16:fix/account-token-additional-data-mutable-default
Open

fix(account): stop sharing one additional_data dict across token calls#39816
ErenAta16 wants to merge 2 commits into
langgenius:mainfrom
ErenAta16:fix/account-token-additional-data-mutable-default

Conversation

@ErenAta16

Copy link
Copy Markdown

Summary

Fixes #39815.

generate_reset_password_token, generate_email_register_token and generate_owner_transfer_token took additional_data as a mutable default and then wrote the 6-digit code into it:

additional_data: dict[str, Any] = {},
...
additional_data["code"] = code
token = TokenManager.generate_token(..., additional_data=additional_data)

That dict is created once at import time. SERVER_WORKER_CLASS defaults to gevent, and generate_token reads additional_data after a Redis round-trip, so a second greenlet can overwrite code in between. The caller returns its own local code to be emailed while the token payload keeps the other request's — the user's emailed code then fails verification. A caller-supplied dict was mutated too, and the default retained the last code for the process lifetime.

This takes None as the default and copies any caller-supplied dict before writing the code. Nothing else in the signature or return value changes.

Running the new tests against main (three parametrisations each):

test before after
test_additional_data_default_is_not_mutable fail pass
test_caller_dict_is_not_mutated fail pass
test_concurrent_calls_keep_their_own_code fail pass

The concurrency one hands control from the first caller to the second at the point generate_token would hit Redis, so it fails deterministically rather than by timing:

AssertionError: assert stored == returned
  Differing items:
  {'first@example.com': '906060'} != {'first@example.com': '517404'}

9 failed on main, 9 passed with the change.

The rest of tests/unit_tests/services/test_account_service.py is unchanged at 116 passed, 1 failed; that one failure (test_create_account_registration_disabled) also fails on a clean checkout, so it is not from this change. ruff check and ruff format --check are clean on both files. I could not run tests/unit_tests/controllers/console/auth/ locally — collection fails there on a stock env with AttributeError: type object 'Swagger' has no attribute 'schema_from_parameter', which looks like the pinned flask-restx git rev rather than anything here.

Note: #39294 also changes these three signatures (adding account_id). If that lands first this becomes a small rebase.

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran make lint && make type-check (backend) and cd web && pnpm exec vp staged (frontend) to appease the lint gods

I ran ruff check and ruff format --check on the two changed files rather than the full make lint && make type-check, which needs the complete dependency set.

From Claude Code

generate_reset_password_token, generate_email_register_token and
generate_owner_transfer_token took additional_data as a mutable default and
wrote the 6-digit code into it. Under gevent workers that one dict is shared
across greenlets, and TokenManager.generate_token reads it after a Redis
round-trip, so an overlapping request can store another request's code in the
token payload while the caller emails its own. A caller-supplied dict was also
mutated, and the default kept the last code for the process lifetime.

Take None as the default and copy any caller dict before writing the code.
@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verification code can leak between concurrent requests via mutable default additional_data in AccountService

1 participant