From f611cc3e9e5095a1b2596d97b104bdf530b86322 Mon Sep 17 00:00:00 2001 From: poslop Date: Wed, 9 Sep 2026 09:13:21 -0500 Subject: [PATCH 1/2] fix #60: clamp expires_in on cans API (400 on zero/negative, cap 1yr) --- internal/api/cans.go | 4 +-- internal/api/cans_expiry_test.go | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 internal/api/cans_expiry_test.go diff --git a/internal/api/cans.go b/internal/api/cans.go index c014e44..b609159 100644 --- a/internal/api/cans.go +++ b/internal/api/cans.go @@ -1,11 +1,11 @@ package api import ( - "palette/internal/store" "encoding/json" "fmt" "io" "net/http" + "palette/internal/store" "strings" "time" @@ -143,8 +143,6 @@ func detectContentType(name string, content []byte) string { return "text/plain" } - - func (a *apiServer) handleGetCan(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") can, err := a.store.GetCan(id) diff --git a/internal/api/cans_expiry_test.go b/internal/api/cans_expiry_test.go new file mode 100644 index 0000000..3b549d0 --- /dev/null +++ b/internal/api/cans_expiry_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "net/http/httptest" + "testing" +) + +// #60: the cans API must clamp expires_in at the boundary exactly like the +// pastes API — reject zero/negative durations and anything over the 1-year +// UI cap, accept the exact boundaries. +func TestCreateCanExpiryBounds(t *testing.T) { + s := testServer(t) + h := s.routes() + + cases := []struct { + expiresIn string + wantCode int + }{ + {"-1h", 400}, // negative + {"-0s", 400}, // negative zero + {"0s", 400}, // zero + {"1ns", 400}, // positive but below the 1-minute floor + {"59s", 400}, // just under the floor + {"1m", 201}, // exactly the floor + {"90s", 201}, // just over the floor + {"8760h", 201}, // exactly 1 year + {"8785h", 400}, // 1 year + 1 day: over the cap + {"87600h", 400}, // 10 years, the originally reported case + } + for _, c := range cases { + globalLimiter = newLimiter() // avoid create rate limit between cases + body, ct := multipartBody(t, map[string]string{ + "json_items": `[{"title":"a.txt","content":"AAA"}]`, + "expires_in": c.expiresIn, + }, "files", "pic.txt", "file data") + req := httptest.NewRequest("POST", "/api/pastes/can", body) + req.Header.Set("Content-Type", ct) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != c.wantCode { + t.Errorf("expires_in %q: got %d want %d (%s)", + c.expiresIn, rec.Code, c.wantCode, rec.Body.String()) + } + } +} From bb5c4f0186042a428bdbe5f2b303e6f39c66a7bc Mon Sep 17 00:00:00 2001 From: fen Date: Wed, 9 Sep 2026 09:15:39 -0500 Subject: [PATCH 2/2] fix #60: apply ValidExpiry clamp to cans create handler (was lost in working tree) --- internal/api/cans.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/api/cans.go b/internal/api/cans.go index b609159..6b0488e 100644 --- a/internal/api/cans.go +++ b/internal/api/cans.go @@ -40,6 +40,12 @@ func (a *apiServer) handleCreateCan(w http.ResponseWriter, r *http.Request) { writeErr(w, 400, "invalid expires_in") return } + // #60: clamp at the API boundary like the pastes API does - + // reject zero/negative and durations past the 1-year UI cap. + if !store.ValidExpiry(d) { + writeErr(w, 400, "expires_in must be between 1 minute and 1 year") + return + } t := now + int64(d.Seconds()) expiresAt = &t }