From 4a15471c76d59f0defcc9e79605fe575380cccbf Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 14:36:07 -0500 Subject: [PATCH] docs(readme): document the secure-404 cross-tenant pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The codebase collapses "exists but not yours" into 404 across every single-row GET / PATCH / DELETE endpoint so a scoped caller can't enumerate another tenant's ID range by status code. The pattern landed across 11 entities (#174, #188, #192, #196, #200, #204, #210, #214, #218, #222, etc.) but the README never mentioned the behavior — operators reading the doc table would reasonably expect 403 on a cross-tenant probe and be surprised by 404. Add a short subsection in the HTTP-conventions block that explains the choice, links the behavior to the same getCompanyId scope check used for 403 paths on other surfaces, and notes that master keys still see all rows. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index cf69b69..7ae224f 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,18 @@ The `/healthz` endpoint is intentionally unauthenticated so it can be hit by orchestrators (Docker `HEALTHCHECK`, Kubernetes liveness, uptime monitors) without sharing a credential. +### Secure-404 on cross-tenant access + +Single-row GET / PATCH / DELETE endpoints return `404 Not Found` — +not `403 Forbidden` — when a non-master key references a row in a +different company's scope. The two outcomes look identical from the +client's side so a scoped caller can't probe sequential IDs to +enumerate the size of another tenant's table by status code. Master +keys still see all rows. The same pattern applies across all 16 +single-row entity endpoints; the auth-scope check that produces it +is the same `getCompanyId(...) !== row.CompId` comparison +the controllers use for the 403 paths on other surfaces. + ![example image](https://github.com/CryptoJones/TimeTrackerAPI/blob/master/setup/postman_example.PNG?raw=true) *(authKey example using Postman)*