package api // Regression tests for #63 and #143: DELETE /api/pastes/{id} must require the // deletion token in the Authorization header (constant-time compare). The // ?token= query parameter is NOT accepted (#143): URL-carried tokens leak // into access logs and browser history. Without a token, or with a wrong // token, the paste must survive and the response must be 403. import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) // createTestPaste creates a paste via the API and returns id + deletion token. func createTestPaste(t *testing.T, h http.Handler) (string, string) { t.Helper() req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(`{"content":"delete me"}`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 201 { t.Fatalf("create: got %d want 201: %s", rec.Code, rec.Body.String()) } var created struct { ID string `json:"id"` DeletionToken string `json:"deletion_token"` } json.Unmarshal(rec.Body.Bytes(), &created) if created.ID == "" || created.DeletionToken == "" { t.Fatalf("create response missing id/deletion_token: %s", rec.Body.String()) } return created.ID, created.DeletionToken } func pasteExists(t *testing.T, h http.Handler, id string) bool { t.Helper() req := httptest.NewRequest("GET", "/api/pastes/"+id, nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code == 200 { return true } if rec.Code == 404 { return false } t.Fatalf("get after delete: got %d", rec.Code) return false } func TestDeleteWithoutTokenForbidden(t *testing.T) { s := testServer(t) h := s.routes() id, _ := createTestPaste(t, h) rec := doReq(t, h, "DELETE", "/api/pastes/"+id, "", "") if rec.Code != http.StatusForbidden { t.Fatalf("delete without token: got %d want 403", rec.Code) } if !pasteExists(t, h, id) { t.Fatal("paste was deleted without a token") } } func TestDeleteWithWrongTokenForbidden(t *testing.T) { s := testServer(t) h := s.routes() id, _ := createTestPaste(t, h) // query param: even the CORRECT token must be rejected now (#143) id2, tok2 := createTestPaste(t, h) rec := doReq(t, h, "DELETE", "/api/pastes/"+id2+"?token="+tok2, "", "") if rec.Code != http.StatusForbidden { t.Fatalf("delete with correct token in query: got %d want 403 (#143)", rec.Code) } if !pasteExists(t, h, id2) { t.Fatal("paste was deleted via ?token= query param (#143 regression)") } // header req := httptest.NewRequest("DELETE", "/api/pastes/"+id, nil) req.Header.Set("Authorization", "Bearer wrong-token") rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("delete with wrong token (header): got %d want 403", rec.Code) } if !pasteExists(t, h, id) { t.Fatal("paste was deleted with a wrong token") } } func TestDeleteWithCorrectToken(t *testing.T) { s := testServer(t) h := s.routes() id, tok := createTestPaste(t, h) // via Authorization header req := httptest.NewRequest("DELETE", "/api/pastes/"+id, nil) req.Header.Set("Authorization", "Bearer "+tok) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("delete with correct token (header): got %d want 200: %s", rec.Code, rec.Body.String()) } if pasteExists(t, h, id) { t.Fatal("paste still exists after authorized delete") } // query param: even with the correct token the delete must fail (#143) id, tok = createTestPaste(t, h) rec = doReq(t, h, "DELETE", "/api/pastes/"+id+"?token="+tok, "", "") if rec.Code != http.StatusForbidden { t.Fatalf("delete with correct token (query): got %d want 403 (#143)", rec.Code) } if !pasteExists(t, h, id) { t.Fatal("paste was deleted via ?token= query param (#143 regression)") } } // #143: the deletion token must be accepted via the Authorization header on // the redeem (hard delete) endpoint too. func TestRedeemWithCorrectTokenHeader(t *testing.T) { s := testServer(t) h := s.routes() id, tok := createTestPaste(t, h) req := httptest.NewRequest("DELETE", "/api/pastes/"+id+"/redeem", nil) req.Header.Set("Authorization", "Bearer "+tok) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("redeem with correct token (header): got %d want 200: %s", rec.Code, rec.Body.String()) } if pasteExists(t, h, id) { t.Fatal("paste still exists after authorized redeem") } // query param must NOT work on redeem either id, tok = createTestPaste(t, h) rec = doReq(t, h, "DELETE", "/api/pastes/"+id+"/redeem?token="+tok, "", "") if rec.Code != http.StatusBadRequest { t.Fatalf("redeem via ?token= query: got %d want 400 (#143)", rec.Code) } if !pasteExists(t, h, id) { t.Fatal("paste was hard-deleted via ?token= query param (#143 regression)") } } func TestDeleteByCreatorViewerCookieStillAllowed(t *testing.T) { s := testServer(t) h := s.routes() // create from a specific browser req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(`{"content":"mine"}`)) req.Header.Set("Content-Type", "application/json") req.AddCookie(&http.Cookie{Name: "vwr", Value: "creator-abc"}) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) var created struct { ID string `json:"id"` } json.Unmarshal(rec.Body.Bytes(), &created) // creator browser deletes without a token: allowed (#37 /mine delete button) rec = doReq(t, h, "DELETE", "/api/pastes/"+created.ID, "creator-abc", "") if rec.Code != 200 { t.Fatalf("creator delete: got %d want 200", rec.Code) } // a different browser is still forbidden id, _ := createTestPaste(t, h) rec = doReq(t, h, "DELETE", "/api/pastes/"+id, "someone-else", "") if rec.Code != http.StatusForbidden { t.Fatalf("other browser delete: got %d want 403", rec.Code) } if !pasteExists(t, h, id) { t.Fatal("paste deleted by unrelated browser") } } func TestDeletionAuthorizationExtract(t *testing.T) { mk := func(hdr, q string) *http.Request { req := httptest.NewRequest("DELETE", "/api/pastes/x"+q, nil) if hdr != "" { req.Header.Set("Authorization", hdr) } return req } cases := []struct { hdr, q, want string }{ {"", "", ""}, {"Bearer tok", "", "tok"}, {"bearer tok", "", "tok"}, {"Token tok", "", "tok"}, {"tok", "", "tok"}, {"", "?token=q", ""}, // #143: query tokens are never accepted {"Bearer hdr", "?token=q", "hdr"}, // header only } for _, c := range cases { if got := deletionAuthorization(mk(c.hdr, c.q)); got != c.want { t.Errorf("deletionAuthorization(hdr=%q q=%q) = %q want %q", c.hdr, c.q, got, c.want) } } }