fix(security): enforce ownership on medication/appointment write paths (#12) #13
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/12-write-path-ownership"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #12. The update/delete handlers for medications and appointments took the auth
Claimsbut never used them — any authenticated user could update or delete any other user's records by id (an IDOR / insecure direct object reference). Same gap onlog_dose(could skew another user's adherence stats) andget_adherence(leaked dose history).Surfaced while wiring the Phase B share-gate into the read paths (PR #11); filed separately as a security item.
Fix
Each affected handler now looks up the item first and confirms
user_id == claims.subbefore mutating or returning. Mismatches return 404 (not 403) to avoid leaking the existence of other users' records. Recipients of shared profiles remain read-only per the ADR — writes stay owner-only.Covered handlers:
update_medication,delete_medication,log_dose,get_adherenceupdate_appointment,delete_appointmenthealth_statswas already checkinguser_id == claims.subon update and delete — unaffected.Audit method
Grepped every handler taking
Extension<Claims>/Extension(_claims)>; the_claims(underscore-prefixed = unused) ones were the red flags. Confirmed each write/delete path's actual behavior, not just the signature.Tests
New
ownership_tests.rs: cross-user update/delete/log-dose/adherence all return 404; the legitimate owner can still do all of the above. Existing tests unaffected (they operate as the same user that created the data).Verification
cargo build+clippy --all-targets --all-features -D warnings+fmt --checkall clean. Tests compile. ⚠️ Same Mongo caveat as before — integration tests run in CI, not locally; I'll fix any failures.Out of scope
medications/:id/deleteandmedications/:id(update) routes use POST (not DELETE/PUT) — that's a pre-existing route-shape choice, unchanged here.