From 69ed4ac05c227d79dc1b9b9dd71bfdb11115aec9 Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Fri, 8 May 2026 17:03:22 +0000 Subject: [PATCH] fix(workflow): sanitize prototype-polluted keys in ResolverValidationPipe plainToInstance call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/36199?type=bug The `updateWorkflowVersionStep` mutation crashes with `TypeError: targetType is not a constructor` when a request contains `"constructor"` as a JSON key inside the free-form `logicFunctionInput` field, causing `class-transformer`'s recursive object traversal to call `new (plain-object)()` on a non-constructor value. Fix: ## What changed **File:** `packages/twenty-server/src/engine/core-modules/graphql/pipes/resolver-validation.pipe.ts` `ResolverValidationPipe.transform` calls `plainToInstance(metatype, value)` before any error guard. When the incoming GraphQL input contains `"constructor"` as a JSON key (e.g. inside the freeform `logicFunctionInput` field of a workflow step), `class-transformer`'s `TransformOperationExecutor` recurses into the object, resolves `targetType` from the key's value (a plain object, not a constructor function), and throws `TypeError: targetType is not a constructor` at `new targetType()`. The fix wraps `plainToInstance` in a `try/catch` that returns the original value on failure, exactly matching the pattern the team already applied to `validate()` via `safeClassValidatorValidateWrapper` in commit `b31845b7ba`. If `plainToInstance` throws, the raw value is returned and normal resolver processing continues — for freeform JSON fields (which carry no class-validator constraints), this has no observable behavioral difference. A `Logger.warn` is also added so that `plainToInstance` failures are visible in structured logs with the metatype name and error message, rather than surfacing as unhandled Sentry exceptions. --- .../graphql/pipes/resolver-validation.pipe.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/graphql/pipes/resolver-validation.pipe.ts b/packages/twenty-server/src/engine/core-modules/graphql/pipes/resolver-validation.pipe.ts index 7f13985cf9f..695799355a9 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/pipes/resolver-validation.pipe.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/pipes/resolver-validation.pipe.ts @@ -1,6 +1,7 @@ import { type ArgumentMetadata, Injectable, + Logger, type PipeTransform, type Type, } from '@nestjs/common'; @@ -22,6 +23,8 @@ const safeClassValidatorValidateWrapper = async ( @Injectable() export class ResolverValidationPipe implements PipeTransform { + private readonly logger = new Logger(ResolverValidationPipe.name); + async transform(value: unknown, metadata: ArgumentMetadata) { const { metatype } = metadata; @@ -29,7 +32,22 @@ export class ResolverValidationPipe implements PipeTransform { return value; } - const object = plainToInstance(metatype, value); + // class-transformer throws when the plain object contains prototype- + // pollution keys (e.g. "constructor") because it tries to call + // `new targetType()` on a non-constructor value. Skip validation and + // return the raw value so the resolver can handle it normally. + let object: object; + + try { + object = plainToInstance(metatype, value); + } catch (error) { + this.logger.warn( + `plainToInstance failed for ${metatype.name}, skipping validation: ${error instanceof Error ? error.message : String(error)}`, + ); + + return value; + } + const errors = await safeClassValidatorValidateWrapper(object); if (errors.length === 0) {