## Summary - Adds SSRF (Server-Side Request Forgery) protection to webhook requests by using the same secure axios adapter already used by HTTP workflow actions - Prevents webhooks from making requests to private/internal IP addresses (10.x, 192.168.x, 172.16-31.x, 169.254.x, localhost) - Adds specific error logging when a webhook fails due to SSRF protection ## Context The HTTP workflow tool (`HTTP_REQUEST` action) already had SSRF protection via `HTTP_TOOL_SAFE_MODE_ENABLED`, but webhooks were using `HttpService` directly without this protection. This inconsistency meant users could potentially configure webhooks to probe internal infrastructure. ### What's protected now: | Feature | Before | After | |---------|--------|-------| | HTTP Workflow Action | Protected (secure adapter) | Protected (secure adapter) | | Webhooks | **Unprotected** | Protected (secure adapter) | ### The secure adapter validates: 1. Protocol must be `http:` or `https:` 2. DNS resolution of hostname 3. Resolved IP must not be in private ranges ## Test plan - [ ] Configure a webhook with an external URL (e.g., `https://webhook.site`) - should work - [ ] Configure a webhook with `http://localhost:3000` - should fail with SSRF error in audit log - [ ] Configure a webhook with `http://10.0.0.1/test` - should fail with SSRF error in audit log - [ ] Configure a webhook with a domain that resolves to a private IP - should fail
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
import { APP_FILTER } from '@nestjs/core';
|
|
import { type NestExpressApplication } from '@nestjs/platform-express';
|
|
import {
|
|
Test,
|
|
type TestingModule,
|
|
type TestingModuleBuilder,
|
|
} from '@nestjs/testing';
|
|
|
|
import bytes from 'bytes';
|
|
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
|
|
|
import { AppModule } from 'src/app.module';
|
|
import { CommandModule } from 'src/command/command.module';
|
|
import { settings } from 'src/engine/constants/settings';
|
|
import { StripeSDKMockService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service';
|
|
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
|
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/constants/captcha-driver.constants';
|
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
|
import { ExceptionHandlerMockService } from 'src/engine/core-modules/exception-handler/mocks/exception-handler-mock.service';
|
|
import { MockedUnhandledExceptionFilter } from 'src/engine/core-modules/exception-handler/mocks/mock-unhandled-exception.filter';
|
|
import { SyncDriver } from 'src/engine/core-modules/message-queue/drivers/sync.driver';
|
|
import { JobsModule } from 'src/engine/core-modules/message-queue/jobs.module';
|
|
import { QUEUE_DRIVER } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
|
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
|
|
|
|
interface TestingModuleCreatePreHook {
|
|
(moduleBuilder: TestingModuleBuilder): TestingModuleBuilder;
|
|
}
|
|
|
|
/**
|
|
* Hook for adding items to nest application
|
|
*/
|
|
export type TestingAppCreatePreHook = (
|
|
app: NestExpressApplication,
|
|
) => Promise<void>;
|
|
|
|
// Shared SyncDriver instance for all queues in tests
|
|
// This enables synchronous processing of jobs during integration tests
|
|
const syncDriver = new SyncDriver();
|
|
|
|
/**
|
|
* Sets basic integration testing module of app
|
|
*/
|
|
export const createApp = async (
|
|
config: {
|
|
moduleBuilderHook?: TestingModuleCreatePreHook;
|
|
appInitHook?: TestingAppCreatePreHook;
|
|
} = {},
|
|
): Promise<NestExpressApplication> => {
|
|
const stripeSDKMockService = new StripeSDKMockService();
|
|
const mockExceptionHandlerService = new ExceptionHandlerMockService();
|
|
let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({
|
|
imports: [
|
|
AppModule,
|
|
CommandModule,
|
|
JobsModule,
|
|
MessageQueueModule.registerExplorer(),
|
|
],
|
|
providers: [
|
|
{
|
|
provide: APP_FILTER,
|
|
useClass: MockedUnhandledExceptionFilter,
|
|
},
|
|
],
|
|
})
|
|
.overrideProvider(StripeSDKService)
|
|
.useValue(stripeSDKMockService)
|
|
.overrideProvider(ExceptionHandlerService)
|
|
.useValue(mockExceptionHandlerService)
|
|
.overrideProvider(CAPTCHA_DRIVER)
|
|
.useValue({
|
|
validate: async () => ({ success: true }),
|
|
})
|
|
.overrideProvider(QUEUE_DRIVER)
|
|
.useValue(syncDriver);
|
|
|
|
if (config.moduleBuilderHook) {
|
|
moduleBuilder = config.moduleBuilderHook(moduleBuilder);
|
|
}
|
|
|
|
const moduleFixture: TestingModule = await moduleBuilder.compile();
|
|
|
|
const app = moduleFixture.createNestApplication<NestExpressApplication>({
|
|
rawBody: true,
|
|
cors: true,
|
|
});
|
|
|
|
app.use(
|
|
'/graphql',
|
|
graphqlUploadExpress({
|
|
maxFieldSize: bytes(settings.storage.maxFileSize),
|
|
maxFiles: 10,
|
|
}),
|
|
);
|
|
|
|
app.use(
|
|
'/metadata',
|
|
graphqlUploadExpress({
|
|
maxFieldSize: bytes(settings.storage.maxFileSize),
|
|
maxFiles: 10,
|
|
}),
|
|
);
|
|
|
|
if (config.appInitHook) {
|
|
await config.appInitHook(app);
|
|
}
|
|
|
|
await app.init();
|
|
|
|
return app;
|
|
};
|