Skip to content

Security: Pairing lockout can be bypassed by approving a valid code during lockout #10195

@stephenlzc

Description

@stephenlzc

The DM pairing system's brute-force protection can be bypassed because PairingStore.approve_code() does not check whether the platform is currently locked out before validating a pairing code.

Impact

After 5 failed approval attempts, a platform enters a 1-hour lockout (_is_locked_out() == True). However, an attacker who has obtained or guessed a pending valid pairing code can still call approve_code() successfully during the lockout period, gaining unauthorized access to the bot. This effectively nullifies the lockout mechanism.

Steps to Reproduce

from gateway.pairing import PairingStore, MAX_FAILED_ATTEMPTS

store = PairingStore()
valid_code = store.generate_code("telegram", "attacker", "Attacker")

# Trigger lockout with wrong codes
for _ in range(MAX_FAILED_ATTEMPTS):
    result = store.approve_code("telegram", "WRONGCODE")
    assert result is None

assert store._is_locked_out("telegram") is True  # Platform is locked out

# But the valid code is still accepted!
result = store.approve_code("telegram", valid_code)
print(result)  # {'user_id': 'attacker', 'user_name': 'Attacker'}
print(store.is_approved("telegram", "attacker"))  # True

Expected Behavior

approve_code("telegram", valid_code) should return None while the platform is locked out, and the user must not be added to the approved list.

Actual Behavior

approve_code() succeeds and adds the user to the approved list even during lockout.

Root Cause

In gateway/pairing.py, the approve_code() method calls _cleanup_expired() and then immediately looks up the code in pending, but it never calls _is_locked_out(platform). The lockout check only exists in generate_code(), so it only blocks new code generation but not code approval.

Suggested Fix

Add a lockout guard at the beginning of approve_code() (after _cleanup_expired and before looking up the pending code):

with self._lock:
    self._cleanup_expired(platform)
    code = code.upper().strip()

    # Prevent brute-force bypass during lockout
    if self._is_locked_out(platform):
        return None

    pending = self._load_json(self._pending_path(platform))
    ...

Environment

  • Repository: NousResearch/hermes-agent
  • File: gateway/pairing.py
  • Method: PairingStore.approve_code()
  • Commit tested: main branch (latest as of 2026-04-15)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical — data loss, security, crash looparea/authAuthentication, OAuth, credential poolscomp/gatewayGateway runner, session dispatch, deliverytype/securitySecurity vulnerability or hardening

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions