Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions nestjs/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('/');
});
});
});
16 changes: 15 additions & 1 deletion nestjs/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading