Skip to content

Commit e787601

Browse files
committed
fix(guard): fix url decoding actor id + token
1 parent 0eb4caf commit e787601

File tree

7 files changed

+137
-7
lines changed

7 files changed

+137
-7
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ features = ["full","extra-traits"]
227227
version = "2.5.4"
228228
features = ["serde"]
229229

230+
[workspace.dependencies.urlencoding]
231+
version = "2.1"
232+
230233
[workspace.dependencies.uuid]
231234
version = "1.11.0"
232235
features = ["v4","serde"]

engine/packages/guard/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ tracing.workspace = true
4848
universaldb.workspace = true
4949
universalpubsub.workspace = true
5050
url.workspace = true
51+
urlencoding.workspace = true
5152
uuid.workspace = true
5253

5354
[dev-dependencies]

engine/packages/guard/src/routing/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,15 @@ pub fn parse_actor_path(path: &str) -> Option<ActorPathInfo> {
208208
return None;
209209
}
210210

211-
(aid.to_string(), Some(tok.to_string()))
211+
// URL-decode both actor_id and token
212+
let decoded_aid = urlencoding::decode(aid).ok()?.to_string();
213+
let decoded_tok = urlencoding::decode(tok).ok()?.to_string();
214+
215+
(decoded_aid, Some(decoded_tok))
212216
} else {
213-
(actor_id_segment.to_string(), None)
217+
// URL-decode actor_id
218+
let decoded_aid = urlencoding::decode(actor_id_segment).ok()?.to_string();
219+
(decoded_aid, None)
214220
};
215221

216222
// Calculate the position in the original path where remaining path starts

