Skip to content

Invite tokens never expire — add 24h TTL (filterable) #11

@dd32

Description

@dd32

Summary

Invite tokens currently have no TTL — once created, they remain valid indefinitely until revoke_invite() is called. A lost URL from months ago is still live.

Agreed in review to add a 24-hour expiry.

Where

includes/class-map-invite.phpcreate_invite_url(), get_post_by_token(), handle_invite_request().

Suggested fix

Store a created-at timestamp alongside the (hashed, per #10) token. On lookup in get_post_by_token() and in handle_invite_request(), reject tokens older than MAP_INVITE_TTL (default DAY_IN_SECONDS, filterable).

Sketch (assuming we land #10 first so the storage shape changes anyway):

const TOKEN_META_KEY = '_map_invite_token';  // now stores an array: [ 'hash' => ..., 'created' => int ]

public static function create_invite_url( int $post_id ): string {
    $token = wp_generate_password( 32, false );
    update_post_meta(
        $post_id,
        self::TOKEN_META_KEY,
        array(
            'hash'    => hash( 'sha256', $token ),
            'created' => time(),
        )
    );
    return self::build_url( $token );
}

public static function get_post_by_token( string $token ): ?\WP_Post {
    if ( '' === $token ) {
        return null;
    }
    $hash = hash( 'sha256', $token );
    $ttl  = (int) apply_filters( 'map_invite_ttl', DAY_IN_SECONDS );

    // Query by hash via a meta_query on the serialized sub-key.
    // Simpler: query by post_status etc., then hash_equals + TTL check in PHP.
    …
}

The array meta shape plus a TTL check means lookup can no longer be a single meta_value equality query. Two realistic approaches:

  1. Store hash as the meta value (scalar) and keep created in a second meta key _map_invite_created. Preserves the simple meta_value lookup; requires updating both on create/revoke.
  2. Store as serialized array (as sketched). Query posts by a lightweight hash index (either by also keeping a _map_invite_hash scalar alongside, or by shifting to a custom table — related to the broader storage discussion in Update README with project description and Playground link #7).

Option 1 is the smaller change.

Handling expiry in handle_invite_request()

When an expired token is presented, surface the same "invalid or revoked" error message (wp_die) — don't leak whether it was expired vs never-existed vs revoked.

Optionally: delete expired tokens as a side-effect of encountering them, so they don't persist in the DB forever.

Filter

apply_filters( 'map_invite_ttl', DAY_IN_SECONDS ) so sites that want longer/shorter can override.

Tests to add

  • Token older than TTL is rejected.
  • Token within TTL is accepted.
  • Filter can override TTL.
  • handle_invite_request with expired token shows the same error page as an invalid token.

References

Internal security review, Apr 2026. Depends on / coordinates with #10.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions