fix: input validation gaps (#68)
CI / test (pull_request) Successful in 24s
CI / docker (pull_request) Skipped

- enforce server-side body cap via http.MaxBytesReader: oversized JSON
  bodies are rejected with 413 instead of being fully decoded first
- negative burn_after_reads rejected with 400 (zero still = default 1)
- limit=0 explicitly maps to default page size; shared parseLimit clamp
  for /api/public and /api/mine (huge/non-numeric values too)
- negative/non-numeric offset clamped to 0 via parseOffset (was
  pass-through)
- regression tests in issue68_validation_test.go

expires_at/expires_in validation intentionally excluded: covered by #60.
This commit is contained in:
agent
2026-09-09 09:21:25 -05:00
parent 03bf327f6b
commit 94a4a3c2ec
3 changed files with 306 additions and 15 deletions
+16 -15
View File
@@ -59,6 +59,7 @@ func (a *apiServer) routes() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Use(a.limitRequestBody) // #68: hard server-side body cap -> 413
r.Use(viewerCookieMiddleware)
// admin (#40): HTML page is open (key entry via form); API is key-guarded
@@ -152,16 +153,22 @@ func (a *apiServer) handleCreatePaste(w http.ResponseWriter, r *http.Request) {
}
var p store.Paste
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
if isBodyTooLarge(err) { // #68: body cut off by MaxBytesReader
writeBodyTooLarge(w)
return
}
writeErr(w, 400, "invalid json body")
return
}
if strings.TrimSpace(p.Content) == "" {
writeErr(w, 400, "content is required")
if status, msg := checkContent(p.Content, s.MaxContentBytes); status != 0 {
writeErr(w, status, msg)
return
}
if int64(len(p.Content)) > s.MaxContentBytes { // #40: admin-tunable
writeErr(w, 413, fmt.Sprintf("content exceeds max %d bytes", s.MaxContentBytes))
return
if p.BurnAfterReads != nil { // #68: reject negative read budgets
if err := parseBurnAfterReads(*p.BurnAfterReads); err != nil {
writeErr(w, 400, err.Error())
return
}
}
// #40: admin-configurable default expiry
if (p.ExpiresIn == nil || *p.ExpiresIn == "") && s.DefaultExpiry != "" {
@@ -254,11 +261,8 @@ func (a *apiServer) handleListMine(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"total": 0, "items": []any{}})
return
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 || limit > 100 {
limit = 50
}
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
limit := parseLimit(r, 50, 100)
offset := parseOffset(r)
rows, total, err := a.store.ListMine(vid, limit, offset)
if err != nil {
writeErr(w, 500, "db error")
@@ -277,11 +281,8 @@ func (a *apiServer) handleListMine(w http.ResponseWriter, r *http.Request) {
}
func (a *apiServer) handleListPublic(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 || limit > 100 {
limit = 25
}
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
limit := parseLimit(r, 25, 100)
offset := parseOffset(r)
rows, total, err := a.store.ListPublic(limit, offset)
if err != nil {
writeErr(w, 500, "db error")