POST /api/v1/analytics/query returns the executed SQL to the caller on a deployment running NODE_ENV=production, with no debug flag requested.
Repro
Request (no debug field of any kind):
{ "cube": "sys_user", "measures": ["count"], "dimensions": [] }Response:
{ "success": true, "data": {
"rows": [{ "count": "2" }],
"fields": [{ "name": "count", "type": "number" }],
"sql": "SELECT COUNT(*) AS \"count\" FROM \"sys_user\" WHERE ((\"sys_user\".\"id\" = $1 OR \"sys_user\".\"id\" IN ($2, $3) OR \"sys_user\".\"id\" = $4 …"
} }Container environment of the serving app (objectos-ee-deploy-app-1):
NODE_ENV=production
OS_TENANCY_POSTURE=isolated
OS_MODE=standalone
Expected
The contract already says this is debug-gated — packages/spec/src/api/analytics.zod.ts:75:
sql: z.string().optional().describe('Executed SQL (if debug enabled)'),Nothing in the request enabled debug, and the deployment is production, so sql should be absent.
Why it matters here specifically
The echoed statement discloses the physical table name, the column naming, and — most sensitive on a multi-tenant deployment — the shape of the isolation predicate itself. In this case it reveals that sys_user is walled by an enumerated id IN (…) member list rather than an organization_id comparison, i.e. it tells a tenant exactly how (and on which column) the wall is built, plus the bound-parameter arity, which leaks the rough member count of their own org and the query surface to probe against.
This is an information-disclosure issue, not a broken wall: every isolation probe I ran on this deployment held (cross-tenant read → 404, cross-tenant update/delete → 403 row-level security, filter/where naming another org → empty, batch update/delete by foreign id → per-row PERMISSION_DENIED, audit log and activity stream partitioned cleanly). The wall is fine; it just should not describe itself to callers.
Suggested fix
Gate the sql field on the same debug switch the schema documents, and default it off outside development — or drop it from the response entirely and keep SQL echo on the dedicated /api/v1/analytics/sql dry-run route (analytics.zod.ts:23), which is the surface that exists for it.
Environment
objectos-ee-deploy stack (Caddy → app → postgres:16) on http://localhost:8080, observed 2026-08-13, org owner session. Image commit not verified; spec references are the current framework main checkout.
POST /api/v1/analytics/queryreturns the executed SQL to the caller on a deployment runningNODE_ENV=production, with no debug flag requested.Repro
Request (no debug field of any kind):
{ "cube": "sys_user", "measures": ["count"], "dimensions": [] }Response:
{ "success": true, "data": { "rows": [{ "count": "2" }], "fields": [{ "name": "count", "type": "number" }], "sql": "SELECT COUNT(*) AS \"count\" FROM \"sys_user\" WHERE ((\"sys_user\".\"id\" = $1 OR \"sys_user\".\"id\" IN ($2, $3) OR \"sys_user\".\"id\" = $4 …" } }Container environment of the serving app (
objectos-ee-deploy-app-1):Expected
The contract already says this is debug-gated —
packages/spec/src/api/analytics.zod.ts:75:Nothing in the request enabled debug, and the deployment is production, so
sqlshould be absent.Why it matters here specifically
The echoed statement discloses the physical table name, the column naming, and — most sensitive on a multi-tenant deployment — the shape of the isolation predicate itself. In this case it reveals that
sys_useris walled by an enumeratedid IN (…)member list rather than anorganization_idcomparison, i.e. it tells a tenant exactly how (and on which column) the wall is built, plus the bound-parameter arity, which leaks the rough member count of their own org and the query surface to probe against.This is an information-disclosure issue, not a broken wall: every isolation probe I ran on this deployment held (cross-tenant read → 404, cross-tenant update/delete → 403 row-level security,
filter/wherenaming another org → empty, batch update/delete by foreign id → per-rowPERMISSION_DENIED, audit log and activity stream partitioned cleanly). The wall is fine; it just should not describe itself to callers.Suggested fix
Gate the
sqlfield on the same debug switch the schema documents, and default it off outside development — or drop it from the response entirely and keep SQL echo on the dedicated/api/v1/analytics/sqldry-run route (analytics.zod.ts:23), which is the surface that exists for it.Environment
objectos-ee-deploystack (Caddy → app → postgres:16) onhttp://localhost:8080, observed 2026-08-13, org owner session. Image commit not verified; spec references are the currentframeworkmain checkout.