Repository navigation
bug(gateway): comma-containing actor key components collide during query routing #5807
Description
Bug: Comma-containing actor key components collide during query routing
Summary
Query-based gateway routing does not preserve actor key component boundaries when a component contains a comma.
Both the TypeScript and Rust clients serialize compound actor keys by joining their components with commas. The guard then URL-decodes the value and splits it on every comma.
Consequently, these two distinct actor keys produce exactly the same gateway routing value:
["tenant,admin"]
["tenant", "admin"]Both are serialized as:
rvt-key=tenant%2Cadmin
The guard parses both values as:
["tenant", "admin"]This can route requests to the wrong actor and potentially break tenant isolation when actor key components contain user-controlled values.
Steps to reproduce
Create two handles whose keys differ only in component boundaries:
const first = client.room.getOrCreate(["tenant,admin"]);
const second = client.room.getOrCreate(["tenant", "admin"]);
console.log(await first.getGatewayUrl());
console.log(await second.getGatewayUrl());The resulting URLs contain the same routing parameter:
rvt-key=tenant%2Cadmin
Requests sent through both handles can therefore resolve to the same actor.
The same collision occurs between a string key and a compound key:
client.room.getOrCreate("tenant,admin");
client.room.getOrCreate(["tenant", "admin"]);The string is internally normalized to ["tenant,admin"], but query-based routing changes it into the two-component key ["tenant", "admin"].
A minimal demonstration of the serialization collision is:
const keys = [
["tenant,admin"],
["tenant", "admin"],
];
for (const key of keys) {
const params = new URLSearchParams();
params.append("rvt-key", key.join(","));
console.log(JSON.stringify(key), params.toString());
}Output:
["tenant,admin"] rvt-key=tenant%2Cadmin
["tenant","admin"] rvt-key=tenant%2Cadmin
Expected behavior
Actor key serialization must preserve component boundaries.
These keys must identify different actors:
["tenant,admin"] !== ["tenant", "admin"]Every supported actor key array should have a unique gateway representation.
Actual behavior
Both keys serialize to the same gateway query parameter and are parsed by the guard as ["tenant", "admin"].
Root cause
The TypeScript client joins key components using commas:
params.append("rvt-key", key.join(","));Relevant code:
The Rust client uses the same representation:
push_query_param(&mut params, "rvt-key", &key.join(","));Relevant code:
The guard URL-decodes the parameter and splits it on every comma:
fn split_key(raw: Option<&str>) -> Vec<String> {
match raw {
None | Some("") => Vec::new(),
Some(s) => s.split(',').map(String::from).collect(),
}
}Relevant code:
Percent-encoding does not prevent the collision because the guard decodes %2C back to , before calling split_key.
Impact
This bug can:
- Route requests to the wrong actor.
- Cause two logically distinct keys to share actor state.
- Break access to actors created with comma-containing key components.
- Create inconsistent behavior between direct actor creation and query-based
getorgetOrCreateoperations. - Violate tenant or resource isolation when user-controlled identifiers contain commas.
- Undermine the documented recommendation that array keys safely preserve user-provided components and prevent key injection.
The relevant documentation states that array keys make user-provided key data safe:
https://github.com/rivet-dev/rivet/blob/main/docs/content/docs/keys.mdx#L13-L22
Affected implementations
- TypeScript RivetKit client
- Rust RivetKit client
- Guard query-based actor routing
- HTTP requests, actions, queue operations, and WebSocket connections that use query-backed
getorgetOrCreatehandles
Suggested fix
Replace comma-separated key serialization with an unambiguous representation shared by the TypeScript client, Rust client, and guard.
Possible approaches include:
- A versioned CBOR/base64url-encoded key array.
- Repeated
rvt-keyparameters, with the guard treating each occurrence as one component. - The existing canonical actor-key serializer, which already escapes component separators.
Backward compatibility for existing comma-delimited gateway URLs may need to be considered.
Suggested regression tests
Add cross-client and guard tests proving that the following pairs remain distinct:
["a,b"] // must differ from ["a", "b"]
[""] // must differ from []
["a,", "b"] // must differ from ["a", "", "b"]
[","] // must differ from ["", ""]Tests should cover both get and getOrCreate, including HTTP and WebSocket gateway URLs.
Environment
Verified against local main checkout:
Commit: 78336a1a0ee33bb45e6b15e89963cbe91713353b
RivetKit package version: 2.3.17
Activity
linear-code commentedon Sep 29, 2026
ACT-6487
Abhikansh3 commentedon Sep 29, 2026
Opened a fix for this: #5808. Actor key components are now sent as repeated
rvt-key-partquery params instead of being comma-joined, so a component containing a literal comma can no longer collide with a multi-component key (["tenant,admin"]vs["tenant","admin"]). The legacyrvt-keyparam is kept unchanged for older/pinned clients.