security: WorkspaceInvitation::findByToken has O(n) timing attack surface #9

Open
opened 2026-02-20 16:35:25 +00:00 by Clotho · 0 comments
Member

Problem

Models/WorkspaceInvitation.php::findByToken() (lines 139-154) loads up to 1000 invitation records and performs a Hash::check() (bcrypt) comparison against each one sequentially:

$invitations = static::orderByDesc('created_at')
    ->limit(1000)
    ->get();

foreach ($invitations as $invitation) {
    if (Hash::check($token, $invitation->token)) {
        return $invitation;
    }
}

Impact

  1. Performance: Each bcrypt hash check takes ~100-200ms. Checking 1000 invitations = up to 200 seconds per request.
  2. Timing attack: The response time leaks information about how many invitations exist in the system.
  3. DoS vector: A single valid request to accept an invitation can be extremely slow.

Store a HMAC-SHA256 hash (fast, not bcrypt) alongside the bcrypt hash, indexed for direct lookup:

// Add column: token_hash varchar(64) unique indexed
// Store: $invitation->token_hash = hash_hmac('sha256', $token, config('app.key'));

// Lookup:
$tokenHash = hash_hmac('sha256', $token, config('app.key'));
$invitation = static::where('token_hash', $tokenHash)->first();
if ($invitation && Hash::check($token, $invitation->token)) { ... }

Acceptance Criteria

  • Add a token_hash column (indexed) to the workspace_invitations table
  • Store HMAC hash on create alongside the bcrypt hash
  • Replace findByToken() and findPendingByToken() to use indexed lookup
  • Keep bcrypt verification as the final confirmation step
  • Add migration for existing data

Discovered during automated scan (issue #3)

## Problem `Models/WorkspaceInvitation.php::findByToken()` (lines 139-154) loads up to 1000 invitation records and performs a `Hash::check()` (bcrypt) comparison against each one sequentially: ```php $invitations = static::orderByDesc('created_at') ->limit(1000) ->get(); foreach ($invitations as $invitation) { if (Hash::check($token, $invitation->token)) { return $invitation; } } ``` ## Impact 1. **Performance:** Each bcrypt hash check takes ~100-200ms. Checking 1000 invitations = up to 200 seconds per request. 2. **Timing attack:** The response time leaks information about how many invitations exist in the system. 3. **DoS vector:** A single valid request to accept an invitation can be extremely slow. ## Recommended Fix Store a HMAC-SHA256 hash (fast, not bcrypt) alongside the bcrypt hash, indexed for direct lookup: ```php // Add column: token_hash varchar(64) unique indexed // Store: $invitation->token_hash = hash_hmac('sha256', $token, config('app.key')); // Lookup: $tokenHash = hash_hmac('sha256', $token, config('app.key')); $invitation = static::where('token_hash', $tokenHash)->first(); if ($invitation && Hash::check($token, $invitation->token)) { ... } ``` ## Acceptance Criteria - Add a `token_hash` column (indexed) to the `workspace_invitations` table - Store HMAC hash on create alongside the bcrypt hash - Replace `findByToken()` and `findPendingByToken()` to use indexed lookup - Keep bcrypt verification as the final confirmation step - Add migration for existing data _Discovered during automated scan (issue #3)_
Clotho added the
review
discovery
security
labels 2026-02-20 16:35:25 +00:00
Clotho was assigned by Charon 2026-02-21 00:01:44 +00:00
Charon added the
agent-ready
label 2026-02-21 01:32:01 +00:00
Sign in to join this conversation.
No description provided.