diff --git a/packages/hoppscotch-backend/src/auth/redirect-uri.validator.spec.ts b/packages/hoppscotch-backend/src/auth/redirect-uri.validator.spec.ts index ac7e9c9e..cb060045 100644 --- a/packages/hoppscotch-backend/src/auth/redirect-uri.validator.spec.ts +++ b/packages/hoppscotch-backend/src/auth/redirect-uri.validator.spec.ts @@ -3,65 +3,95 @@ import { isValidLocalhostRedirectUri } from './redirect-uri.validator'; describe('isValidLocalhostRedirectUri', () => { describe('valid loopback URIs', () => { test('Should return true for http://localhost with a port', () => { - expect(isValidLocalhostRedirectUri('http://localhost:3000/device-token')).toBe(true); + expect( + isValidLocalhostRedirectUri('http://localhost:3000/device-token'), + ).toBe(true); }); test('Should return true for http://localhost without a port', () => { - expect(isValidLocalhostRedirectUri('http://localhost/callback')).toBe(true); + expect(isValidLocalhostRedirectUri('http://localhost/callback')).toBe( + true, + ); }); test('Should return true for http://127.0.0.1 with a port', () => { - expect(isValidLocalhostRedirectUri('http://127.0.0.1:8080/callback')).toBe(true); + expect( + isValidLocalhostRedirectUri('http://127.0.0.1:8080/callback'), + ).toBe(true); }); test('Should return true for http://[::1] IPv6 loopback', () => { - expect(isValidLocalhostRedirectUri('http://[::1]:9090/callback')).toBe(true); + expect(isValidLocalhostRedirectUri('http://[::1]:9090/callback')).toBe( + true, + ); }); }); describe('DNS wildcard bypass vectors', () => { test('Should reject localhost subdomain via sslip.io wildcard DNS', () => { - expect(isValidLocalhostRedirectUri('http://localhost.1.2.3.4.sslip.io:3000/steal')).toBe(false); + expect( + isValidLocalhostRedirectUri( + 'http://localhost.1.2.3.4.sslip.io:3000/steal', + ), + ).toBe(false); }); test('Should reject localhost subdomain via nip.io wildcard DNS', () => { - expect(isValidLocalhostRedirectUri('http://localhost.10.0.0.1.nip.io/steal')).toBe(false); + expect( + isValidLocalhostRedirectUri('http://localhost.10.0.0.1.nip.io/steal'), + ).toBe(false); }); test('Should reject localhost as a subdomain of attacker domain', () => { - expect(isValidLocalhostRedirectUri('http://localhost.evil.com/steal')).toBe(false); + expect( + isValidLocalhostRedirectUri('http://localhost.evil.com/steal'), + ).toBe(false); }); test('Should reject localhost with trailing dot FQDN trick', () => { - expect(isValidLocalhostRedirectUri('http://localhost./callback')).toBe(false); + expect(isValidLocalhostRedirectUri('http://localhost./callback')).toBe( + false, + ); }); test('Should reject domain that starts with localhost string', () => { - expect(isValidLocalhostRedirectUri('http://localhostevil.com/callback')).toBe(false); + expect( + isValidLocalhostRedirectUri('http://localhostevil.com/callback'), + ).toBe(false); }); }); describe('protocol enforcement', () => { test('Should reject https since loopback listeners do not serve TLS', () => { - expect(isValidLocalhostRedirectUri('https://localhost:3000/callback')).toBe(false); + expect( + isValidLocalhostRedirectUri('https://localhost:3000/callback'), + ).toBe(false); }); test('Should reject ftp protocol', () => { - expect(isValidLocalhostRedirectUri('ftp://localhost/callback')).toBe(false); + expect(isValidLocalhostRedirectUri('ftp://localhost/callback')).toBe( + false, + ); }); }); describe('credential and remote host rejection', () => { test('Should reject URL with embedded credentials', () => { - expect(isValidLocalhostRedirectUri('http://user:pass@localhost:3000/callback')).toBe(false); + expect( + isValidLocalhostRedirectUri('http://user:pass@localhost:3000/callback'), + ).toBe(false); }); test('Should reject an arbitrary remote host', () => { - expect(isValidLocalhostRedirectUri('http://attacker.com:3000/callback')).toBe(false); + expect( + isValidLocalhostRedirectUri('http://attacker.com:3000/callback'), + ).toBe(false); }); test('Should reject 0.0.0.0 since it is not a loopback address', () => { - expect(isValidLocalhostRedirectUri('http://0.0.0.0:3000/callback')).toBe(false); + expect(isValidLocalhostRedirectUri('http://0.0.0.0:3000/callback')).toBe( + false, + ); }); }); diff --git a/packages/hoppscotch-backend/src/auth/redirect-uri.validator.ts b/packages/hoppscotch-backend/src/auth/redirect-uri.validator.ts index 9d69719b..e82a1740 100644 --- a/packages/hoppscotch-backend/src/auth/redirect-uri.validator.ts +++ b/packages/hoppscotch-backend/src/auth/redirect-uri.validator.ts @@ -1,7 +1,9 @@ // Only true loopback addresses are safe for native app redirects const LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]']; -export function isValidLocalhostRedirectUri(uri: string | undefined | null): boolean { +export function isValidLocalhostRedirectUri( + uri: string | undefined | null, +): boolean { if (!uri) return false; let url: URL; diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.controller.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.controller.ts index e83d302f..8872bba9 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.controller.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.controller.ts @@ -177,10 +177,7 @@ export class MockServerController { // Security headers to prevent XSS via mock responses res.setHeader('X-Content-Type-Options', 'nosniff'); if (!isSubdomainAccess) { - res.setHeader( - 'Content-Security-Policy', - "default-src 'none'; sandbox", - ); + res.setHeader('Content-Security-Policy', "default-src 'none'; sandbox"); res.setHeader('X-Frame-Options', 'DENY'); } diff --git a/packages/hoppscotch-backend/src/team/team.service.spec.ts b/packages/hoppscotch-backend/src/team/team.service.spec.ts index 0ab92145..c70e2f3f 100644 --- a/packages/hoppscotch-backend/src/team/team.service.spec.ts +++ b/packages/hoppscotch-backend/src/team/team.service.spec.ts @@ -800,17 +800,20 @@ describe('getMembersOfTeam', () => { describe('getTeamsOfUser', () => { test('resolves with the first 10 elements when no cursor is given', async () => { - mockPrisma.teamMember.findMany.mockResolvedValue([]); + mockPrisma.team.findMany.mockResolvedValue([]); await teamService.getTeamsOfUser(dbTeamMember.userUid, null); - expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({ + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ take: 10, + skip: 0, + cursor: undefined, where: { - userUid: dbTeamMember.userUid, - }, - include: { - team: true, + members: { + some: { + userUid: dbTeamMember.userUid, + }, + }, }, }); }); @@ -818,30 +821,28 @@ describe('getTeamsOfUser', () => { test('resolves as expected for paginated requests with cursor', async () => { const cursor = 'secondpage'; - mockPrisma.teamMember.findMany.mockResolvedValue([]); + mockPrisma.team.findMany.mockResolvedValue([]); await teamService.getTeamsOfUser(dbTeamMember.userUid, cursor); - expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({ + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ take: 10, skip: 1, cursor: { - teamID_userUid: { - teamID: cursor, - userUid: dbTeamMember.userUid, - }, + id: cursor, }, where: { - userUid: dbTeamMember.userUid, - }, - include: { - team: true, + members: { + some: { + userUid: dbTeamMember.userUid, + }, + }, }, }); }); test('resolves with an empty array for an invalid cursor', () => { // Invalid cursors return an empty array - mockPrisma.teamMember.findMany.mockResolvedValue([]); + mockPrisma.team.findMany.mockResolvedValue([]); return expect( teamService.getTeamsOfUser(dbTeamMember.userUid, 'invalidcursor'), @@ -849,7 +850,7 @@ describe('getTeamsOfUser', () => { }); test('resolves with an empty array for invalid user id and null cursor', () => { - mockPrisma.teamMember.findMany.mockResolvedValue([]); + mockPrisma.team.findMany.mockResolvedValue([]); return expect( teamService.getTeamsOfUser('invalidid', null), @@ -857,7 +858,7 @@ describe('getTeamsOfUser', () => { }); test('resolves with an empty array for invalid user id and invalid cursor', () => { - mockPrisma.teamMember.findMany.mockResolvedValue([]); + mockPrisma.team.findMany.mockResolvedValue([]); return expect( teamService.getTeamsOfUser('invalidId', 'invalidCursor'), diff --git a/packages/hoppscotch-backend/src/team/team.service.ts b/packages/hoppscotch-backend/src/team/team.service.ts index b0d618e7..cb5bc31c 100644 --- a/packages/hoppscotch-backend/src/team/team.service.ts +++ b/packages/hoppscotch-backend/src/team/team.service.ts @@ -266,37 +266,20 @@ export class TeamService implements UserDataHandler, OnModuleInit { cursor: string | null, take = 10, ): Promise { - if (!cursor) { - const entries = await this.prisma.teamMember.findMany({ - take, - where: { - userUid: uid, - }, - include: { - team: true, - }, - }); - - return entries.map((entry) => entry.team); - } else { - const entries = await this.prisma.teamMember.findMany({ - take, - skip: 1, - cursor: { - teamID_userUid: { - teamID: cursor, + const teams = await this.prisma.team.findMany({ + take, + skip: cursor ? 1 : 0, + cursor: cursor ? { id: cursor } : undefined, + where: { + members: { + some: { userUid: uid, }, }, - where: { - userUid: uid, - }, - include: { - team: true, - }, - }); - return entries.map((entry) => entry.team); - } + }, + }); + + return teams; } async getTeamWithID(teamID: string): Promise {