Remove ?token= deletion-token path (#143)
CI / test (pull_request) Successful in 35s
CI / docker (pull_request) Skipped

The deletion token was accepted via the ?token= query parameter on both
DELETE /api/pastes/{id} and /redeem, and round-tripped through the paste
URL after creation. URL-carried bearer secrets leak into reverse-proxy
access logs and browser history.

- API: deletion tokens are now accepted only via the Authorization header
  (Bearer/Token/bare); query params are ignored on both endpoints
- Web create flow: token moves to the browser via a short-lived tok_<id>
  HttpOnly cookie instead of the redirect URL; the paste view reads it
  from the cookie, never from ?token=
- Web view: the delete button calls redeem() which takes the token from
  sessionStorage and sends it as an Authorization header
- Tests: correct token in query must be rejected (403/400); header path
  still deletes/redeems; extraction unit cases updated

Fixes #143
This commit is contained in:
fen
2026-09-10 08:24:05 -05:00
parent 33ccfd373a
commit 27763cd1ba
7 changed files with 81 additions and 29 deletions
+10 -7
View File
@@ -311,9 +311,10 @@ func (a *apiServer) handleDeletePaste(w http.ResponseWriter, r *http.Request) {
return
}
// #63: deletion requires authorization. Either the deletion token issued
// at create time (Authorization header or ?token= query param, matching
// the create response's "deletion_token" field), or the creator browser
// itself (client-sent vwr cookie matching the paste's viewer, #37).
// at create time (Authorization header; #143 removed the ?token= query
// path so the bearer secret never lands in access logs or history), or
// the creator browser itself (client-sent vwr cookie matching the
// paste's viewer, #37).
if !a.deletionAuthorized(r, row) {
writeErr(w, 403, "deletion token required")
return
@@ -325,9 +326,11 @@ func (a *apiServer) handleDeletePaste(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]string{"status": "soft-deleted"})
}
// deletionAuthorization extracts the deletion token from the request: the
// Authorization header ("Bearer <t>", "Token <t>", or a bare token) or the
// token query parameter. Returns "" when absent.
// deletionAuthorization extracts the deletion token from the request:
// the Authorization header ("Bearer <t>", "Token <t>", or a bare token).
// The ?token= query parameter is deliberately NOT accepted (#143): URL
// query strings end up in proxy access logs and browser history. Returns
// "" when absent.
func deletionAuthorization(r *http.Request) string {
if h := r.Header.Get("Authorization"); h != "" {
for _, prefix := range []string{"Bearer ", "Token "} {
@@ -337,7 +340,7 @@ func deletionAuthorization(r *http.Request) string {
}
return strings.TrimSpace(h)
}
return r.URL.Query().Get("token")
return ""
}
// deletionAuthorized reports whether the request may soft-delete the paste: