Skip to content

SQL Injection in Twenty `searchVector` Field Settings Allows Arbitrary PostgreSQL Execution

Critical
prastoin published GHSA-mm7j-q9q3-qqwj Jul 27, 2026

Package

npm twentyhq/twenty (npm)

Affected versions

< 2.15.0

Patched versions

2.15.0

Description

Summary

Twenty exposed metadata field updates over REST and GraphQL to users holding the
DATA_MODEL permission. UpdateFieldInput accepted an arbitrary JSON settings
object, and for the system TS_VECTOR field named searchVector,
settings.asExpression was concatenated unescaped into a generated-column
definition and executed as ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS (...).
A workspace administrator with the DATA_MODEL permission could therefore inject
stacked PostgreSQL statements through a normally hidden system field.

This crosses the intended boundary between "edit CRM schema" and "execute arbitrary
SQL as the application database user." Fixed in Twenty 2.15.0 (PR #21947).

Details

settings is a public GraphQL JSON field and is not among the properties omitted by
UpdateFieldInput, so it is fully caller-controlled on both the REST
(PATCH /rest/metadata/fields/:id) and GraphQL (updateOneField) mutations, which
are guarded only by the DATA_MODEL permission.

During update conversion, the caller-supplied settings was copied into
universalSettings, and the system-field migration validator explicitly allowed
universalSettings edits on system fields. The TS_VECTOR validator only checked
the field name, isSystem, and the presence of an expression — it did not parse,
allowlist, or escape asExpression:

  • .../flat-field-metadata/utils/compute-flat-field-to-update-and-related-flat-field-to-update.util.ts (copies settings → universalSettings)
  • .../workspace-migration-builder/validators/services/flat-field-metadata-validator.service.ts (SYSTEM_FIELD_ALLOWED_UPDATE_PROPERTIES permits universalSettings)
  • .../flat-field-metadata/validators/utils/validate-ts-vector-flat-field-metadata.util.ts (only presence check)

The value then reached the SQL sink unescaped. buildSqlColumnDefinition placed it
directly between parentheses, and the result was executed via queryRunner.query:

// packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/utils/build-sql-column-definition.util.ts
if (column.asExpression && column.type === 'tsvector') {
  parts.push(`GENERATED ALWAYS AS (${column.asExpression})`);

Identifier escaping does not help, because the injected value is an SQL expression,
not an identifier.

PoC

As an authenticated user with the DATA_MODEL permission, update the system
searchVector field:

PATCH /rest/metadata/fields/{searchVectorFieldId}
{ "settings": { "asExpression": "to_tsvector('english', 'ok')) STORED; SELECT pg_sleep(5); --", "generatedType": "STORED" } }

The stacked statement breaks out of the GENERATED ALWAYS AS (...) clause in the
executed DDL:

ALTER TABLE "workspace_x"."person" ADD COLUMN "searchVector" tsvector
GENERATED ALWAYS AS (to_tsvector('english', 'ok')) STORED; SELECT pg_sleep(5); --) STORED

The 5-second response delay confirms execution of the injected statement.

Impact

  • Primitive: arbitrary PostgreSQL statement execution as the Twenty application
    database user.
  • Cross-workspace: where the app DB role owns all workspace schemas, a
    DATA_MODEL user in one workspace can read or modify data in other workspaces.
  • Remote code execution (default self-hosted deployments): the shipped Docker
    Compose, Podman, and app-dev defaults connect as a PostgreSQL superuser, so the
    injection escalates to OS command execution inside the database container via
    COPY ... TO PROGRAM. Helm / managed-Postgres deployments running as a
    non-superuser role are limited to SQL execution as the app role.
  • Availability: the same path can corrupt generated columns or run expensive
    statements, causing workspace- or database-level denial of service.

This requires an authenticated, high-privilege (DATA_MODEL) workspace user; it is
not exploitable unauthenticated.

Remediation

Upgrade to Twenty 2.15.0 or later, which validates and rejects unsafe TS_VECTOR
expressions at the metadata layer and adds a last-resort guard before the
GENERATED ALWAYS AS (...) clause is emitted.

Additional hardening for self-hosted deployments: run the application against a
dedicated, non-superuser PostgreSQL role (do not use the postgres bootstrap
superuser), and restrict the DATA_MODEL permission to trusted operators.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

CVE ID

CVE-2026-73069

Weaknesses

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data. Learn more on MITRE.

Credits