Skip to content

fix(security): reject ':' in tenant ids, count only POSTs for eve rate limit#5

Merged
CahidArda merged 2 commits into
mainfrom
harden-id-separator-ratelimit
Jun 24, 2026
Merged

fix(security): reject ':' in tenant ids, count only POSTs for eve rate limit#5
CahidArda merged 2 commits into
mainfrom
harden-id-separator-ratelimit

Conversation

@CahidArda

Copy link
Copy Markdown
Collaborator

Summary

Hardens the per-user tenant boundary and fixes the eve rate-limit double-count,
plus a sweep of stale docs/docstrings found in review. Folded into the existing
harden-tenant-isolation changeset (no new changeset).

Tenant isolation — reject : in ids

The whole isolation model rests on the key shape <prefix>:<userId>:<...>, but
only emptiness was validated. A : in an id let keys collide across users — e.g.
user "a" + session "b:c" resolves to the same Redis key as user "a:b" +
session "c", so the direct-key paths (getChat/saveChat/deleteChat/forget)
could read or overwrite another user's data. The search-backed methods were safe
(they filter on the userId field), which is why it was easy to miss.

  • AgentMemory, ChatHistory, ToolCache now reject a : in userId /
    sessionId / toolName up front (alongside the existing non-empty check).
  • Added unit tests for the rejection across all three primitives.

eve rate limiting — count only POSTs

eve drives each turn as two authenticated requests — the message POST plus
a follow-up GET …/stream that opens the reply stream — and the auth walk runs on
both, so createRateLimitAuth was charging every turn twice (a slidingWindow(20)
was really ~10 turns/min).

  • createRateLimitAuth now throttles only POST requests, so one turn = one
    token; session-read GETs fall through unthrottled.
  • Tradeoff: the stream GET is no longer rate-limited (it requires a valid
    session id to reach). Documented in the README + docstring.
  • Updated tests (a bare Request defaults to GET).

Docs

  • Document deriving userId from a verified server-side auth source (Clerk,
    Auth.js/NextAuth, Supabase Auth, Auth0, …), never a client-supplied value;
    added ⚠️ DEMO ONLY warnings to the ai-sdk-demo routes that trust an
    x-user-id header.
  • Dropped @upstash/agentkit-sdk from the eve README install command — it's
    already a regular dependency, not something users install.
  • Fixed stale references: search-tools {@link withIndex}
    ReactiveSearchIndex; ai-sdk memory "scope" → "userId"; corrected CLAUDE.md's
    superseded namespace key-naming note.

Testing

  • SDK + eve unit tests pass against live Redis (incl. new colon-rejection and
    GET-skip tests); pnpm typecheck and pnpm lint clean.

Notes

  • The :-rejection is a behavior change (callers passing colon-bearing ids now
    throw), but it lands within the already-unreleased breaking harden-tenant-isolation
    line, so it's captured in that changeset rather than a new one.

…e limit

Harden the per-user key boundary and fix the eve rate-limit double-count,
plus stale-doc cleanup. Folded into the existing tenant-isolation changeset.

Tenant isolation
- AgentMemory/ChatHistory/ToolCache now reject a ':' (the key separator) in
  userId/sessionId/toolName. The whole isolation model rests on the key shape
  `<prefix>:<userId>:<...>`, and only emptiness was validated — a ':' let keys
  collide across users (e.g. user "a"+session "b:c" reached user "a:b"+session
  "c" via the direct-key getChat/saveChat/deleteChat/forget paths).
- Added unit tests for the rejection across all three primitives.

eve rate limiting
- createRateLimitAuth counts only POST requests. eve drives each turn as two
  authenticated requests (the message POST + a follow-up GET .../stream) and
  the auth walk runs on both, so a turn was charged twice. Now one turn = one
  token; session-read GETs fall through unthrottled. Documented in the README
  and docstring; updated tests (a bare Request defaults to GET).

Docs
- Document deriving userId from a verified auth source (Clerk, Auth.js,
  Supabase Auth, Auth0, ...), never a client-supplied value; added DEMO-ONLY
  warnings to the ai-sdk-demo routes that trust an x-user-id header.
- Fixed stale references: search-tools `{@link withIndex}` -> ReactiveSearchIndex;
  ai-sdk memory "scope" -> "userId"; corrected CLAUDE.md's superseded
  namespace key-naming note.
@CahidArda CahidArda merged commit 391310e into main Jun 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant