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:
@@ -1,4 +1,3 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module';
|
||||
@@ -22,6 +21,7 @@ import { restToCommonArgsHandlers } from 'src/engine/api/rest/core/rest-to-commo
|
||||
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
@@ -58,7 +58,6 @@ const restApiCoreResolvers = [
|
||||
AuthModule,
|
||||
ApiKeyModule,
|
||||
UserRoleModule,
|
||||
HttpModule,
|
||||
TwentyORMModule,
|
||||
RecordTransformerModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
@@ -72,6 +71,7 @@ const restApiCoreResolvers = [
|
||||
providers: [
|
||||
RestApiService,
|
||||
RestApiCoreService,
|
||||
SecureHttpClientService,
|
||||
...restApiCoreResolvers,
|
||||
...restToCommonArgsHandlers,
|
||||
],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module';
|
||||
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
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';
|
||||
import { RestApiCoreModule } from 'src/engine/api/rest/core/rest-api-core.module';
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
@@ -15,12 +15,11 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
MetadataQueryBuilderModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
AuthModule,
|
||||
HttpModule,
|
||||
RestApiCoreModule,
|
||||
PermissionsModule,
|
||||
],
|
||||
controllers: [RestApiMetadataController],
|
||||
providers: [RestApiService, RestApiMetadataService],
|
||||
providers: [RestApiService, RestApiMetadataService, SecureHttpClientService],
|
||||
exports: [RestApiMetadataService, RestApiService],
|
||||
})
|
||||
export class RestApiModule {}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type AxiosResponse } from 'axios';
|
||||
@@ -6,6 +5,7 @@ import { type AxiosResponse } from 'axios';
|
||||
import { type Query } from 'src/engine/api/rest/core/types/query.type';
|
||||
import { RestApiException } from 'src/engine/api/rest/errors/RestApiException';
|
||||
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
|
||||
export enum GraphqlApiType {
|
||||
CORE = 'core',
|
||||
@@ -14,7 +14,9 @@ export enum GraphqlApiType {
|
||||
|
||||
@Injectable()
|
||||
export class RestApiService {
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
constructor(
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
) {}
|
||||
|
||||
async call(
|
||||
graphqlApiType: GraphqlApiType,
|
||||
@@ -28,8 +30,11 @@ export class RestApiService {
|
||||
: GraphqlApiType.METADATA
|
||||
}`;
|
||||
|
||||
// Internal request to the server's own GraphQL endpoint
|
||||
const httpClient = this.secureHttpClientService.getInternalHttpClient();
|
||||
|
||||
try {
|
||||
response = await this.httpService.axiosRef.post(url, data, {
|
||||
response = await httpClient.post(url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: requestContext.headers.authorization,
|
||||
|
||||
Reference in New Issue
Block a user