Description
Summary
The public email-token approve route is registered for GET as well as POST, and the handler performs the approval on either method. Anything that issues a GET to the emailed link approves the purchase: mail security link scanners, URL previewers, browser prefetch.
Location (origin/main 5659f3b)
internal/api/router.go:163registersGET /api/purchases/approve/withapprovePurchaseHandler,AuthPublic;:164registers the POST.internal/api/router.go:588approvePurchaseHandlerandinternal/api/handler_purchases.go:594approvePurchasedo not branch on the HTTP method.internal/api/router.go:568resolveApprovalTokenaccepts the token from the query string on GET.- Precedent in the same router: the revoke email link (
revokeViaEmailTokenHandler) renders a confirmation form on GET and executes only on POST.
Failure scenario
An approver's mail gateway pre-fetches GET /api/purchases/approve/{id}?token=... to scan the link. The purchase is approved and executed without any human click. The scan has no session and needs nothing beyond the token already in the URL.
Suggested fix
GET renders a confirmation page (or redirects to the SPA approval modal); only POST with the token in the body approves, matching the revoke flow. The one-click email link still works: one click opens the confirmation, one more confirms.
Context
Found while implementing #529 (PR #548). Related: #529 (token-authorized details read needs a GET path too, see below), #71 and #77 (token hashing at rest). Searched for an existing issue on side-effecting GET approval; none found.
Acceptance
GET /api/purchases/approve/{id}?token=... never changes purchase state, and approving from the email link still works through a POST.
Activity
test(api): send approve as POST in the rate limiter test and mock the…
fix(api): stop cancelling a purchase on GET (#911)