engine/packages/guard/tests/parse_actor_path.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,54 @@ fn test_parse_actor_path_special_characters() {
107107

108108
#[test]
109109
fn test_parse_actor_path_encoded_characters() {
110-
// URL encoded characters in path
110+
// URL encoded characters in remaining path
111111
let path = "/gateway/actor-123/api%20endpoint/test%2Fpath";
112112
let result = parse_actor_path(path).unwrap();
113113
assert_eq!(result.actor_id, "actor-123");
114114
assert_eq!(result.token, None);
115115
assert_eq!(result.remaining_path, "/api%20endpoint/test%2Fpath");
116116
}
117117

118+
#[test]
119+
fn test_parse_actor_path_encoded_actor_id() {
120+
// URL encoded characters in actor_id (e.g., actor-123 with hyphen encoded)
121+
let path = "/gateway/actor%2D123/endpoint";
122+
let result = parse_actor_path(path).unwrap();
123+
assert_eq!(result.actor_id, "actor-123");
124+
assert_eq!(result.token, None);
125+
assert_eq!(result.remaining_path, "/endpoint");
126+
}
127+
128+
#[test]
129+
fn test_parse_actor_path_encoded_token() {
130+
// URL encoded characters in token (e.g., @ symbol encoded in token)
131+
let path = "/gateway/actor-123@tok%40en/endpoint";
132+
let result = parse_actor_path(path).unwrap();
133+
assert_eq!(result.actor_id, "actor-123");
134+
assert_eq!(result.token, Some("tok@en".to_string()));
135+
assert_eq!(result.remaining_path, "/endpoint");
136+
}
137+
138+
#[test]
139+
fn test_parse_actor_path_encoded_actor_id_and_token() {
140+
// URL encoded characters in both actor_id and token
141+
let path = "/gateway/actor%2D123@token%2Dwith%2Dencoded/endpoint";
142+
let result = parse_actor_path(path).unwrap();
143+
assert_eq!(result.actor_id, "actor-123");
144+
assert_eq!(result.token, Some("token-with-encoded".to_string()));
145+
assert_eq!(result.remaining_path, "/endpoint");
146+
}
147+
148+
#[test]
149+
fn test_parse_actor_path_encoded_spaces() {
150+
// URL encoded spaces in actor_id
151+
let path = "/gateway/actor%20with%20spaces/endpoint";
152+
let result = parse_actor_path(path).unwrap();
153+
assert_eq!(result.actor_id, "actor with spaces");
154+
assert_eq!(result.token, None);
155+
assert_eq!(result.remaining_path, "/endpoint");
156+
}
157+
118158
// Invalid path tests
119159

120160
#[test]

rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,16 +399,31 @@ export function parseActorPath(path: string): ActorPathInfo | null {
399399
const atPos = actorSegment.indexOf("@");
400400
if (atPos !== -1) {
401401
// Pattern: /gateway/{actor_id}@{token}/{...path}
402-
actorId = actorSegment.slice(0, atPos);
403-
token = actorSegment.slice(atPos + 1);
402+
const rawActorId = actorSegment.slice(0, atPos);
403+
const rawToken = actorSegment.slice(atPos + 1);
404404

405405
// Check for empty actor_id or token
406-
if (actorId.length === 0 || token.length === 0) {
406+
if (rawActorId.length === 0 || rawToken.length === 0) {
407+
return null;
408+
}
409+
410+
// URL-decode both actor_id and token
411+
try {
412+
actorId = decodeURIComponent(rawActorId);
413+
token = decodeURIComponent(rawToken);
414+
} catch (e) {
415+
// Invalid URL encoding
407416
return null;
408417
}
409418
} else {
410419
// Pattern: /gateway/{actor_id}/{...path}
411-
actorId = actorSegment;
420+
// URL-decode actor_id
421+
try {
422+
actorId = decodeURIComponent(actorSegment);
423+
} catch (e) {
424+
// Invalid URL encoding
425+
return null;
426+
}
412427
token = undefined;
413428
}
414429

rivetkit-typescript/packages/rivetkit/tests/parse-actor-path.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,64 @@ describe("parseActorPath", () => {
178178
});
179179
});
180180

181+
describe("URL-encoded actor_id and token", () => {
182+
test("should decode URL-encoded characters in actor_id", () => {
183+
const path = "/gateway/actor%2D123/endpoint";
184+
const result = parseActorPath(path);
185+
186+
expect(result).not.toBeNull();
187+
expect(result?.actorId).toBe("actor-123");
188+
expect(result?.token).toBeUndefined();
189+
expect(result?.remainingPath).toBe("/endpoint");
190+
});
191+
192+
test("should decode URL-encoded characters in token", () => {
193+
const path = "/gateway/actor-123@tok%40en/endpoint";
194+
const result = parseActorPath(path);
195+
196+
expect(result).not.toBeNull();
197+
expect(result?.actorId).toBe("actor-123");
198+
expect(result?.token).toBe("tok@en");
199+
expect(result?.remainingPath).toBe("/endpoint");
200+
});
201+
202+
test("should decode URL-encoded characters in both actor_id and token", () => {
203+
const path = "/gateway/actor%2D123@token%2Dwith%2Dencoded/endpoint";
204+
const result = parseActorPath(path);
205+
206+
expect(result).not.toBeNull();
207+
expect(result?.actorId).toBe("actor-123");
208+
expect(result?.token).toBe("token-with-encoded");
209+
expect(result?.remainingPath).toBe("/endpoint");
210+
});
211+
212+
test("should decode URL-encoded spaces in actor_id", () => {
213+
const path = "/gateway/actor%20with%20spaces/endpoint";
214+
const result = parseActorPath(path);
215+
216+
expect(result).not.toBeNull();
217+
expect(result?.actorId).toBe("actor with spaces");
218+
expect(result?.token).toBeUndefined();
219+
expect(result?.remainingPath).toBe("/endpoint");
220+
});
221+
222+
test("should reject invalid URL encoding in actor_id", () => {
223+
// %ZZ is invalid hex
224+
const path = "/gateway/actor%ZZ123/endpoint";
225+
const result = parseActorPath(path);
226+
227+
expect(result).toBeNull();
228+
});
229+
230+
test("should reject invalid URL encoding in token", () => {
231+
// %GG is invalid hex
232+
const path = "/gateway/actor-123@token%GG/endpoint";
233+
const result = parseActorPath(path);
234+
235+
expect(result).toBeNull();
236+
});
237+
});
238+
181239
describe("Invalid paths - wrong prefix", () => {
182240
test("should reject path with wrong prefix", () => {
183241
expect(parseActorPath("/api/123/endpoint")).toBeNull();

0 commit comments

Comments
 (0)