Centralize outbound HTTP requests through SecureHttpClientService (#17779)

## Summary

- Migrates all direct `axios` and `@nestjs/axios` `HttpService` usages
across the server to go through `SecureHttpClientService`, which
conditionally applies SSRF protection based on the
`OUTBOUND_HTTP_SAFE_MODE_ENABLED` config flag
- `SecureHttpClientService.getHttpClient()` now accepts optional
`AxiosRequestConfig` (e.g., `baseURL`) so callers can configure their
client while still getting protection
- Adds `getInternalHttpClient()` for trusted same-server requests (e.g.,
REST-to-GraphQL proxy, code-interpreter downloading internal files)
- Renames `getSecureAdapter` to `getSecureAxiosAdapter` for clarity
- Captcha drivers now receive a pre-configured `AxiosInstance` from the
module factory instead of creating their own

## Migrated services

| Service | Previous | Risk level |
|---------|----------|-----------|
| `file-upload.service` | `HttpService` | High (user-provided image
URLs) |
| `code-interpreter-tool` | `HttpService` + direct adapter | High
(user-provided file URLs) |
| `search-help-center-tool` | `axios.post()` | Low (hardcoded endpoints)
|
| `http-tool` | Already migrated | High (user-provided URLs) |
| `admin-panel.service` | `axios.get()` | Low (Docker Hub API) |
| `sign-in-up.service` | `HttpService` | Medium (logo URL validation) |
| `google-apis-scopes` | `HttpService` | Low (Google API) |
| `geo-map.service` | `HttpService` | Low (Google Maps API) |
| `telemetry.service` | `HttpService` | Low (telemetry endpoint) |
| `rest-api.service` | `HttpService` | Internal (uses
`getInternalHttpClient`) |
| `create-company.service` | `axios.create()` | Low (Twenty companies
API) |
| `google-recaptcha.driver` | `axios.create()` | Low (Google reCAPTCHA)
|
| `turnstile.driver` | `axios.create()` | Low (Cloudflare Turnstile) |

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] Admin panel unit tests pass
- [x] Secure adapter unit tests pass

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-02-08 19:52:35 +01:00
committed by GitHub
parent bc6268bb29
commit d7132b35d3
29 changed files with 187 additions and 134 deletions
@@ -1,14 +1,14 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { GeoMapResolver } from 'src/engine/core-modules/geo-map/resolver/geo-map.resolver';
import { GeoMapService } from 'src/engine/core-modules/geo-map/services/geo-map.service';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({
imports: [HttpModule, WorkspaceCacheStorageModule, TokenModule],
providers: [GeoMapService, GeoMapResolver],
imports: [WorkspaceCacheStorageModule, TokenModule],
providers: [GeoMapService, GeoMapResolver, SecureHttpClientService],
exports: [],
})
export class GeoMapModule {}
@@ -1,4 +1,3 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
@@ -12,6 +11,7 @@ import {
type AddressFields,
sanitizePlaceDetailsResults,
} from 'src/engine/core-modules/geo-map/utils/sanitize-place-details-results.util';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
@@ -19,7 +19,7 @@ export class GeoMapService {
private apiMapKey: string | undefined;
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly httpService: HttpService,
private readonly secureHttpClientService: SecureHttpClientService,
) {
if (
!this.twentyConfigService.get(
@@ -50,7 +50,9 @@ export class GeoMapService {
if (isDefined(isFieldCity) && isFieldCity === true) {
url += `&types=(cities)`;
}
const result = await this.httpService.axiosRef.get(url);
const httpClient = this.secureHttpClientService.getHttpClient();
const result = await httpClient.get(url);
if (result.data.status === 'OK') {
return sanitizeAutocompleteResults(result.data.predictions);
@@ -63,7 +65,9 @@ export class GeoMapService {
placeId: string,
token: string,
): Promise<AddressFields | undefined> {
const result = await this.httpService.axiosRef.get(
const httpClient = this.secureHttpClientService.getHttpClient();
const result = await httpClient.get(
`https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&sessiontoken=${token}&fields=address_components%2Cgeometry&key=${this.apiMapKey}`,
);