diff --git a/nestjs/src/auth/auth.service.spec.ts b/nestjs/src/auth/auth.service.spec.ts index 4945c8be2..16013c515 100644 --- a/nestjs/src/auth/auth.service.spec.ts +++ b/nestjs/src/auth/auth.service.spec.ts @@ -305,4 +305,42 @@ describe('AuthService', () => { expect(usersService.updateUser).not.toHaveBeenCalled(); }); }); + + describe('getRedirectUrl', () => { + it('should return / when no loginData is provided', () => { + expect(service.getRedirectUrl(undefined)).toBe('/'); + }); + + it('should return / when loginData has no redirectUrl', () => { + expect(service.getRedirectUrl({})).toBe('/'); + }); + + it('should return a valid relative path', () => { + expect(service.getRedirectUrl({ redirectUrl: '/dashboard' })).toBe('/dashboard'); + }); + + it('should return a relative path with query string', () => { + expect(service.getRedirectUrl({ redirectUrl: '/course?id=123' })).toBe('/course?id=123'); + }); + + it('should decode a percent-encoded relative path', () => { + expect(service.getRedirectUrl({ redirectUrl: '/course%3Fid%3D123' })).toBe('/course?id=123'); + }); + + it('should return / for an absolute URL (open redirect prevention)', () => { + expect(service.getRedirectUrl({ redirectUrl: 'https://evil.com' })).toBe('/'); + }); + + it('should return / for a protocol-relative URL (open redirect prevention)', () => { + expect(service.getRedirectUrl({ redirectUrl: '//evil.com' })).toBe('/'); + }); + + it('should return / for an http URL (open redirect prevention)', () => { + expect(service.getRedirectUrl({ redirectUrl: 'http://evil.com/path' })).toBe('/'); + }); + + it('should return / for malformed percent-encoding', () => { + expect(service.getRedirectUrl({ redirectUrl: '/path%GGinvalid' })).toBe('/'); + }); + }); }); diff --git a/nestjs/src/auth/auth.service.ts b/nestjs/src/auth/auth.service.ts index 1bebcb14a..731e49c91 100644 --- a/nestjs/src/auth/auth.service.ts +++ b/nestjs/src/auth/auth.service.ts @@ -164,7 +164,21 @@ export class AuthService { } public getRedirectUrl(loginData?: LoginData) { - return loginData?.redirectUrl ? decodeURIComponent(loginData.redirectUrl) : '/'; + if (!loginData?.redirectUrl) { + return '/'; + } + let url: string; + try { + url = decodeURIComponent(loginData.redirectUrl); + } catch { + return '/'; + } + // Only allow relative paths to prevent open redirects. + // Reject protocol-relative URLs (starting with //) which behave like absolute URLs. + if (url.startsWith('/') && !url.startsWith('//')) { + return url; + } + return '/'; } public async onConnectionComplete(loginData: LoginData, userId: number) {