Skip to content

Security: No Per-User MCP Token Count Limit — Unbounded Token Accumulation #1068

@BenjaminMichaelis

Description

@BenjaminMichaelis

Summary

McpTokenController.CreateToken does not enforce a limit on the number of active MCP API tokens a single user can hold. Each token gets its own independent rate-limit bucket (mcp-user:{userId} keyed on the authenticated user's ID, not the token), but a malicious user could create many tokens and use them from different clients simultaneously to multiply their effective request rate.

Affected Code

EssentialCSharp.Web/Controllers/McpTokenController.csCreateToken:

// No check on how many tokens the user already has
var (rawToken, entity) = await tokenService.CreateTokenAsync(userId, name, expiresAt, ...);

EssentialCSharp.Web/Services/McpRateLimiterPolicy.cs — rate limiting is keyed by userId:

string userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? ...;
return RateLimitPartition.GetTokenBucketLimiter(partitionKey: $"mcp-user:{userId}", ...);

Since the rate limiter already uses userId as the partition key (not the individual token), this specific vector is lower severity than it initially appears — all tokens for a user share the same bucket. However, there remain two related concerns:

  1. Unlimited token accumulation: No cleanup policy means the database grows unboundedly.
  2. No token count visibility: There is no way to detect a user who has created an unusual number of tokens (a potential indicator of account compromise or abuse preparation).

Risk

OWASP AI Agent Security — §6 Monitoring & Observability / Denial of Wallet (DoW)

  • Unbounded token creation is a housekeeping and abuse-signal gap.
  • In a future scenario where rate limiting is refactored to be per-token rather than per-user, the lack of a count cap would immediately enable rate-limit bypass.

Recommended Mitigations

  1. Enforce a per-user active token cap (e.g., max 10 non-revoked, non-expired tokens):
    int activeCount = await db.McpApiTokens
        .CountAsync(t => t.UserId == userId && t.RevokedAt == null
                      && (t.ExpiresAt == null || t.ExpiresAt > DateTime.UtcNow), cancellationToken);
    if (activeCount >= MaxTokensPerUser)
        return BadRequest(new { Error = $"Maximum of {MaxTokensPerUser} active tokens allowed." });
  2. Surface active token count to users in the MCP setup UI so they can manage their tokens.
  3. Add a background job to purge expired/revoked tokens older than a retention window.

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions