fix: CalDAV sync broken by SSRF hostname replacement (#19291)
/closes #19272
This commit is contained in:
+55
@@ -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<string, unknown> = {},
|
||||
): 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());
|
||||
|
||||
-12
@@ -109,18 +109,6 @@ export class SecureHttpClientService {
|
||||
return resolveAndValidateHostname(hostnameOrUrl);
|
||||
}
|
||||
|
||||
async getValidatedUrl(serverUrl: string): Promise<string> {
|
||||
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');
|
||||
}
|
||||
|
||||
+2
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user