fix(auth): replace non-atomic exists+del with getdel to prevent TOCTOU race#84
Merged
DeFiVC merged 1 commit intoJul 23, 2026
Conversation
…U race The verifyChallenge method checked redis.exists() and later called redis.del() after a long processing window (XDR decoding, signature verification, database queries). Two concurrent verify requests for the same address could both pass the exists check before either deleted the key, allowing double JWT issuance. Replace the separate exists/del calls with a single redis.getdel() that atomically retrieves and deletes the challenge in one operation. This ensures the challenge is consumed exactly once. Closes ChainLearnOfficial#72
DeFiVC
approved these changes
Jul 23, 2026
DeFiVC
left a comment
Contributor
There was a problem hiding this comment.
Review
Approve
All CI checks green. The fix correctly replaces the non-atomic + pair with , which atomically retrieves and deletes the challenge in a single operation. This eliminates the TOCTOU race condition where concurrent requests could both pass the existence check.
What's good:
- Minimal, focused change — exactly what the issue asked for
- Correct use of for atomic check-and-delete
- Tests properly updated to mock the new behavior
- Single clean commit with descriptive message
No issues found. Ready to merge.
DeFiVC
approved these changes
Jul 23, 2026
DeFiVC
left a comment
Contributor
There was a problem hiding this comment.
Review
Approve
All CI checks green. The fix correctly replaces the non-atomic redis.exists() + redis.del() pair with redis.getdel(), which atomically retrieves and deletes the challenge in a single operation. This eliminates the TOCTOU race condition where concurrent requests could both pass the existence check.
What is good:
- Minimal, focused change - exactly what the issue asked for
- Correct use of getdel for atomic check-and-delete
- Tests properly updated to mock the new behavior
- Single clean commit with descriptive message
No issues found. Ready to merge.
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #72
Type of Change
Summary
The
verifyChallengemethod inauth.service.tshad a TOCTOU race condition whereredis.exists()was called at line 81 andredis.del()at line 150, with a long processing window in between (XDR decoding, signature verification, database queries). Two concurrent verify requests for the same address could both pass theexistscheck before either deleted the key, enabling double JWT issuance from a single SEP-10 challenge.This PR replaces the non-atomic
exists/delpair with a singleredis.getdel()call that atomically retrieves and deletes the challenge, ensuring each challenge is consumed exactly once.Motivation / Context
Fixes #72 — an attacker can obtain multiple JWTs from a single SEP-10 challenge, enabling token multiplication. The issue was identified as a security concern with clear acceptance criteria: two concurrent verify requests for the same challenge should only succeed once.
Detailed Changes
src/modules/auth/auth.service.ts:redis.exists()check +redis.del()cleanup withredis.getdel()at the start ofverifyChallengegetdelatomically returns the value if the key exists and deletes it — only the first concurrent request will receive the challenge dataredis.del()call since the challenge is now consumed atomically at entrygetdel()returns null, the existing "Challenge expired or not found" error is throwntests/unit/services/sep10-auth.test.ts:getdelinstead ofexists/delmockRedis.getdelappropriatelyTesting
tests/unit/services/sep10-auth.test.tsasync-lockmodule (unrelated to this change)