T09 · Insecure Skill Coding Practices
- Location
rules/security-rate-limiting.md:30- Finding
Client-Controlled Identity Allows Rate-Limit Bypass
- Content
View full analysis
Vulnerability Details
File Location:
rules/security-rate-limiting.md:30-38
Vulnerability Type: Rate-limit bypass through an untrusted identity key
Risk Level: MediumVulnerable Code
typescript const runtime = new CopilotRuntime({ middleware: { onBeforeRequest: async (options) => { const userId = options.properties?.userId if (!userId) throw new Error("Unauthorized") try { await limiter.consume(userId) } catch { throw new Error("Rate limit exceeded") } }, }, })The same insecure pattern is duplicated in the compiled documentation at
AGENTS.md:566-578.Technical Analysis
The example presented as the correct implementation uses
options.properties?.userIdas the rate-limiter key. CopilotKit request properties are supplied by the client unless the application explicitly replaces or validates them using trusted server-side authentication state.The code only verifies that
userIdis nonempty. It does not authenticate the request, verify that the claimed identifier belongs to the caller, or derive the identifier from a validated JWT or server-managed session. Consequently, possession of any arbitrary string is treated as sufficient authorization and as a distinct rate-limit identity.Rate limiting and authentication are separate controls. A client-provided identifier cannot securely provide either control without cryptographic or server-side verification.
Attack Path
- An attacker sends a request to the exposed CopilotKit runtime endpoint.
- The attacker supplies an arbitrary nonempty value in
properties.userId. - The middleware accepts that value and creates or selects its corresponding rate-limit bucket.
- After approaching the configured limit, the attacker changes
properties.userId. - The limiter treats the new value as a different user with a fresh quota.
- The attacker repeats this process to make effectively unbounded agent or LLM requests.
...[truncated 594 chars]
- Remediation
View remediation
Remediation Suggestions
- Authenticate every request before applying a user-scoped rate limit.
- Derive the limiter key exclusively from trusted server-side identity data, such as the
subclaim of a cryptographically verified JWT or a server-managed session. - Do not trust
properties.userId, headers, query parameters, or request bodies as identity sources without verification. - Reject requests when a client-provided identity conflicts with the authenticated identity.
- Clearly document that rate limiting does not replace authentication.
- Consider layered limits, including per-account, global, and trusted-proxy-aware IP limits, to reduce distributed abuse.
- Use a shared persistent limiter such as Redis in horizontally scaled production deployments rather than an in-memory limiter local to each instance.
A hardened pattern should resemble:
typescript const runtime = new CopilotRuntime({ middleware: { onBeforeRequest: async (options) => { const token = options.properties?.authToken if (!token) throw new Error("Unauthorized") const payload = await verifyJwt(token) const authenticatedUserId = payload?.sub if (!authenticatedUserId) throw new Error("Unauthorized") try { await limiter.consume(authenticatedUserId) } catch { throw new Error("Rate limit exceeded") } }, }, })Apply the same correction to the duplicated example in
AGENTS.md:566-578.
