From 8cd0edaaf7e86a662749dacc909d88b1c5f0c302 Mon Sep 17 00:00:00 2001 From: Danswar <48102227+Danswar@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:11:24 -0300 Subject: [PATCH 1/2] chore(config): drop the unused PAYMENT_URL frontend config (#4548) payment.dfx.swiss has served a 301 redirect to app.dfx.swiss since 2026-07-22, so PAYMENT_URL no longer designates a distinct frontend. Config.frontend.payment had no consumers even before that. Verified on develop (dd59d8a78): - references to frontend.payment outside config.ts: 0 - positive controls from the same search: frontend.services 24, isRedirectUrlAllowed/allowedUrls 2 - no dynamic access (no "frontend[") and no spread or serialization of the frontend object, so nothing observes the property at runtime SERVICES_URL stays two-valued on purpose: the second entry feeds allowedUrls for redirect-URL validation. The Bicep template still declares the parameter; that belongs to the App Service deployment path and is left untouched here. --- .env.example | 1 - src/config/config.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/.env.example b/.env.example index 5e7045785c..68300ecaac 100644 --- a/.env.example +++ b/.env.example @@ -116,7 +116,6 @@ YAPEAL_ROOT_CA= YAPEAL_WEBHOOK_API_KEY= YAPEAL_ACCOUNT_IDENTIFIER= -PAYMENT_URL=https://dev.payment.dfx.swiss SERVICES_URL=https://dev.app.dfx.swiss;https://dev.services.dfx.swiss LIMIT_REQUEST_SUPPORT_NAME= diff --git a/src/config/config.ts b/src/config/config.ts index cf77d593ae..153d71e4ea 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -686,7 +686,6 @@ export class Configuration { frontend = { allowedUrls: (process.env.SERVICES_URL ?? '').split(';'), services: (process.env.SERVICES_URL ?? '').split(';')[0], - payment: process.env.PAYMENT_URL, isRedirectUrlAllowed: (url: string): boolean => { try { From 94cb86c0a20075ecc051f7659b5ff180419a94d3 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 1 Aug 2026 02:09:02 +0200 Subject: [PATCH 2/2] fix(gs): disable the /gs/support endpoint pending key validation (#4554) * fix(gs): disable the /gs/support endpoint pending key validation The `key` query parameter of this endpoint reaches the query builders behind `GsService.getSupportData` without being restricted to the columns of the target entity. Disable the route until that validation is in place: the handler now rejects every request with 503 and the service is no longer reached. Internal support tooling that reads from this endpoint is unavailable until the follow-up change lands. * refactor(gs): drive the /gs/support kill switch from the config Follow the master-switch pattern already used for the ledger: the endpoint reads a hard-coded flag in `Config.support` that ships off and states, next to the switch, what has to hold before it goes back on. The handler keeps its DTO, so the switch is the only thing that decides. Reject with `ForbiddenException('Endpoint disabled')` like the neighbouring `/gs/db` handlers instead of `ServiceUnavailableException`, which CONTRIBUTING.md reserves for external outages. * fix(gs): tighten the support switch comment and its spec setup Keep the switch comment to the condition for turning it back on, and name the endpoint by its real path. Install the module-level `Config` in the spec through the ConfigService constructor instead of a discarded provider call, and pin the switch explicitly in the rejection test so flipping the shipped default fails exactly the one test that guards it. --- src/config/config.ts | 4 +++ .../gs/__tests__/gs.controller.spec.ts | 34 ++++++++++++++++++- src/subdomains/generic/gs/gs.controller.ts | 4 +++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/config/config.ts b/src/config/config.ts index 153d71e4ea..785a112b1d 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -649,6 +649,10 @@ export class Configuration { ]; support = { + // Master switch for the support data endpoint (`GET /v1/gs/support`) — hard-coded (intentionally + // no env/DB/setting), default OFF. Turn it back on only once that endpoint validates its `key` + // query parameter against an allowlist of the target entity's columns. + dataEndpointEnabled: false, limitRequest: { mailName: process.env.LIMIT_REQUEST_SUPPORT_NAME, mailAddress: process.env.LIMIT_REQUEST_SUPPORT_MAIL, diff --git a/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts b/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts index a9fe37e0df..4504a80dce 100644 --- a/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts +++ b/src/subdomains/generic/gs/__tests__/gs.controller.spec.ts @@ -1,11 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { BadRequestException } from '@nestjs/common'; +import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { Config, ConfigService, Configuration } from 'src/config/config'; import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import * as processServiceModule from 'src/shared/services/process.service'; import { DbQueryDto } from 'src/subdomains/generic/gs/dto/db-query.dto'; +import { SupportTable } from 'src/subdomains/generic/gs/dto/gs.dto'; import { GsTriggerType } from 'src/subdomains/generic/gs/dto/gs-trigger-type.enum'; +import { SupportDataQuery } from 'src/subdomains/generic/gs/dto/support-data.dto'; import { GsController } from 'src/subdomains/generic/gs/gs.controller'; import { GsService } from 'src/subdomains/generic/gs/gs.service'; @@ -31,6 +34,10 @@ describe('GsController', () => { beforeEach(() => { service = createMock(); controller = new GsController(service); + // This suite builds the controller directly instead of through a Nest TestingModule, so the + // module-level `Config` has to be installed by hand — that is what the ConfigService constructor + // does. A fresh one per test keeps the endpoint switch at its shipped default. + new ConfigService(new Configuration()); verboseSpy = jest.spyOn(DfxLogger.prototype, 'verbose').mockImplementation(); jest.spyOn(processServiceModule, 'DisabledProcess').mockReturnValue(false); }); @@ -114,4 +121,29 @@ describe('GsController', () => { expect(verboseSpy.mock.calls[1][0]).toBe('DB data call for asset in x?forged failed:'); }); }); + + describe('getSupportData', () => { + const supportQuery = Object.assign(new SupportDataQuery(), { table: SupportTable.USER_DATA, key: 'id', value: 1 }); + + it('rejects while the endpoint switch is off, without reaching the GS service', async () => { + // Set explicitly rather than relying on the shipped default: flipping that default is meant to + // fail exactly one test — the one below that guards it. + Config.support.dataEndpointEnabled = false; + + await expect(controller.getSupportData(supportQuery)).rejects.toBeInstanceOf(ForbiddenException); + expect(service.getSupportData).not.toHaveBeenCalled(); + }); + + it('ships with the endpoint switch off', () => { + expect(Config.support.dataEndpointEnabled).toBe(false); + }); + + it('reaches the GS service once the switch is flipped on', async () => { + Config.support.dataEndpointEnabled = true; + + await controller.getSupportData(supportQuery); + + expect(service.getSupportData).toHaveBeenCalledWith(supportQuery); + }); + }); }); diff --git a/src/subdomains/generic/gs/gs.controller.ts b/src/subdomains/generic/gs/gs.controller.ts index 52e2134dd1..9ef3a70b39 100644 --- a/src/subdomains/generic/gs/gs.controller.ts +++ b/src/subdomains/generic/gs/gs.controller.ts @@ -1,6 +1,7 @@ import { BadRequestException, Body, Controller, ForbiddenException, Get, Post, Query, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiExcludeEndpoint } from '@nestjs/swagger'; +import { Config } from 'src/config/config'; import { GetJwt } from 'src/shared/auth/get-jwt.decorator'; import { JwtPayload } from 'src/shared/auth/jwt-payload.interface'; import { RoleGuard } from 'src/shared/auth/role.guard'; @@ -61,6 +62,9 @@ export class GsController { @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.SUPPORT), UserActiveGuard()) async getSupportData(@Query() query: SupportDataQuery): Promise { + // Off by default; the switch in `Config.support` states what has to hold before it goes back on. + if (!Config.support.dataEndpointEnabled) throw new ForbiddenException('Endpoint disabled'); + return this.gsService.getSupportData(query); }