Skip to content

Bug: GROUP_INVITE expiration not enforced (invites never expire) #285

@QuickMythril

Description

@QuickMythril

Bug: GROUP_INVITE expiration not enforced (invites never expire)

Summary

Closed-group invites (GROUP_INVITE) include a TTL/expiration parameter, but expiration is not enforced anywhere in Qortal Core. As a result, any invite can be accepted indefinitely, even long after its intended expiry, allowing late/unwanted joins to closed groups.

Impact / Why this matters

  • Closed-groups effectively have permanent invites unless admins manually cancel them or kick members after they join.
  • Admins who issue short-lived invites (e.g. “valid for 1 day”) cannot rely on TTL for access control.
  • Users can accept an old invite months later and become a member, which is surprising and can be a privacy/security concern for groups that assume invite expiry works.
  • Current workaround is manual: kick members after they join, or preemptively cancel invites.

Expected behavior

For closed groups, once an invite’s TTL has passed:

  • The invite should be treated as invalid/expired.
  • A JOIN_GROUP should not be auto-approved by an expired invite (ideally it becomes a join request again, requiring a fresh invite/approval).

Actual behavior

  • Issued invites can be accepted at any time, regardless of TTL/expiration.
  • JOIN_GROUP finds an invite record and finalizes membership even if the invite’s expires_when is in the past.

Technical cause (current implementation)

  • Invite TTL is stored and expires_when is computed when saving GROUP_INVITE (expiry persisted in GroupInvites.expires_when).

  • However:

    • Join processing does not check expires_when at all; it only checks whether an invite exists and then uses it to approve membership.
    • Invite retrieval APIs / repository queries also do not filter expired invites; they return all invites (expired or not), and the join path treats any returned invite as valid.

In short: expiration is data-only right now; it is never consulted in validation/finalization logic.

Reproduction (simple)

  1. Create a closed group.
  2. Admin sends GROUP_INVITE to address X with a short TTL (e.g. 60 seconds).
  3. Wait until TTL has passed.
  4. Address X sends JOIN_GROUP.
  5. Observe: X is still added as a member (invite behaves as non-expiring).

Proposed fix (high level)

  1. Consensus-enforced expiry on membership finalization (behind a feature trigger):

    • After the trigger height, treat an invite as valid only if expiry == null/0 (never) OR finalization-time <= expires_when.
    • If expired, ignore invite and handle JOIN_GROUP as a join request (preferred) or reject (more disruptive).
    • Must be feature-triggered because it changes membership outcomes (consensus).
  2. API filtering for invites:

    • Default invite-list endpoints should omit expired invites.
    • Add optional flag (e.g. includeExpired=true) to show them for debugging/UI needs.
  3. Tests:

    • Add unit/integration tests covering:

      • pre-trigger vs post-trigger behavior
      • invite-first and join-first ordering
      • expired invite does not grant membership
      • API does not return expired invites by default

Notes / Related

  • There is a manual cancellation tx (CANCEL_GROUP_INVITE), but that’s not a substitute for TTL enforcement.
  • Prefer filtering over DB pruning for expired invites (avoid unnecessary state mutation; keep consensus logic deterministic).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions