A valid token proves who you are โ not that this row is yours.
6a
Insecure Direct Object Reference: the server trusts the id in the request. Asha is user 7.
6b
The middleware only needs one question answered: who owns this? Each route passes a function that answers it.
// OwnerFunc returns the user id that owns the target. type OwnerFunc func(r *http.Request) (string, error) // PathOwner: the {param} in the URL *is* the owner id. func PathOwner(param string) OwnerFunc { return func(r *http.Request) (string, error) { return r.PathValue(param), nil } } // PostOwner: load the post, return who owns it. func PostOwner(r *http.Request) (string, error) { p, ok := posts[r.PathValue("id")] if !ok { return "", ErrNotFound } return p.OwnerID, nil } func OwnerOrAdmin(owner OwnerFunc, next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { c := auth.ClaimsFrom(r.Context()) id, err := owner(r) if err != nil { http.Error(w, "not found", 404) return } if c.Subject != id && c.Role != "admin" { http.Error(w, "not found", 404) return } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } // wiring mux.Handle("PATCH /users/{id}", guard(PathOwner("id"), patchUser)) mux.Handle("PATCH /posts/{id}", guard(PostOwner, editPost))
โโ vulnerable: auth only, no ownership check asha GET /users/7 โ 200 {"id":"7","name":"Asha","email":"asha@example.com"} asha GET /users/8 โ 200 {"id":"8","name":"Ravi","email":"ravi@example.com"} asha GET /users/9 โ 200 {"id":"9","name":"Meera","email":"meera@example.com"} โโ guarded: OwnerOrAdmin asha GET /users/7 โ 200 {"id":"7","name":"Asha","email":"asha@example.com"} asha GET /users/8 โ 404 not found asha PATCH /users/7 โ 200 {"id":"7","name":"Asha K","email":"asha@example.com"} asha PATCH /users/8 โ 404 not found admin PATCH /users/8 โ 200 {"id":"8","name":"Ravi S","email":"ravi@example.com"} asha PATCH /posts/3 โ 404 not found admin PATCH /posts/3 โ 200 post updated
c.Subject from the verified token โ never an id from the body or a header.WHERE owner_id = $1.PATCH /users/{id} wrapped in RequireAuth(OwnerOrAdmin(PathOwner("id"), โฆ)).PATCH /posts/{id} with a PostOwner loader.Recap
owner == claims.Subject (or admin).OwnerFunc per resource type.