From c521ba29bebad020d7393a5f9ddcb8d6d971fbb9 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:45:59 +0530 Subject: [PATCH] fix: CalDAV sync broken by SSRF hostname replacement (#19291) /closes #19272 --- .../secure-http-client.service.spec.ts | 55 +++++++++++++++++++ .../secure-http-client.service.ts | 12 ---- .../caldav/providers/caldav.provider.ts | 3 +- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/secure-http-client/__tests__/secure-http-client.service.spec.ts b/packages/twenty-server/src/engine/core-modules/secure-http-client/__tests__/secure-http-client.service.spec.ts index f065b2ce8b..4121b15ec6 100644 --- a/packages/twenty-server/src/engine/core-modules/secure-http-client/__tests__/secure-http-client.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/secure-http-client/__tests__/secure-http-client.service.spec.ts @@ -11,6 +11,17 @@ jest.mock('axios-retry', () => ({ default: jest.fn(), })); +jest.mock( + 'src/engine/core-modules/secure-http-client/utils/resolve-and-validate-hostname.util', + () => ({ + resolveAndValidateHostname: jest.fn(), + }), +); + +import { resolveAndValidateHostname } from 'src/engine/core-modules/secure-http-client/utils/resolve-and-validate-hostname.util'; + +const mockResolveAndValidate = jest.mocked(resolveAndValidateHostname); + const createMockConfigService = ( overrides: Record = {}, ): TwentyConfigService => { @@ -260,6 +271,50 @@ describe('SecureHttpClientService', () => { }); }); + describe('getValidatedHost', () => { + it('should return the original hostname when safe mode is off', async () => { + const service = new SecureHttpClientService(createMockConfigService()); + + const result = await service.getValidatedHost( + 'https://caldav.icloud.com/principals/', + ); + + expect(result).toBe('https://caldav.icloud.com/principals/'); + expect(mockResolveAndValidate).not.toHaveBeenCalled(); + }); + + it('should validate and return resolved IP when safe mode is on', async () => { + mockResolveAndValidate.mockResolvedValue('17.248.239.66'); + const service = new SecureHttpClientService( + createMockConfigService({ OUTBOUND_HTTP_SAFE_MODE_ENABLED: true }), + ); + + const result = await service.getValidatedHost( + 'https://caldav.icloud.com/principals/', + ); + + expect(mockResolveAndValidate).toHaveBeenCalledWith( + 'https://caldav.icloud.com/principals/', + ); + expect(result).toBe('17.248.239.66'); + }); + + it('should throw when hostname resolves to a private IP', async () => { + mockResolveAndValidate.mockRejectedValue( + new Error( + 'Connection to internal IP address 192.168.1.1 is not allowed.', + ), + ); + const service = new SecureHttpClientService( + createMockConfigService({ OUTBOUND_HTTP_SAFE_MODE_ENABLED: true }), + ); + + await expect( + service.getValidatedHost('https://my-local-server.local/'), + ).rejects.toThrow('internal IP address'); + }); + }); + describe('logging interceptor', () => { it('should add a request interceptor when context is provided', () => { const service = new SecureHttpClientService(createMockConfigService()); diff --git a/packages/twenty-server/src/engine/core-modules/secure-http-client/secure-http-client.service.ts b/packages/twenty-server/src/engine/core-modules/secure-http-client/secure-http-client.service.ts index a2e0b8cc69..c673ad5348 100644 --- a/packages/twenty-server/src/engine/core-modules/secure-http-client/secure-http-client.service.ts +++ b/packages/twenty-server/src/engine/core-modules/secure-http-client/secure-http-client.service.ts @@ -109,18 +109,6 @@ export class SecureHttpClientService { return resolveAndValidateHostname(hostnameOrUrl); } - async getValidatedUrl(serverUrl: string): Promise { - if (!this.isSafeModeEnabled()) { - return serverUrl; - } - const resolvedIp = await resolveAndValidateHostname(serverUrl); - const url = new URL(serverUrl); - - url.hostname = resolvedIp; - - return url.toString(); - } - private isSafeModeEnabled(): boolean { return this.twentyConfigService.get('OUTBOUND_HTTP_SAFE_MODE_ENABLED'); } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/providers/caldav.provider.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/providers/caldav.provider.ts index d9e826750d..1e8cd513c1 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/providers/caldav.provider.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/providers/caldav.provider.ts @@ -26,9 +26,10 @@ export class CalDavClientProvider { throw new Error('Missing required CalDAV connection parameters'); } - const serverUrl = await this.secureHttpClientService.getValidatedUrl( + await this.secureHttpClientService.getValidatedHost( connectedAccount.connectionParameters.CALDAV.host, ); + const serverUrl = connectedAccount.connectionParameters.CALDAV.host; return new CalDAVClient({ username: