T09 · Insecure Skill Coding Practices
Warning
- Location
- rules/security-rate-limiting.md:30
- Finding
- Client-Controlled Identity Allows Rate-Limit Bypass## Vulnerability Details **File Location**: `rules/security-rate-limiting.md:30-38` **Vulnerability Type**: Rate-limit bypass through an untrusted identity key **Risk Level**: Medium ### Vulnerable 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?.userId` as 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 `userId` is 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 1. An attacker sends a request to the exposed CopilotKit runtime endpoint. 2. The attacker supplies an arbitrary nonempty value in `properties.userId`. 3. The middleware accepts that value and creates or selects its corresponding rate-limit bucket. 4. After approaching the configured limit, the attacker changes `properties.userId`. 5. The limiter treats the new value as a different user with a fresh quota. 6. The attacker repeats this process to make effectively unbounded agent or LLM requests. ## ...[truncated 594 chars]
- 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 `sub` claim 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`.
