diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 7cf66d383c..b9e2112264 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -42,3 +42,13 @@ export class RequiredError extends Error { super(msg); } } + +/** + * Error thrown when a path parameter is "." or "..", which would change the request path. + */ +export class InvalidPathParameterError extends Error { + override name = 'InvalidPathParameterError' as const; + constructor(msg?: string) { + super(msg); + } +} diff --git a/src/lib/runtime.ts b/src/lib/runtime.ts index 50ea25f305..302c94b638 100644 --- a/src/lib/runtime.ts +++ b/src/lib/runtime.ts @@ -1,5 +1,5 @@ import { retry } from './retry.js'; -import { FetchError, RequiredError, TimeoutError } from './errors.js'; +import { FetchError, InvalidPathParameterError, RequiredError, TimeoutError } from './errors.js'; import { RequestOpts, InitOverrideFunction, @@ -11,6 +11,17 @@ import { export * from './models.js'; +// "." and ".." are dot segments: URL resolution would remove them and send the request to a different path. +function validatePathSegments(path: string) { + for (const segment of path.split('/')) { + if (segment === '.' || segment === '..') { + throw new InvalidPathParameterError( + `Invalid path parameter "${segment}": "." and ".." are not allowed because they change the request path.` + ); + } + } +} + /** * @private * This is the base class for all generated API classes. @@ -55,6 +66,7 @@ export class BaseAPI { context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction ) { + validatePathSegments(context.path); let url = this.configuration.baseUrl + context.path; if (context.query !== undefined && Object.keys(context.query).length !== 0) { // only add the querystring to the URL if there are query parameters. diff --git a/test/lib/runtime.test.ts b/test/lib/runtime.test.ts index 2d3bdef31f..a3f9d04c5c 100644 --- a/test/lib/runtime.test.ts +++ b/test/lib/runtime.test.ts @@ -8,6 +8,7 @@ import { ResponseError, ManagementApiError, AuthApiError, + InvalidPathParameterError, } from '../../src/index.js'; import { InitOverrideFunction, RequestOpts } from '../../src/lib/models.js'; import { BaseAPI, applyQueryParams, CustomDomainHeader } from '../../src/lib/runtime.js'; @@ -407,6 +408,39 @@ describe('Runtime for ManagementClient', () => { jest.clearAllMocks(); }); + it.each(['..', '.'])( + 'should reject "%s" as a path parameter without sending a request', + async (id) => { + const request = nock(URL).get(/.*/).reply(200, []); + const client = new ManagementClient({ domain: 'tenant.auth0.com', token: 'TOKEN' }); + + await expect(client.users.getRoles({ id })).rejects.toThrow(InvalidPathParameterError); + expect(request.isDone()).toBe(false); + } + ); + + it('should not request a token when the path parameter is rejected', async () => { + const tokenRequest = nock('https://tenant.auth0.com') + .post('/oauth/token') + .reply(200, { access_token: 'TOKEN', expires_in: 86400 }); + const client = new ManagementClient({ + domain: 'tenant.auth0.com', + clientId: 'cid', + clientSecret: 'csecret', + }); + + await expect(client.users.getRoles({ id: '..' })).rejects.toThrow(InvalidPathParameterError); + expect(tokenRequest.isDone()).toBe(false); + }); + + it.each(['...', 'a.b'])('should allow "%s" as a path parameter', async (id) => { + const request = nock(URL).get(`/users/${id}/roles`).reply(200, []); + const client = new ManagementClient({ domain: 'tenant.auth0.com', token: 'TOKEN' }); + + await client.users.getRoles({ id }); + expect(request.isDone()).toBe(true); + }); + it('should retry if enabled', async () => { const request = nock(URL, { encodedQueryParams: true }) .get('/clients')