Skip to content

SECURITY: DELETE /api/v1/reports/:id answers 500 for another owner's report but 204 for a nonexistent id — an enumeration oracle over report ids #7523

Description

@huangyiirene

Symptom

DELETE /api/v1/reports/:id answers differently depending on whether the target id exists:

Target Response
Another owner's report id 500 REPORT_DELETE_FAILED
An id that does not exist 204 No Content

The 500-vs-204 split is an enumeration oracle over other users' saved-report ids: an authenticated caller can probe ids and read existence straight off the status code. That is precisely what the deny-as-404 posture exists to prevent, and it is the one hole in an otherwise clean surface — in the same run, cross-owner GET / run / upsert-overwrite / unschedule all answered 404, schedule list returned empty, ownerId could not be spoofed on create, and anonymous access was 401. No cross-owner 2xx anywhere; the leak is purely in the status-code discrimination.

Reproduced 2× on two distinct reports.

Root cause

The route, not the service.

  • The service layer is already correct. deleteReport() returns early for an unknown id and throws REPORT_NOT_FOUND for a cross-owner id, commented "others get a not-found so the delete neither fires nor reveals the report's existence". The intent is written down and implemented.
  • The route discards it. The DELETE ${dataPath}/reports/:id handler in packages/rest/src/rest-server.ts has a catch that goes straight to res.status(500) with REPORT_DELETE_FAILED. It never calls the file-local handleValidation(res, error) helper that maps REPORT_NOT_FOUND* → 404.
  • The sibling handler proves the shape. DELETE .../reports/schedules/:scheduleId, in the same file, does call handleValidation — which is why that route answers 404 correctly. One file, two handlers, only one of them wired through the mapper.

Not a stale-dist artifact.

Fix: one line — route the delete handler's catch through handleValidation(res, error) so REPORT_NOT_FOUND* becomes 404. Then align the unknown-id arm to the same status so the two arms are byte-indistinguishable; otherwise 404-vs-204 remains the same oracle in a quieter costume.

Reproduction

  1. Boot the showcase. Authenticate as user A and create a saved report; note its id.
  2. Authenticate as user B (a different owner).
  3. As B: DELETE /api/v1/reports/:idOwnedByA → observe 500 REPORT_DELETE_FAILED.
  4. As B: DELETE /api/v1/reports/:idThatDoesNotExist → observe 204 No Content.
  5. The differing status codes are the oracle. Repeat with a second report owned by A — reproduced 2×.

Re-check the seam directly:

rg -n "REPORT_DELETE_FAILED|handleValidation" packages/rest/src/rest-server.ts

Source

Extracted from the QA run #7515 (framework a86db17). The item is saved-report-ownership; #6683's owner gate is otherwise live and the item's revision-2 positive-deny assertions all hold.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions