Repository navigation
Description
Surfaced by the ADR-0096 execution-surface audit (the #2849 bug-class sweep). Two related defects on the reports surface, same root: ReportService discards the caller's ExecutionContext and runs against sys_saved_report / the report's target object with SYSTEM_CTX ({ isSystem: true }), so RLS/FLS/ownership/tenant scoping are all bypassed. The REST route layer only calls enforceAuth (authentication), not ownership.
Finding 1 — IDOR: any authenticated user can read or delete any saved report
packages/plugins/plugin-reports/src/report-service.ts:
getReport(reportId, _context)(:244) —_contextunused; queriessys_saved_reportby id withSYSTEM_CTX(:246).deleteReport(reportId, _context)(:251) — same; deletes by id + cascades schedules withSYSTEM_CTX(:255-260).listReports({ object, ownerId })(:231) —ownerIdis caller-supplied and optional; with it omitted, returns up to 500 reports across all owners withSYSTEM_CTX(:238-239).
The routes (packages/rest/src/rest-server.ts):
GET /data/reports/:id(:5448) →svc.getReport(req.params.id, context)DELETE /data/reports/:id(:5472) →svc.deleteReport(req.params.id, context)GET /data/reports(:5400) →svc.listReports({ object, ownerId: q.ownerId }, context)
Each does enforceAuth(req, res, context) then hands the id/query to the service, which throws the context away. Impact: an authenticated low-privilege user reads or deletes any other user's saved report by id (cross-owner; cross-tenant in tenancy.mode='multi'), and enumerates all reports via the list endpoint.
Finding 2 — Scheduled reports run as system, exfiltrating the target object
report-service.ts dispatchDue() (:411-425) executes each due schedule via:
const report = await this.getReport(schedule.report_id, { isSystem: true }); // :413
const result = await this.executeReport({ ...report, format: fmt }, { isSystem: true }, false); // :425executeReport threads its context into the target-object query (:299-306), so { isSystem: true } makes the scheduled run bypass the report owner's RLS/sharing and email out every row of the target object. The interactive path (:299, caller-scoped) is correct; the scheduled path is not. Unlike flows post-#1888, reports have no owner-scoped runAs — and there is no warning/lint here.
Suggested fix
get/delete/listReportsmust run under the caller's context (respect_context), or enforce an explicit owner/tenant predicate againstsys_saved_report.SYSTEM_CTXis legitimate only for server-controlled metadata stamps (last_run_at), not for authorization-bearing reads/deletes.dispatchDuemust resolve the schedule/report owner (owner_id, already on the row) to a realExecutionContextand runexecuteReportunder it — the reports analogue of ADR-0073runAs:'user'/owner-scoping — not{ isSystem: true }.
Relationship to ADR-0096 / #2849
This is the TRUSTED-IMPLICIT class the ADR names (a surface that ignores the caller and rides system authority). It is not the #2849 empty-principal fall-open (it's an unconditional SYSTEM_CTX bypass), but it is exactly what the ADR-0096 D4 conformance matrix + D2 systemContext(reason) audit would have caught. Tracking under the ADR-0096 evidence base; fix independently of the mechanism.
Activity
docs(adr): ADR-0096 — add enumerated evidence + D4 matrix seed
fix(security): close reports IDOR, RAG retrieval fall-open, and bulk-…
ADR-0096 + fixes for the 3 exploitable execution-surface holes (objec…
fix(rest): stop `DELETE /reports/:id` revealing whether a report id e…
fix(plugin-reports): stop `DELETE /reports/schedules/:scheduleId` rev…