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:
@@ -8,14 +8,18 @@ import {
|
||||
CaptchaDriverType,
|
||||
type CaptchaModuleAsyncOptions,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
|
||||
|
||||
@Global()
|
||||
export class CaptchaModule {
|
||||
static forRoot(options: CaptchaModuleAsyncOptions): DynamicModule {
|
||||
const provider = {
|
||||
provide: CAPTCHA_DRIVER,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
useFactory: async (...args: any[]) => {
|
||||
useFactory: async (
|
||||
secureHttpClientService: SecureHttpClientService,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...args: any[]
|
||||
) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
if (!config) {
|
||||
@@ -24,19 +28,30 @@ export class CaptchaModule {
|
||||
|
||||
switch (config.type) {
|
||||
case CaptchaDriverType.GOOGLE_RECAPTCHA:
|
||||
return new GoogleRecaptchaDriver(config.options);
|
||||
return new GoogleRecaptchaDriver(
|
||||
config.options,
|
||||
secureHttpClientService.getHttpClient({
|
||||
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
|
||||
}),
|
||||
);
|
||||
case CaptchaDriverType.TURNSTILE:
|
||||
return new TurnstileDriver(config.options);
|
||||
return new TurnstileDriver(
|
||||
config.options,
|
||||
secureHttpClientService.getHttpClient({
|
||||
baseURL:
|
||||
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
}),
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
inject: options.inject || [],
|
||||
inject: [SecureHttpClientService, ...(options.inject || [])],
|
||||
};
|
||||
|
||||
return {
|
||||
module: CaptchaModule,
|
||||
providers: [CaptchaService, provider],
|
||||
providers: [CaptchaService, SecureHttpClientService, provider],
|
||||
exports: [CaptchaService],
|
||||
};
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,4 +1,4 @@
|
||||
import axios, { type AxiosInstance } from 'axios';
|
||||
import { type AxiosInstance } from 'axios';
|
||||
|
||||
import { type CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
import { type CaptchaServerResponse } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-server-response';
|
||||
@@ -12,12 +12,13 @@ export class GoogleRecaptchaDriver implements CaptchaDriver {
|
||||
private readonly _siteKey: string;
|
||||
private readonly secretKey: string;
|
||||
private readonly httpService: AxiosInstance;
|
||||
constructor(private _options: CaptchaDriverOptions) {
|
||||
constructor(
|
||||
private _options: CaptchaDriverOptions,
|
||||
httpClient: AxiosInstance,
|
||||
) {
|
||||
this._siteKey = _options.siteKey;
|
||||
this.secretKey = _options.secretKey;
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
|
||||
});
|
||||
this.httpService = httpClient;
|
||||
}
|
||||
|
||||
async validate(token: string): Promise<CaptchaValidateResult> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, { type AxiosInstance } from 'axios';
|
||||
import { type AxiosInstance } from 'axios';
|
||||
|
||||
import { type CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
import { type CaptchaServerResponse } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-server-response';
|
||||
@@ -12,12 +12,13 @@ export class TurnstileDriver implements CaptchaDriver {
|
||||
private readonly _siteKey: string;
|
||||
private readonly secretKey: string;
|
||||
private readonly httpService: AxiosInstance;
|
||||
constructor(private _options: CaptchaDriverOptions) {
|
||||
constructor(
|
||||
private _options: CaptchaDriverOptions,
|
||||
httpClient: AxiosInstance,
|
||||
) {
|
||||
this._siteKey = _options.siteKey;
|
||||
this.secretKey = _options.secretKey;
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
});
|
||||
this.httpService = httpClient;
|
||||
}
|
||||
|
||||
async validate(token: string): Promise<CaptchaValidateResult> {
|
||||
|
||||
Reference in New Issue
Block a user