Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 69ed4ac05c fix(workflow): sanitize prototype-polluted keys in ResolverValidationPipe plainToInstance call
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.
2026-05-08 17:03:22 +00:00
@@ -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) {