fix #63: require deletion token on DELETE /api/pastes/{id}
- DELETE now demands the create-time deletion token (Authorization header: Bearer/Token/bare, or ?token= query param), compared with the constant-time store.DeletionTokenEqual. 403 otherwise. - Creator-browser deletes via the /mine button (matching vwr cookie, #37) remain allowed; other browsers and plain API clients get 403. - Regression tests: no token, wrong token (header+query), correct token (header+query), creator-cookie path, token extraction. - Adapted TestSoftDelete to pass the deletion token. - docs/API.md delete section updated. - Based on #58's SoftDelete (bool, error) signature.
This commit is contained in:
+37
-6
@@ -237,12 +237,12 @@ func (a *apiServer) handleDeletePaste(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, 404, "paste not found")
|
||||
return
|
||||
}
|
||||
// viewer-cookie delete enforcement (#37): only the browser that created
|
||||
// the paste (matching vwr) may delete it via this endpoint. Requests with
|
||||
// no client-sent vwr cookie (plain API clients) are unaffected.
|
||||
vid := currentViewerID(r)
|
||||
if vid != "" && viewerSentCookie(r) && row.ViewerID.Valid && row.ViewerID.String != "" && row.ViewerID.String != vid {
|
||||
writeErr(w, 403, "not your paste")
|
||||
// #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).
|
||||
if !a.deletionAuthorized(r, row) {
|
||||
writeErr(w, 403, "deletion token required")
|
||||
return
|
||||
}
|
||||
if _, err := a.store.SoftDelete(row.ID); err != nil {
|
||||
@@ -252,6 +252,37 @@ 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.
|
||||
func deletionAuthorization(r *http.Request) string {
|
||||
if h := r.Header.Get("Authorization"); h != "" {
|
||||
for _, prefix := range []string{"Bearer ", "Token "} {
|
||||
if len(h) > len(prefix) && strings.EqualFold(h[:len(prefix)], prefix) {
|
||||
return strings.TrimSpace(h[len(prefix):])
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(h)
|
||||
}
|
||||
return r.URL.Query().Get("token")
|
||||
}
|
||||
|
||||
// deletionAuthorized reports whether the request may soft-delete the paste:
|
||||
// a valid constant-time-matched deletion token, or the creator browser's
|
||||
// viewer cookie (#37). Plain API clients with no token get false.
|
||||
func (a *apiServer) deletionAuthorized(r *http.Request, row *store.PasteRow) bool {
|
||||
if tok := deletionAuthorization(r); tok != "" {
|
||||
return row.DeletionToken.Valid && row.DeletionToken.String != "" &&
|
||||
store.DeletionTokenEqual(row.DeletionToken.String, tok)
|
||||
}
|
||||
// viewer-cookie delete enforcement (#37): only the browser that created
|
||||
// the paste (matching vwr) may delete it via this endpoint. Requests with
|
||||
// no client-sent vwr cookie (plain API clients) are unaffected.
|
||||
vid := currentViewerID(r)
|
||||
return vid != "" && viewerSentCookie(r) && row.ViewerID.Valid &&
|
||||
row.ViewerID.String != "" && row.ViewerID.String == vid
|
||||
}
|
||||
|
||||
// handleListMine serves /api/mine: pastes created from this browser (#37).
|
||||
func (a *apiServer) handleListMine(w http.ResponseWriter, r *http.Request) {
|
||||
vid := currentViewerID(r)
|
||||
|
||||
Reference in New Issue
Block a user