ssrf hardening (#19963)

Hardened CalDav with new approach of wrapping axios ssrf http agent to
fetch via `@lifeomic/axios-fetch` because `tsdav` only accept `fetch`
override.

Also Hardened test endpoint
This commit is contained in:
neo773
2026-04-29 17:43:19 +05:30
committed by GitHub
parent 480e5796ec
commit abfa6200dd
10 changed files with 235 additions and 17 deletions
@@ -11,6 +11,14 @@ jest.mock('axios-retry', () => ({
default: jest.fn(),
}));
jest.mock('@lifeomic/axios-fetch', () => ({
buildAxiosFetch: jest.fn(() => jest.fn()),
}));
import { buildAxiosFetch } from '@lifeomic/axios-fetch';
const mockBuildAxiosFetch = jest.mocked(buildAxiosFetch);
jest.mock(
'src/engine/core-modules/secure-http-client/utils/resolve-and-validate-hostname.util',
() => ({
@@ -315,6 +323,37 @@ describe('SecureHttpClientService', () => {
});
});
describe('createSsrfSafeFetch', () => {
beforeEach(() => {
mockBuildAxiosFetch.mockClear();
});
it('should return globalThis.fetch when safe mode is off', () => {
const service = new SecureHttpClientService(createMockConfigService());
const result = service.createSsrfSafeFetch();
expect(result).toBe(globalThis.fetch);
expect(mockBuildAxiosFetch).not.toHaveBeenCalled();
});
it('should wrap an SSRF-protected axios client when safe mode is on', () => {
const service = new SecureHttpClientService(
createMockConfigService({ OUTBOUND_HTTP_SAFE_MODE_ENABLED: true }),
);
service.createSsrfSafeFetch();
expect(mockBuildAxiosFetch).toHaveBeenCalledTimes(1);
const axiosClient = mockBuildAxiosFetch.mock.calls[0][0] as ReturnType<
SecureHttpClientService['getHttpClient']
>;
expect(axiosClient.defaults.httpAgent).toBeInstanceOf(http.Agent);
expect(axiosClient.defaults.httpsAgent).toBeInstanceOf(https.Agent);
});
});
describe('logging interceptor', () => {
it('should add a request interceptor when context is provided', () => {
const service = new SecureHttpClientService(createMockConfigService());
@@ -4,6 +4,8 @@ import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios';
import axiosRetry from 'axios-retry';
import { isDefined } from 'twenty-shared/utils';
import { buildAxiosFetch } from '@lifeomic/axios-fetch';
import { createSsrfSafeAgent } from 'src/engine/core-modules/secure-http-client/utils/create-ssrf-safe-agent.util';
import { resolveAndValidateHostname } from 'src/engine/core-modules/secure-http-client/utils/resolve-and-validate-hostname.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -101,6 +103,14 @@ export class SecureHttpClientService {
return axios.create(config);
}
createSsrfSafeFetch(): typeof globalThis.fetch {
if (!this.isSafeModeEnabled()) {
return globalThis.fetch;
}
return buildAxiosFetch(this.getHttpClient()) as typeof globalThis.fetch;
}
async getValidatedHost(hostnameOrUrl: string): Promise<string> {
if (!this.isSafeModeEnabled()) {
return hostnameOrUrl;