Skip to content

Commit da5b109

Browse files
committed
test(security): reproduce privileged view RLS bypass
1 parent 46392fb commit da5b109

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

tests/smoke_context_lifecycle_outbox_security_definer_search_path_authority.sh

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,91 @@ loaded = store.load("definer-search-path-a")
180180
assert loaded is not None
181181
assert loaded.evidence_id == "definer-search-path-a"
182182
PY
183+
184+
docker exec -i "${container}" psql -U postgres -d postgres -v ON_ERROR_STOP=1 <<'SQL'
185+
CREATE ROLE cwl_llm_batch_outbox_view_owner NOLOGIN
186+
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION BYPASSRLS;
187+
GRANT USAGE, CREATE ON SCHEMA public TO cwl_llm_batch_outbox_view_owner;
188+
GRANT SELECT ON public.llm_context_lifecycle_outbox
189+
TO cwl_llm_batch_outbox_view_owner;
190+
SET ROLE cwl_llm_batch_outbox_view_owner;
191+
CREATE VIEW public.cwl_llm_batch_outbox_privileged_view AS
192+
SELECT tenant_scope, evidence_id
193+
FROM public.llm_context_lifecycle_outbox;
194+
RESET ROLE;
195+
REVOKE CREATE ON SCHEMA public FROM cwl_llm_batch_outbox_view_owner;
196+
REVOKE ALL ON public.cwl_llm_batch_outbox_privileged_view FROM PUBLIC;
197+
GRANT SELECT ON public.cwl_llm_batch_outbox_privileged_view
198+
TO cwl_llm_batch_outbox_definer_path_caller;
199+
SQL
200+
201+
view_counts="$(
202+
docker exec -i "${container}" psql -h 127.0.0.1 \
203+
-U cwl_llm_batch_outbox_definer_path_caller -d postgres -Atq \
204+
-v ON_ERROR_STOP=1 <<'SQL'
205+
BEGIN;
206+
SELECT pg_catalog.set_config('pg_llm_batch.tenant_scope', 'tenant-a', true);
207+
SELECT pg_catalog.count(*) FROM ONLY public.llm_context_lifecycle_outbox;
208+
SELECT pg_catalog.count(*) FROM public.cwl_llm_batch_outbox_privileged_view;
209+
ROLLBACK;
210+
SQL
211+
)"
212+
if [[ "${view_counts}" != $'tenant-a\n1\n2' ]]; then
213+
echo "privileged view specimen did not reproduce the forced-RLS bypass" >&2
214+
printf '%s\n' "${view_counts}" >&2
215+
exit 1
216+
fi
217+
218+
docker run --rm -i --network "container:${container}" "${component_image}" python - <<'PY'
219+
from pg_llm_batch.context_lifecycle_outbox import PostgresContextLifecycleOutboxStore
220+
from pg_llm_batch.exceptions import ConfigError
221+
222+
store = PostgresContextLifecycleOutboxStore(
223+
"postgresql://cwl_llm_batch_outbox_definer_path_caller@127.0.0.1/postgres",
224+
tenant_scope="tenant-a",
225+
tenant_scope_sha256="a" * 64,
226+
)
227+
try:
228+
store.load("definer-search-path-a")
229+
except ConfigError as exc:
230+
assert "separated forced RLS authority" in str(exc)
231+
else:
232+
raise AssertionError(
233+
"runtime admitted a caller-selectable non-security-invoker view whose BYPASSRLS "
234+
"owner can read the lifecycle outbox across forced-RLS tenant boundaries"
235+
)
236+
PY
237+
238+
docker exec -i "${container}" psql -U postgres -d postgres -v ON_ERROR_STOP=1 <<'SQL'
239+
ALTER VIEW public.cwl_llm_batch_outbox_privileged_view
240+
SET (security_invoker = true);
241+
SQL
242+
243+
safe_view_count="$(
244+
docker exec -i "${container}" psql -h 127.0.0.1 \
245+
-U cwl_llm_batch_outbox_definer_path_caller -d postgres -Atq \
246+
-v ON_ERROR_STOP=1 <<'SQL'
247+
BEGIN;
248+
SELECT pg_catalog.set_config('pg_llm_batch.tenant_scope', 'tenant-a', true);
249+
SELECT pg_catalog.count(*) FROM public.cwl_llm_batch_outbox_privileged_view;
250+
ROLLBACK;
251+
SQL
252+
)"
253+
if [[ "${safe_view_count}" != $'tenant-a\n1' ]]; then
254+
echo "security-invoker view positive control did not preserve tenant RLS" >&2
255+
printf '%s\n' "${safe_view_count}" >&2
256+
exit 1
257+
fi
258+
259+
docker run --rm -i --network "container:${container}" "${component_image}" python - <<'PY'
260+
from pg_llm_batch.context_lifecycle_outbox import PostgresContextLifecycleOutboxStore
261+
262+
store = PostgresContextLifecycleOutboxStore(
263+
"postgresql://cwl_llm_batch_outbox_definer_path_caller@127.0.0.1/postgres",
264+
tenant_scope="tenant-a",
265+
tenant_scope_sha256="a" * 64,
266+
)
267+
loaded = store.load("definer-search-path-a")
268+
assert loaded is not None
269+
assert loaded.evidence_id == "definer-search-path-a"
270+
PY

0 commit comments

Comments
 (0)