security: fix O(n) timing attack in findByToken #52

Open
Charon wants to merge 1 commit from feat/fix-token-timing-attack into dev
Member

Summary

Fixes #9 — eliminates O(n) timing attack surface in WorkspaceInvitation::findByToken.

  • Adds a token_hash column (SHA-256 of the raw token) to workspace_invitations for O(1) indexed SQL lookup
  • Rewrites findByToken() and findPendingByToken() to query by token_hash first, then verify with Hash::check() (single bcrypt call instead of up to 1000)
  • Adds updating model event so re-invites also populate token_hash
  • Updates HashInvitationTokens command to backfill token_hash for existing rows
  • Includes migration for the new nullable indexed column

Security

The previous implementation loaded up to 1000 invitation records and ran Hash::check() (bcrypt, ~100ms each) sequentially against each. This created:

  1. Timing attack — response time leaked how many records were scanned before a match
  2. DoS vector — a single request could force ~100 seconds of CPU-bound bcrypt work

The fix uses SHA-256 for fast candidate lookup (non-sensitive, just an index key) and bcrypt only once for final verification.

Test plan

  • Existing WorkspaceInvitationTest tests pass (findByToken, findPendingByToken, acceptInvitation, verify token)
  • Run migration on dev database
  • Verify new invitations populate both token and token_hash columns
  • Verify re-invite flow updates token_hash when token is regenerated

🤖 Generated with Claude Code

## Summary Fixes #9 — eliminates O(n) timing attack surface in `WorkspaceInvitation::findByToken`. - Adds a `token_hash` column (SHA-256 of the raw token) to `workspace_invitations` for O(1) indexed SQL lookup - Rewrites `findByToken()` and `findPendingByToken()` to query by `token_hash` first, then verify with `Hash::check()` (single bcrypt call instead of up to 1000) - Adds `updating` model event so re-invites also populate `token_hash` - Updates `HashInvitationTokens` command to backfill `token_hash` for existing rows - Includes migration for the new nullable indexed column ## Security The previous implementation loaded up to 1000 invitation records and ran `Hash::check()` (bcrypt, ~100ms each) sequentially against each. This created: 1. **Timing attack** — response time leaked how many records were scanned before a match 2. **DoS vector** — a single request could force ~100 seconds of CPU-bound bcrypt work The fix uses SHA-256 for fast candidate lookup (non-sensitive, just an index key) and bcrypt only once for final verification. ## Test plan - [ ] Existing `WorkspaceInvitationTest` tests pass (findByToken, findPendingByToken, acceptInvitation, verify token) - [ ] Run migration on dev database - [ ] Verify new invitations populate both `token` and `token_hash` columns - [ ] Verify re-invite flow updates `token_hash` when token is regenerated 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Charon added 1 commit 2026-03-24 13:06:23 +00:00
Add a SHA-256 token_hash lookup column to workspace_invitations so that
findByToken and findPendingByToken can locate the candidate row with a
single indexed SQL query instead of loading up to 1000 rows and running
bcrypt against each one sequentially.

The bcrypt hash in the token column is still verified after the O(1)
lookup, preserving the existing security guarantee while eliminating
both the timing side-channel and the performance bottleneck.

Changes:
- Migration to add nullable indexed token_hash column
- Model booted() creating/updating events compute SHA-256 alongside bcrypt
- findByToken/findPendingByToken rewritten to WHERE token_hash then Hash::check
- HashInvitationTokens command updated to populate token_hash for existing rows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/fix-token-timing-attack:feat/fix-token-timing-attack
git checkout feat/fix-token-timing-attack

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git checkout dev
git merge --no-ff feat/fix-token-timing-attack
git checkout feat/fix-token-timing-attack
git rebase dev
git checkout dev
git merge --ff-only feat/fix-token-timing-attack
git checkout feat/fix-token-timing-attack
git rebase dev
git checkout dev
git merge --no-ff feat/fix-token-timing-attack
git checkout dev
git merge --squash feat/fix-token-timing-attack
git checkout dev
git merge --ff-only feat/fix-token-timing-attack
git checkout dev
git merge feat/fix-token-timing-attack
git push origin dev
Sign in to join this conversation.
No description provided.