Description
validate_query enforces the omop schema allowlist only for schema-qualified table
references. Unqualified references skip the check entirely, so the allowlist is bypassed by
simply omitting the schema.
trust/data-access-api/data_access_api/services/cohort.py:162-167:
for table in stmt.find_all(exp.Table):
schema_node = table.args.get("db")
if schema_node is None:
# Unqualified — Postgres resolves via search_path, which is set to
# the omop schema in the OMOP DB image, so unqualified references
# can only resolve to omop tables.
continue
Verified against develop (69dd7f77):
REJECTED : SELECT * FROM pg_catalog.pg_class
ALLOWED : SELECT * FROM pg_class
ALLOWED : SELECT rolname FROM pg_roles
ALLOWED : SELECT tablename FROM pg_tables
REJECTED : SELECT * FROM public.anything
ALLOWED : SELECT * FROM omop.person
Reproduce:
cd trust/data-access-api
export DATA_ACCESS_POSTGRES_USER=u DATA_ACCESS_POSTGRES_PASSWORD=p OMOP_POSTGRES_DB=d
export AES_KEY_BASE64=$(python3 -c "import base64,os;print(base64.b64encode(os.urandom(32)).decode())")
export PYTHONPATH="$(pwd)/../observability:$(pwd)/data_access_api"
uv run python -c "
from data_access_api.services.cohort import validate_query
validate_query('SELECT * FROM pg_class') # ALLOWED
validate_query('SELECT * FROM pg_catalog.pg_class') # rejected
"
Why the stated justification does not hold
- The search_path is never actually set.
trust/omop-db/files/init.sql:13 issues a
session-level SET search_path TO omop, public inside one initdb psql invocation. Nothing
persists it — there is no ALTER DATABASE … SET search_path and no role-level setting. A live
trust omop-db reports "$user", public, and SELECT count(*) FROM image_occurrence
(unqualified) fails with "relation does not exist".
pg_catalog is implicitly in every search_path regardless of configuration, so
pg_class / pg_roles / pg_tables resolve even if the intended search_path were applied.
Impact
A researcher on an approved project can enumerate the trust database catalogue — table names,
role names, schema layout — and reach unqualified names in public. data_analyst_reader is
read-only, so this is information disclosure rather than modification, and it requires an approved
project (insider path). It nonetheless defeats a control the code claims to enforce, and the
suppression threshold does not help: these queries return well over COHORT_QUERY_THRESHOLD rows.
Found while reviewing #837; the code is from #839 and is live on stag/prod.
Acceptance criteria
Technical considerations
The trust is the authoritative layer here (the hub check is fast feedback only), so the fix belongs
in validate_query. Rejecting unqualified references outright is the simplest sound option, but it
would break any shipped tutorial or user query that omits the schema — worth grepping
fl-tutorials/ and the e2e query files before choosing.
Description
validate_queryenforces theomopschema allowlist only for schema-qualified tablereferences. Unqualified references skip the check entirely, so the allowlist is bypassed by
simply omitting the schema.
trust/data-access-api/data_access_api/services/cohort.py:162-167:Verified against
develop(69dd7f77):Reproduce:
Why the stated justification does not hold
trust/omop-db/files/init.sql:13issues asession-level
SET search_path TO omop, publicinside one initdbpsqlinvocation. Nothingpersists it — there is no
ALTER DATABASE … SET search_pathand no role-level setting. A livetrust
omop-dbreports"$user", public, andSELECT count(*) FROM image_occurrence(unqualified) fails with "relation does not exist".
pg_catalogis implicitly in every search_path regardless of configuration, sopg_class/pg_roles/pg_tablesresolve even if the intended search_path were applied.Impact
A researcher on an approved project can enumerate the trust database catalogue — table names,
role names, schema layout — and reach unqualified names in
public.data_analyst_readerisread-only, so this is information disclosure rather than modification, and it requires an approved
project (insider path). It nonetheless defeats a control the code claims to enforce, and the
suppression threshold does not help: these queries return well over
COHORT_QUERY_THRESHOLDrows.Found while reviewing #837; the code is from #839 and is live on stag/prod.
Acceptance criteria
tests/services/test_cohort.pycurrently covers only the qualified formpg_catalogaccess should be blocked explicitly rather than relying on schema matchingTechnical considerations
The trust is the authoritative layer here (the hub check is fast feedback only), so the fix belongs
in
validate_query. Rejecting unqualified references outright is the simplest sound option, but itwould break any shipped tutorial or user query that omits the schema — worth grepping
fl-tutorials/and the e2e query files before choosing.