From bf39c99e7b9441456eedee02bc8d29cc76d6c63f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:46:47 +0000 Subject: [PATCH 1/3] Encode userId path parameter in user API key methods createUserApiKey and listUserApiKeys interpolated the userId argument directly into the request path without encodeURIComponent. Because the HTTP client builds the URL with new URL(path, baseURL), a userId containing ../ segments and a ? was normalized and query-split, letting the SDK's environment-key-authenticated request be redirected to an arbitrary API path (e.g. /organizations/org_.../api_keys). Wrap userId in encodeURIComponent, matching the SDK's established pattern (e.g. getUserConnectedAccount), so the argument stays confined to the user resource route. Add regression tests. --- src/user-management/user-management.spec.ts | 30 +++++++++++++++++++++ src/user-management/user-management.ts | 6 ++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index e71cf89bb..67bd5bd3d 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -2184,6 +2184,19 @@ describe('UserManagement', () => { organization_id: 'org_01EHZNVPK3SFK441A1RGBFSHRT', }); }); + + it('encodes the userId so it cannot escape the route template', async () => { + fetchOnce(listUserApiKeysFixture); + + await workos.userManagement.listUserApiKeys( + '../../organizations/org_01TARGET/api_keys?', + ); + + const url = new URL(fetchURL() as string); + expect(url.pathname).toBe( + '/user_management/users/..%2F..%2Forganizations%2Forg_01TARGET%2Fapi_keys%3F/api_keys', + ); + }); }); describe('createUserApiKey', () => { @@ -2241,6 +2254,23 @@ describe('UserManagement', () => { 'Idempotency-Key': 'the-idempotency-key', }); }); + + it('encodes the userId so it cannot escape the route template', async () => { + fetchOnce(createUserApiKeyFixture, { status: 201 }); + + await workos.userManagement.createUserApiKey( + '../../../organizations/org_01TARGET/api_keys?', + { + name: 'attacker-key', + organizationId: 'org_01EHZNVPK3SFK441A1RGBFSHRT', + }, + ); + + const url = new URL(fetchURL() as string); + expect(url.pathname).toBe( + '/user_management/users/..%2F..%2F..%2Forganizations%2Forg_01TARGET%2Fapi_keys%3F/api_keys', + ); + }); }); describe('getUserIdentities', () => { diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index fd02fe8ea..41403e877 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -1074,14 +1074,14 @@ export class UserManagement { return new AutoPaginatable( await fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/api_keys`, + `/user_management/users/${encodeURIComponent(userId)}/api_keys`, deserializeUserApiKey, serializedOptions, ), (params) => fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/api_keys`, + `/user_management/users/${encodeURIComponent(userId)}/api_keys`, deserializeUserApiKey, params, ), @@ -1109,7 +1109,7 @@ export class UserManagement { SerializedUserApiKeyWithValue, ReturnType >( - `/user_management/users/${userId}/api_keys`, + `/user_management/users/${encodeURIComponent(userId)}/api_keys`, serializeCreateUserApiKeyOptions(options), requestOptions, ); From 088d8056173885e27faee49ae7d44cfa79e0d7cd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:52:24 +0000 Subject: [PATCH 2/3] Encode userId in remaining user-management methods Extend the encodeURIComponent fix to the other user-scoped endpoints that share the same raw-interpolation path-traversal primitive: getUser, sendVerificationEmail, verifyEmail, listSessions, deleteUser, and getUserIdentities. Add regression tests for getUser and deleteUser. --- src/user-management/user-management.spec.ts | 24 +++++++++++++++++++++ src/user-management/user-management.ts | 23 ++++++++++++-------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index 67bd5bd3d..7f4daea2c 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -69,6 +69,17 @@ describe('UserManagement', () => { locale: 'en-US', }); }); + + it('encodes the userId so it cannot escape the route template', async () => { + fetchOnce(userFixture); + + await workos.userManagement.getUser('../../organizations/org_01TARGET?'); + + const url = new URL(fetchURL() as string); + expect(url.pathname).toBe( + '/user_management/users/..%2F..%2Forganizations%2Forg_01TARGET%3F', + ); + }); }); describe('getUserByExternalId', () => { @@ -2130,6 +2141,19 @@ describe('UserManagement', () => { expect(fetchURL()).toContain(`/user_management/users/${userId}`); expect(resp).toBeUndefined(); }); + + it('encodes the userId so it cannot escape the route template', async () => { + fetchOnce(); + + await workos.userManagement.deleteUser( + '../../organizations/org_01TARGET?', + ); + + const url = new URL(fetchURL() as string); + expect(url.pathname).toBe( + '/user_management/users/..%2F..%2Forganizations%2Forg_01TARGET%3F', + ); + }); }); describe('listUserApiKeys', () => { diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index 41403e877..87716162a 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -253,7 +253,7 @@ export class UserManagement { */ async getUser(userId: string): Promise { const { data } = await this.workos.get( - `/user_management/users/${userId}`, + `/user_management/users/${encodeURIComponent(userId)}`, ); return deserializeUser(data); @@ -860,7 +860,7 @@ export class UserManagement { userId, }: SendVerificationEmailOptions): Promise<{ user: User }> { const { data } = await this.workos.post<{ user: UserResponse }>( - `/user_management/users/${userId}/email_verification/send`, + `/user_management/users/${encodeURIComponent(userId)}/email_verification/send`, {}, ); @@ -931,9 +931,12 @@ export class UserManagement { const { data } = await this.workos.post< { user: UserResponse }, SerializedVerifyEmailOptions - >(`/user_management/users/${userId}/email_verification/confirm`, { - code, - }); + >( + `/user_management/users/${encodeURIComponent(userId)}/email_verification/confirm`, + { + code, + }, + ); return { user: deserializeUser(data.user) }; } @@ -1028,14 +1031,14 @@ export class UserManagement { return new AutoPaginatable( await fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/sessions`, + `/user_management/users/${encodeURIComponent(userId)}/sessions`, deserializeSession, options ? serializeListSessionsOptions(options) : undefined, ), (params) => fetchAndDeserialize( this.workos, - `/user_management/users/${userId}/sessions`, + `/user_management/users/${encodeURIComponent(userId)}/sessions`, deserializeSession, params, ), @@ -1051,7 +1054,9 @@ export class UserManagement { * @throws {NotFoundException} 404 */ async deleteUser(userId: string) { - await this.workos.delete(`/user_management/users/${userId}`); + await this.workos.delete( + `/user_management/users/${encodeURIComponent(userId)}`, + ); } /** @@ -1130,7 +1135,7 @@ export class UserManagement { } const { data } = await this.workos.get( - `/user_management/users/${userId}/identities`, + `/user_management/users/${encodeURIComponent(userId)}/identities`, ); return deserializeIdentities(data); From 17708afaf8851eabf977f84f1aaaf99611fa18d3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:56:40 +0000 Subject: [PATCH 3/3] Encode userId in updateUser path updateUser interpolated payload.userId raw into the PUT path, leaving the same path-traversal primitive open. Encode it and add a regression test. --- src/user-management/user-management.spec.ts | 14 ++++++++++++++ src/user-management/user-management.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index 7f4daea2c..41e658ccf 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -2033,6 +2033,20 @@ describe('UserManagement', () => { }); }); + it('encodes the userId so it cannot escape the route template', async () => { + fetchOnce(userFixture); + + await workos.userManagement.updateUser({ + userId: '../../organizations/org_01TARGET?', + firstName: 'Dane', + }); + + const url = new URL(fetchURL() as string); + expect(url.pathname).toBe( + '/user_management/users/..%2F..%2Forganizations%2Forg_01TARGET%3F', + ); + }); + describe('when only one property is provided', () => { it('sends a updateUser request', async () => { fetchOnce(userFixture); diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index 87716162a..c836bb292 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -1008,7 +1008,7 @@ export class UserManagement { */ async updateUser(payload: UpdateUserOptions): Promise { const { data } = await this.workos.put( - `/user_management/users/${payload.userId}`, + `/user_management/users/${encodeURIComponent(payload.userId)}`, serializeUpdateUserOptions(payload), );